반응형

Intro


안녕하세요, 오늘은 python의 type hint에 대해서 알아보겠습니다.

python의 장점은 우리가 정의한 변수들의 타입을 알아서 동적으로 정해주는 것이죠. 하지만 이는 양날의 검이라고 볼 수있습니다. 프로그램 복잡도가 높아질수록, 많은 사람과 협업할수록 변수의 type을 파악해야 사전에 디버그할 수 있고, 적절한 로직을 짤 수 있습니다.

따라서 python에서는 이런 단점을 보완하기 위해서 type hint (a.k.a annotation) 라는 기능을 지원합니다. 이 변수의 type은 무엇인지, 또는 이 함수의 인수는 어떤 type이고, 어떤 type의 결과를 return하는지 명시해주는 것입니다. 이는 강제성은 없지만, mypy와 같은 type checker 또는 pycharm같은 IDE에서 검사를 해서 명시해준 type의 변수, 인수, 리턴 값이 나오지 않으면 경고를 보내줍니다.

하지만 이또한 강건한 프로그래밍이 가능하게 장점과 함께, 개발자의 노력과 시간이 더 소비된다는 단점도 있습니다. 저는 장점이 더 크다고 생각하기 때문에 Real Python 에 정리된 type hint 사용법을 보고 이글에 정리하고자 합니다.

Example1


원기둥 (cylinder)의 부피를 구하는 함수를 구현해보겠습니다. 원기둥의 부피를 구하는 공식은 $\pi r^2h$이고, 반지를 $r$과 높이 $h$만 알면 구할 수 있습니다. type hint를 적용해서 구현해보면 아래와 같습니다.

PI: float = np.pi

def get_cylinder_volume(radius: float, height: float = 1.) -> float:
    return PI * np.power(radius, 2) * height

get_cylinder_volume(2)
>> 12.566370614359172

get_cylinder_volume(int(2), 2)
>> 25.132741228718345

get_cylinder_volume.__annotations__
>> {'radius': float, 'height': float, 'return': float}

여기서 볼 point는 4가지 입니다.

  1. 처음 변수 PI를 선언할 때 type hint를 적용했습니다. :을 통해 type을 명시할 수 있습니다.
  2. get_cylinder_volume의 인수 type을 정해주고, return 값의 type은 float이라고 hint를 줬습니다.
  3. type hint와 다른 type의 인수가 전해진다고 해서 에러가 발생하지 않습니다. 하지만 type hint를 지키짐 않으면 나중에 어딘가에서는 에러가 발생할 수도 있겠죠!
  4. 함수의 __annotations__를 호출하면, 주어진 hint에 대한 정보가 저장되어있음을 할 수 있습니다. type checker 프로그램들은 이를 참고하겠죠?

추가) 함수가 어떤 값을 반환하지 않는 경우, -> None을 써줄수도 있고, 아예 안적어주는 것도 괜찮습니다.

Example2


float, int, str같은 기본적인 type 쉽게 hint를 줄 수 있지만, list, tupel, dict와 같은 경우는 추가적인 정보가 필요합니다. 단지 list로 끝날게 아니라, 어떤 type의 값들로 구성되어 있는지도 알아야 합니다. 따라서 이런 추가적인 정보를 제공하기 위해서는 typing모듈의 도움을 받아야 합니다. 위 제가 참고한 사이트의 예시를 인용하겠습니다.

from typing import Dict, List, Tuple

names: List[str] = ["Guido", "Jukka", "Ivan"]
version: Tuple[int, int, int] = (3, 7, 1)
options: Dict[str, bool] = {"centered": False, "capitalize": True}

3가지 변수와 type hint로 아래와 같이 정보를 알 수 있습니다.

  1. name: str type의 변수로 이루어진 list
  2. version: 3개의 값을 갖고있고, 3개 모두 int type 변수인 tuple, 값의 개수와 hint의 개수가 맞지 않으면 경고가 나갑니다.
  3. options: key값은 str, value값은 booldict

추가) 다루고자 하는 data가 list, tuple 둘 다 상관 없을 때는 좀 더 상위 개념인 Sequence를 사용할 수 있습니다. 하지만 웬만하면 정확하고, 통일성있도록 하는게 좋습니다.
추가) 만약 여러 type의 값을 갖고있는 list의 경우, List[Any], 또는 List[Union[int, str]]등으로 표현할 수 있습니다. 좀 더 명확한 Union이 더 좋을 거 같네요.

Example3

typing 모듈에서는 TypeVar라는 특별한 Type variable이 있습니다. 이는 상황에 따라서 어떤 type의 변수를 받을 수 있는 변수입니다. TypeVar은 주어진 값들을 모두 아우르면서 가장 specific한 type을 취합니다. 참고한 사이트의 예시를 인용하겠습니다.

아래와 같이 주어진 Sequence의 값 중 임의로 하나를 선택해주는 choose라는 함수를 정의했습니다.

import random
from typing import Sequence, TypeVar

Choosable = TypeVar("Choosable")

def choose(items: Sequence[Choosable]) -> Choosable:
    return random.choice(items)

주어지는 input에 따라 결정되는 Choosable의 type은 아래와 같습니다.

# str only
choose(["Guido", "Jukka", "Ivan"]) 
> Choosable: str
# int only
choose([1, 2, 3]) 
> Choosable: int
# bool & int & float
choose([True, 42, 3.14]) 
> Choosable: float
# str & int
choose(["Python", 3, 7]) 
> Choosable: object

마지막 예시에서는 strint사이의 공통 parent class가 없기 때문에 최상위 클래스인 object로 결정되었습니다.

만약 Choosable = TypeVar("Choosable", str, float) 처럼 선언하면, 마지막 예시처럼 정해지는 type이 str 또는 float이 아닌 경우에는 (int는 float의 하위 클래스라서 괜찮습니다.) 경고가 발생합니다.

마무리


오늘은 python의 type hint에 대해서 알아봤습니다. 오늘 소개한 것 말고도 함수를 인수로 넘겨줬을 때 줄 수 있는 hint인 Callable, 그리고 기본적으로 None을 내장하고 있어서 처음에는 None으로 초기화하고 나중에 값을 할당하고 싶을 때 사용할 수 있는 Optional hint도 있습니다.

글 읽어주셔서 감사합니다!

References


Python3.9 typing docs
[Real Python] type checking

반응형
반응형

Intro


안녕하세요, 오늘은 python의 built-in 모듈 중 하나인 timeit에 대해서 소개해드리겠습니다.

timeit은 우리가 작성한 코드의 실행 시간을 측정하는데 사용됩니다. 우리가 작성하고자 하는 로직을 구현하는 방법이 여러 가지가 있을 때, 더 빠른 방법을 선택하기 위한 테스트를 진행할 때 timeit을 사용합니다.

timeit을 사용하는 방법은 크게 두가지가 있습니다.

  1. timeit모듈 import해서, timeit.timeit() 사용하기
  2. IPython magic command %%timeit 사용하기

저는 jupyter notebook에서 두 번째 방법을 이용해서 수행 시간을 비교하는 테스트를 해보겠습니다.

Test1: list comprehension vs. map function


python의 기본 기능인 list comprehesion과 map의 속도를 비교해보겠습니다.

%%timeit
"-".join([str(n) for n in range(100000)])
>> 21.7 ms ± 1.37 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit
"-".join(map(str, range(100000)))
>> 17.1 ms ± 653 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

map이 list comprehension보다 빠른 것을 확인할 수 있습니다. 여기서 한가지 차이 점은, list comprehension 실험 결과는 10 loops, map 실험 결과는 100 loops를 돌았다고 나옵니다. 이는 %%timeit magic command를 사용해서 속도를 측정할 때, 정확한 측정을 위해 해당 코드를 여러 번 실행해서 걸리는 시간을 평균냅니다. 그리고 이 숫자는 연산의 복잡도에 따라서 자동으로 정해집니다. (간단한 연산은 더 많이, 복잡한 연산은 더 조금)

위 실험의 경우 map 실험이 복잡도가 더 낮은 연산으로 구분되어서, list comprehension실험보다 많은 loop가 돈 것을 확인할 수 있습니다.

만약에 loop수를 통일하고 싶다면, -n [NUM_LOOPS] 인자를 주면 됩니다. 아래처럼요

%%timeit -n 10
"-".join(map(str, range(100000)))
>> 17.7 ms ± 1.33 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Test2: (pandas) list comprehension vs. map function


pandas DataFrame에도, 특정 columns의 값에 동일한 함수를 적용시켜주는 map함수가 존재합니다. 하지만 듣기로는 pandas의 map함수는 잘 설계되어있지 않다고 들었는데요, 실제로 그런지 확인해보겠습니다.

아래와 같이 데이터를 준비하겠습니다.

import pandas as pd
import numpy as np

data_2d = np.random.rand(100000, 5)
df = pd.DataFrame(data_2d, columns=[f'data{idx}' for idx in range(len(data_2d[1]))])
df.head()
>>  data0        data1        data2        data3        data4
0    0.275805    0.693519    0.644440    0.277043    0.173847
1    0.598473    0.796305    0.106011    0.824349    0.061107
2    0.664825    0.998174    0.575312    0.225689    0.274685
3    0.577050    0.705411    0.800339    0.623300    0.254203
4    0.828888    0.291775    0.347555    0.277151    0.27

그리고 값을 소수점 2자리 까지만 고려해주는 round_value함수를 정의해준 뒤, list comprehension과 map의 속도를 비교해보겠습니다.

def round_value(x):
    return np.round(x, 2)

%%timeit
df['data0'].map(round_value)
>> 989 ms ± 17.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)


%%timeit
[round_value(value) for value in df['data0'].values]
>> 692 ms ± 38.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

소문이 사실이었습니다. pandas의 경우, map보다 list comprehension의 속도가 훨씬 빨랐습니다!

map을 사용했을 때 코드 가독성이 더 좋긴 하지만, 속도가 큰 차이가 나니 앞으로는 list comprehension을 사용하는 것이 좋겠네요.

Test3: (pandas) loc( ) vs. at( ) vs. iloc( ) vs. iat( )

위 4가지 함수들은 모두 DataFrame상에서 특정위치의 값을 가져오거나, 바꿀 때 사용할 수 있습니다.

앞에 i가 붙은 함수들을 index를 통해서만 접근이 가능하고, 그렇지 않는 함수들은 column 이름 등을 통해서 접근이 가능합니다.

또한 (i)at(i)loc의 차이점은, 한 번에 하나의 값에 접근할 것인지, 여러 값에 접근할 것인지에 따라 차이가 있습니다. 즉, (i)at가 하나의 값을 찾거나 수정하는데 더 특화되어 있다고 볼 수 있습니다.

바로 속도 비교 실험을 진행하겠습니다.

# at
%%timeit
[df.at[idx, 'data1'] for idx in df.index]
>> 401 ms ± 31.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# loc
%%timeit
[df.loc[idx, 'data1'] for idx in df.index]
>> 661 ms ± 13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# iat
%%timeit
[df.iat[idx, 1] for idx in df.index]
>> 1.9 s ± 22.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# iloc
%%timeit
[df.iloc[idx, 1] for idx in df.index]
>> 2.16 s ± 16.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

실험 결과, (i)loc보다는 (i)at이 빠르고, i로 시작하는 함수보다는 그렇지 않은 함수가 더 빠르단 걸 알 수 있습니다. 신기하네요, 상황에 따라서 i가 붙은 함수를 사용할 수 있겠지만, 가능하면 하나의 값만 가져올 때는 at, 여러 개의 값을 가져올 때는 loc를 사용하는 것이 좋겠네요.

마무리


오늘은 python의 timeit 모듈에 대해서 알아보고, 평소에 궁금했던 것들을 테스트해 봤습니다. 글 읽어주셔서 감사합니다!

References


Python3.8 timeit docs
IPython timeit magic commands

반응형
반응형

Intro


안녕하세요, 오늘은 python의 Mixin에 대해서 정리해보고, 제가 이해하는데 도움이 됐던 코드를 소개해드리겠습니다.

Mixin vs. Inheritance (상속)


Mixin은 Inheritance의 한 종류 입니다. 하지만 쓰임에 있어서 일반적인 inheritance와 미묘하게 다른 차이가 있습니다.

간단하게 말하자면 일반적인 inheritance에서는 핵심 기능을 부모 class에, 부가적인 기능을 자식 class에 구현합니다.

예를 들면 부모 class는 Car, 자식 class는 SportsCar, Truck 등이 될 수 있습니다.

다만 mixin은 어떤 핵심 기능을 구현한 class가 부가적인 기능을 추가하기 위해 특정 클래스를 상속 받습니다.

마치 닭과 달걀의 관계와 유사하다고 느껴지실 수 있겠지만, mixin은 다양한 기능이 조합되어야 하는 문자열 처리, 또는 프레임워크를 사용할 때 주로 쓰입니다.

아래 두 가지 예시 코드를 보겠습니다.

Tokenizer with Mixin


주어진 문자열을 -을 기준으로 분리해주는 tokenizer인 BaseTokenizer 클래스가 있습니다. 이를 상속한 Tokenizer 클래스가 있는데, 여기에 각 token을 대문자로 바꿔주는 기능을 추가하고 싶습니다. 즉 주기능은 tokenize인데, 여기에 대문자 변환 기능을 추가하고 싶은 것이죠.

여기서 의문이 생길 수 있습니다. 그냥 Tokenizer 클래스에 대문자 변화 method를 추가해주면 되니까요.

하지만, tokenizer는 tokenizer여야 합니다.

따라서 클래스 안에 부가적인 이것 저것 기능을 구현하기 보다는 필요한 기능이 구현된 클래스를 상속하는게 더 좋은 코드가 되는 것이죠.

아래 예시 코드는 파이썬 클린 코드의 코드를 가져왔습니다.

class BaseTokenizer: 
    def __init__(self, str_token): 
        self.str_token = str_token 
    def __iter__(self): 
        yield from self.str_token.split("-")

class UpperIterableMixin: 
    def __iter__(self): 
        return map(str.upper, super().__iter__()) 

class Tokenizer(UpperIterableMixin, BaseTokenizer): 
    pass

tokens = Tokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0")
list(tokens)

>> ['28A2320B', 'FD3F', '4627', '9792', 'A2B38E3C46B0']

이와 같이, UpperIterableMixin을 상속함으로써 클래스에 추가적인 기능을 지원해주는 것을 Mixin이라고 할 수 있습니다.

Database with Mixin

아래 코드는 마이크로소프트에서 제공하는 Mixin 소개 영상의 코드를 가져왔습니다.

우리가 어떤 framework를 사용하려고 합니다.framework는 다양한 기능을 제공하죠, 특히 database에 접근할 때는, database에 접속하고, 관련 log를 남겨야 합니다.

하지만 앞서 말했듯이, 접속과 로깅은 database만의 기능이 아닙니다. database는 데이터만 잘 가져오고, 저장하면 됩니다. 따라서 이러한 기타 기능들을 추가하기 위해서 mixin을 사용할 수 있습니다. 아래 코드의 LoggableConnection이 해당합니다.

Database를 다루는 framework는 이러한 기능이 구현되었는지 확인한 뒤, 기능을 수행합니다.

class Loggable:
    def __init__(self):
        self.title = ''
    def log(self):
        print('Log message from ' + self.title)

class Connection:
    def __init__(self):
        self.server = ''
    def connect(self):
        print('Connecting to database on ' + self.server)

class SqlDatabase(Connection, Loggable):
    def __init__(self):
        self.title = 'Sql Connection Demo'
        self.server = 'Some_Server'

def framework(item):
    if isinstance(item, Connection):
        item.connect()
    if isinstance(item, Loggable):
        item.log()

sql_connection = SqlDatabase()
framework(sql_connection)

>> Connecting to database on Some_Server
>> Log message from Sql Connection Demo

마무리


오늘은 python의 mixin에 대해서 정리했습니다. 글 읽어주셔서 감사합니다!

References


파이썬 클린 코드
Mixin 소개 영상

반응형
반응형

Intro


안녕하세요, 오늘은 decorator에 대한 마지막 정리 글입니다. 앞선 글들을 통해 우리는 decorator란 무엇인지, 어떤 기능을 하는지, 그리고 @property, @staticmethod, @classmethod와 같은 기본 decorator들에 대해서 알게되었습니다.

이번 글은 decorator 정리에 대한 마지막 글로, 제가 프로그래밍하면서 마주쳤던 그 외 decorator인 dataclass와 functools에 대해서 정리하겠습니다.

@dataclass


@dataclass는 class를 선언할 때 class위에 붙여주는 decorator입니다. 이 decorator가 붙은 class는 dataclass가 되고, dataclass의 instance는 아래의 기능들을 수행할 수 있게 됩니다.

@dataclass를 사용하기 위해서는 dataclass 함수를 import 해줘야 합니다.

from dataclass import dataclass
  1. intance를 출력하면 갖고 있는 필드 값 나열 (repr)
  2. hashable해서 dict나 set에 추가 가능
  3. 대소관계 비교
  4. 필드 값 변경 방지

위의 기능을 무조건 구현하는 것은 아니고, @dataclass decorator의 인수를 조절해서 원하는 기능만 추가할 수 있습니다.
인수의 종류 및 기본 값은 아래와 같습니다.

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

  • init: __init__이 구현되어 있지 않은 경우에 instance를 생성할 때 받은 값들을 자동으로 instance의 필드 값으로 매핑해주는 __init__함수를 추가해줍니다.
  • repr: __repr__을 추가해줍니다. 이를 추가하면 instance를 print했을 때, instance가 갖고있는 필드 값들을 알 수 있습니다.
  • eq: __eq__를 추가해줍니다. 이를 추가하면 instance가 갖고있는 필드 값들이 모두 동일하면 instance 비교 연산 (==)에서 True를 반환합니다.
  • order: __lt__, __le__, __gt__, __ge__을 추가해줍니다. 이를 추가하면 instance사이의 필드 값에 기반한 대소관계 비교가 가능해집니다.
  • unsafe_hash: __hash__를 추가해줍니다. instance를 hashable하게 만들어줘서 dict나 set의 key로 사용될 수 있습니다.
  • frozen: instance의 필드를 불변하게 만듭니다.

이렇듯, @dataclass는 우리가 원하는 기능을 구현하기 위해서 번거롭게 반복해야 하는 단순한 로직들을, 손쉽게 추가할 수 있도록 돕습니다.

주의) 넘겨주는 인자의 조합, 또는 기존 class에 추가하고자 하는 기능을 담당하는 매직 메소드 (__~~~__)가 이미 추가되어있을 시, 에러가 발생할 수 있습니다. 이에 대해서는 python의 dataclass구현 코드에 적힌 doc string을 참고하셔서 미리 예방하는 것을 추천드립니다.

@dataclass와 관련된 code test는 이미 너무 잘 정리된 블로그 글을 발견해서, 해당 URL을 남기는 것으로 대체하도록 하겠습니다.
[파이썬] 데이터 클래스 사용법 (dataclasses 모듈)

functools


functools는 함수에 여러 기능을 부여해줄 수 있는 모듈입니다. 오늘은 functools가 제공해주는 여러 기능 중 cached, cached_property에 대해서 알아보겠습니다.

@cache decorator는 함수가 이전에 받았던 인수에 대한 결과값을 caching하는 것 입니다. 메모리는 좀 소모되겠지만, 중복 연산을 피할 수 있습니다.

피보나치 함수는 잘못 구현하면 내부에서 무수히 많은 recursive call이 발생해서 결과 값을 얻지 못할 수 있습니다. 그래서 이전에는 중복 연산이 발생하지 않도록 임의로 로직을 구현했지만, @cache를 사용하면 쉽게 해결할 수 있습니다.

아래는 예시입니다.

from functools import cache
import time

@cache
def fibonacci_cached(n: int) -> int:
    if n in [0, 1]:
        return 1
    else:
        return fibonacci_cached(n-1) + fibonacci_cached(n-2)

def fibonacci_uncached(n: int) -> int:
    if n in [0, 1]:
        return 1
    else:
        return fibonacci_uncached(n-1) + fibonacci_uncached(n-2)

num = 40
print(f"start fibonacci{num} w/ cache")
start = time.time()
print(fibonacci_cached(40))
end = time.time()
print(f"result: {end - start} sec\n")

print(f"start fibonacci{num} w/o cache")
start = time.time()
print(fibonacci_uncached(40))
end = time.time()
print(f"result: {end - start} sec")

결과는 아래와 같습니다. @cache가 붙은 fibonacci_cached()의 실행 결과가 훨씬 빠름을 알 수 있습니다.

start fibonacci40 w/ cache
165580141
result: 0.0 sec

start fibonacci40 w/o cache
165580141
result: 48.55391979217529 sec

@cached_property decorator는 @cache와 달리 이전의 결과 값들을 모두 저장하지 않습니다. @cached_property를 통해 caching 되는 것은 @cached_property가 붙은 함수의 이름과 결과값이, instance의 새로운 필드와 필드 값으로 caching됩니다. 그리고 역시 호출할 때 마다 새롭게 실행되지 않고, caching된 값을 반환합니다.

아래는 예시입니다.

from functools import cached_property
import time

import numpy as np

class DataSet:
    def __init__(self, sequence_of_numbers: np.ndarray):
        self._data = tuple(sequence_of_numbers)

    @property
    def stdev_property(self) -> np.float64:
        return np.std(self._data)

    @cached_property
    def stdev_cached_property(self):
        return np.std(self._data)

dataset = DataSet(np.random.rand(1000))

print('Before stdev_property')
print(dataset.__dict__.keys())
print('\nAfter stdev_property')
dataset.stdev_property
print(dataset.__dict__.keys())
print('---------------------------------------')
print('\nBefore stdev_cached_property')
print(dataset.__dict__.keys())
print('\nAfter stdev_cached_property')
dataset.stdev_cached_property
print(dataset.__dict__.keys())

print('---------------------------------------')
print('\nstdev_cached_property value before _data changed')
print(dataset.stdev_cached_property)
dataset._data = np.random.rand(100)
print('\nstdev_cached_property value After _data changed')
print(dataset.stdev_cached_property)

결과는 아래와 같습니다. @cached_property가 붙은 stdev_cached_property()의 실행 결과 instance의 새로운 필드로 추가된고, 이후에 호출하면 재 실행없이 caching된 값이 반환되는 것을 볼 수 있습니다.

Before stdev_property
dict_keys(['_data'])

After stdev_property
dict_keys(['_data'])
---------------------------------------

Before stdev_cached_property
dict_keys(['_data'])

After stdev_cached_property
dict_keys(['_data', 'stdev_cached_property'])
---------------------------------------

stdev_cached_property value before _data changed
0.289598527937108

stdev_cached_property value After _data changed
0.289598527937108

마무리


4개의 글에 거쳐서 decorator에 대해서 정리해봤습니다! 모든 decorator를 다루진 못했지만, decorator의 메카니즘에 대해서 이해할 수 있었던 계기였던 거 같습니다.

감사합니다.

References


Python 3.9 dataclass doc string
Python 3.9 - dataclass documentation
[파이썬] 데이터 클래스 사용법 (dataclasses 모듈)

반응형
반응형

Intro


안녕하세요, 오늘은 우리가 python에서 자주 쓰이는 decorator인 @classmethod@staticmethod 대해서 정리해보려고 합니다.

지난 글(LINK)에서 decorator @뒤에는 decorator를 붙인 함수실행된 뒤 실행할 함수의 이름이 온다고 했습니다.

네, classmethodstaticmethod역시 함수 이름입니다. 이들은 각각 앞서 실행된 함수를 class methodstatic method로 변환해주는 함수입니다. class method와 static method는 class나 instance를 통해서 접근할 수 있습니다.

참고) 두 decorator 모두 class내에서 선언되어 사용됩니다. 일발적으로 우리가 class안에 선언해서 첫 번째 인자로 self를 받는 method를 instance method라고 합니다.

instance method, class method, static method


intance method는 우리가 일반적으로 class 내부에 정의하는 self라는 인수를 첫 번째로 받는 method입니다. 이때 우리는 class의 instance를 생성해야만 instance method를 실행할 수 있습니다.

반면에 class methodstatic method는 객체가 없어도 실행될 수 있습니다. 그렇다면 둘의 차이점은 무엇일까요?

그것은 바로 class method는 첫 번째 인자로 항상 class를 받고(일반적으로 cls라고 이름 붙힙니다.), static method는 아무런 인자나 자유롭게 받을 수 있다는 겁니다.

이는 class method는 class내에서 _공유되는 variable, class method, 그리고 static method_에 접근할 수 있고, static method는 다른 variable나 method에는 접근할 수 없고, 인자로 받은 값만으로 할 수 있는 기능을 수행합니다.

따라서 class의 instance들끼리 공유하는 variable이나 class/ static method를 다루는 method를 정의할 때는 @classmethod를 붙여줍니다.

static method는 사실 class내에 꼭 있을 필요는 없지만, class와 연관성이 있는 method를 안에 정의하고 class, instance를 통해 사용함으로써 코드 가독성을 높일 수 있고, 이 경우 @staticmethod를 붙여줍니다.

Code test


간단한 예시를 통해서 @classmethod, @staticmethod decorator의 쓰임을 확인해보겠습니다.
오직 기능 확인만을 목적으로 만든 허접한 예시입니다.

class Employee():
    company = "저승네트워크"

    def __init__(self, name, enter_year):
        self.name = name
        self.enter_year = enter_year

    def self_introduction(self):
        print(f'안녕하세요, 저는 {self.company}에 {self.enter_year}년에 입사한 {self.name} 이라고 합니다.')

    @classmethod
    def company_introduction(cls):
        print(f'저는 {cls.company}에 다닙니다.')

    @classmethod
    def recommendation_with_company(cls, friend):
        cls.recommendation(friend)
        print(f'{cls.company}에 꼭 필요한 사람입니다.')

    @staticmethod
    def recommendation(friend):
        print(f'제 친구 {friend}를 우리 회사에 추천합니다.')

class variable company, instance variable name, enter_year, instance method __init__(), self_introduction(), classmethod company_introduction(), recommendation_with_company(), static_method recommendation을 선언했습니다.

이들을, class 또는 instance를 통해서 사용해보겠습니다.

Employee를 통해 접근할 수 있는 것은 class variable, class method, static method입니다. 그 외에는 instance가 있어야합니다.

instance를 생성한 뒤에는 어느 것이든 접근할 수 있습니다. 애초에 instance들 끼리 서로 공유하라고 class variable/method와 static method가 있는 것이니까요.

마무리


오늘은 python의 @classmethod, @staticmethod decorator에 대해서 살펴봤습니다. 좋은 하루 보내세요!

References


파이썬 - OOP Part 4. 클래스 메소드와 스태틱 메소드 (Class Method and Static Method) (LINK)

반응형
반응형

Intro


안녕하세요, 지난글에서는 python의 decorator에 대해서 간단히 살펴봤습니다.
이번 글에서는 python에서 기본으로 제공해주면서 많이 쓰이는 @property decorator에 대해서 정리하겠습니다.

@property


@property decorator는 많은 분들이 private 변수의 getter, setter를 설정하기 위해서 많이 사용한다고 해서, 그 용도로만 쓰이는 건줄 알았습니다. 하지만 조금 더 알아본 결과, 제가 생각하는 @property decorator의 본질은 "function(함수)을 variable(변수)처럼 사용하기" 입니다.

예를 들면 아래와 같습니다.

class PropertyTest():
    @property
    def func_with_property(self):
        print("called with property")

    def func_without_property(self):
        print("called without property")

if __name__ == "__main__":
    pt = PropertyTest()

    tc.func_with_property
    ## output: called with property

    tc.func_without_property
    ## output: <bound method PropertyTest.func_without_property of <__main__.PropertyTestClass object at 0x0000016FC1D827F0>>

    tc.tc.func_without_property()
    ## output: called without property

위의 예시를 보시면, @property 를 위에 적은 func_with_property를 호출할 때는 ()를 안붙여도 그 반환값을 알 수 있습니다.
이러한 특성을 활용해서 private 변수의 getter, setter를 설정할 수 있는 겁니다.

우선 python에서도 private 변수를 설정할 수 있는 걸 모르는 분이 있을 수 있는데요, 변수이름 앞에 __ 또는 _를 붙여서 외부로부터 접근을 제어할 수 있습니다. 그래서 private 변수에 접근하기 위한 getter와 setter가 필요한 것이죠.

python의 private 변수와 관련한 내용은 따로 다루도록 하겠습니다.

먼저 일반적인 getter, setter를 이용한 private 변수와 public 변수로의 접근을 비교해보겠습니다.

class GetterSetterWithFunc():
    def __init__(self, pri_var, pub_var):
        self.__pri_var = pri_var
        self.pub_var = pub_var

    def get_pri_var(self):
        return self.__pri_var

    def set_pri_var(self, pri_var):
        self.__pri_var = pri_var

if __name__ == "__main__":
    gswf = GetterSetterWithFunc(1, 10)

    gswf.__pri_var
    ## output: AttributeError

    gswf.pri_var
    ## output: AttributeError

    gswf.get_pri_var()
    ## output: 1

    gswf.set_pri_var(100)
    gswf.get_pri_var()
    ## output: 100

    gswf.pub_var
    ## output: 10

    gswf.pub_var = 1000
    gswf.pub_var
    ## output: 1000

위의 예시와 같이, private 변수인 pri_varget_pri_var()set_pri_var()을 통해서만 접근이 가능한 반면, public 변수인 pub_var은 바로 접근이 가능합니다.

그런데, private 변수와 public 변수를 다룰 때 이를 구분해서 사용하기 불편하기도 하죠, @property decorator를 사용하면 private, public상관없이 다룰 수 있습니다. 어떻게 달라지는지 보겠습니다.

class GetterSetterWithProperty():
    def __init__(self, pri_var, pub_var):
        self.pri_var = pri_var
        self.pub_var = pub_var

    @property
    def pri_var(self):
        return self.__pri_var

    @pri_var.setter
    def pri_var(self, pri_var):
        self.__pri_var = pri_var

if __name__ == "__main__":
    gswp = GetterSetterWithProperty(1, 10)

    gswp.pri_var
    ## output: 1

    gswp.pri_var = 100
    gswp.pri_var
    ## output: 100

    gswp.pub_var
    ## output: 10

    gswp.pub_var = 1000
    gswp.pub_var
    ## output: 1000

이와 같이, @property를 사용하면 private, public 변수를 같은 방법으로 다룰 수 있게 됩니다. 다만 차이점이 있다면, __init__()에서 호출되는 self.pri_var는 변수가 아닌 아래에 정의된 pri_var function이고, private 변수인 pri_var는 해당 function을 실행할 때 생성 됩니다. 반면 pub_var__init__()에서 바로 생성됩니다.

마무리


오늘은 python의 기본 decorator 중 하나인 @property decorator에 대해서 정리해봤습니다. 다음 글에서는 @staticmethod, @classmethod에 대해서 정리하겠습니다.

글 읽어주셔서 감사합니다!

반응형
반응형

Intro


안녕하세요, 오늘은 우리가 python코드를 보다보면 발견할 수 있는 decorator에 대해서 정리해보려고 합니다.
decorator는 class나 method를 정의할 때, 그 위에 @로 시작하는 구문을 얘기합니다.

Java의 annotation vs. Python의 decorator


Java언어에도 @을 사용하는 annotation 이 있죠.

하지만, 두 단어의 이름이 다르듯이, 의미또한 다릅니다.참고

Java의 _annotation_은 @을 달아주는 객체에 대한 추가적인 meta data를 제공해주는 역할을 합니다. 이것 자체가 어떤 로직으로서의 역할을 한다기 보다는, 컴파일러가 이 정보를 참고해서 코드를 실행하는데 도움을 주는 역할로 보면 될 거 같습니다. (잘 활용하면 로직으로서도 사용이 가능하다는 거 같은데... 정확히는 모르겠습니다.)

반면에 Python의 decorator는 로직으로서의 역할을 합니다. 즉, 정의 위에 decorator가 붙은 함수를 실행하면, 그 함수 이외에 실행되는 로직이 있고, 그 로직을 수행할 함수의 이름을 @뒤에 써주는 겁니다.

Code test


간단한 예시를 통해서 decorator의 쓰임을 확인해보겠습니다.
default_path에 위치한 config.json 파일의 전체 filepath를 반환해주는 함수 get_config_fp()이 있다고 합시다.
그런데 처음에 이 함수를 짤 때, 실수로 인자로 받은 default_path를 그대로 반환하게 실수했다고 합니다. (또는 기존의 함수에 어떤 기능을 추가하고 싶다고 합시다.)

def get_config_fp(default_path):
    return default_path

decorator를 사용하면, 원래 함수의 수정없이 원하는 기능을 추가해줄 수 있습니다. decorator의 활용법을 두루 익히기 위해서, 불필요하지만 아래 2개의 decorator역할을 해줄 함수를 정의했습니다.

## default_path뒤에 "/"를 추가해주는 decorator
def add_slash(func):
    def slash_decorator(default_path):
        return f"{func(default_path)}/"
    return slash_decorator

## default_path뒤에 파일 이름을 추가해주는 decorator
def add_fn(filename):
    def fn_decorator(func):
        def func_wrapper(default_path):
            return f"{func(default_path)}{filename}"
        return func_wrapper
    return fn_decorator

add_slash는 기능을 추가하고자 하는 함수 외에 다른 인자를 받지않는 decorator이고, add_fn는 다른 인자도 함께 받는 decorator입니다.
위의 구현에서도 보이듯이, 추가 인자를 받지 않는 add_slash는 func -> func의 인자 순으로 입력을 받고, add_fn은 추가 인자 -> func -> func의 인자 순으로 입력을 받아 처리하는 것을 알 수 있습니다.

decorator의 동작을 조금더 자세히 보기 위해서, get_config_fp()와 같은 기능을 하는 함수들을 아래와 같이 정의했습니다.

def get_config_fp(default_path):
    return default_path

@add_slash
def get_config_fp2(default_path):
    return default_path

@add_fn('config.json')
@add_slash
def get_config_fp3(default_path):
    return default_path

보시는 바와 같이 decorator는 여러 개를 사용할 수 있습니다. 실행되는 순서는 target fucntion과 가까운게 먼저 실행됩니다.

전체 코드 및 수행 결과는 아래와 같습니다.

마무리


오늘은 python의 decorator에 대해서 간단히 살펴봤습니다.
다음에는 python에서 많이 사용되는 주요 decorator들과 그 사용법에 대해서 정리하겠습니다.
감사합니다. 좋은 하루 보내세요!

References


PEP 318 -- Decorators for Functions and Methods(LINK)

반응형

+ Recent posts