언어 Language/파이썬 Python

Q. staticmethod와 classmethod 데코레이터decorator의 차이는?

Tap to restart 2023. 3. 14. 19:00

A. staticmethod는 클래스에 접근이 안 되고, classmethod는 접근이 된다. 메소드 위에 두 데코레이터를 사용할 경우 객체 생성 없이 바로 메소드를 사용할 수 있다. 

 

예제

쉽게 이해할 수 있는 예는 아래와 같다.

class Calculator:
    PI = 3.141592

    def sum(self, num1, num2):
        return num1 + num2

    @staticmethod
    def static_sum(num1, num2):
        return num1 + num2

    def get_circle_area(self, radius):
        return radius * radius * self.PI

    @classmethod
    def class_get_circle_area(cls, radius):
        return radius * radius * cls.PI


if __name__ == "__main__":
    print(Calculator.static_sum(3, 5))
    print(Calculator.class_get_circle_area(4))

    calculator = Calculator()
    print(calculator.sum(2, 3))
    print(calculator.static_sum(3, 5))
    print(calculator.get_circle_area(4))
    print(calculator.class_get_circle_area(4))

출력 결과는 다음과 같다.

8
50.265472
5
8
50.265472
50.265472

 
staticmethod를 사용한 static_sum 메소드는 PI에 접근할 수 없다. 자바나 c++의 staticmethod와 비슷하다.
PI에 접근이 필요한 class_get_circle_area는 PI에 cls.PI로 접근할 수 있다. 위 예에서 알 수 있듯이 staticmethod나 classmethod 둘다 객체 생성 없이 바로 사용 가능하다. 물론 객체 생성한 뒤 객체로부터 사용도 가능하다.
 

python 기본 제공 모듈 사례

좀 찾아보면 classmethod는 굉장히 많이 사용하고 있다는 것을 알 수 있다.
 
출처: builtins.pyi

class str(Sequence[str]):
	...
    @staticmethod
    @overload
    def maketrans(__x: dict[int, _T] | dict[str, _T] | dict[str | int, _T]) -> dict[int, _T]: ...
    @staticmethod
    @overload
    def maketrans(__x: str, __y: str, __z: str | None = ...) -> dict[int, int | None]: ...
    ...
class float:
    def __new__(cls: Type[_T], x: SupportsFloat | SupportsIndex | str | bytes | bytearray = ...) -> _T: ...
    def as_integer_ratio(self) -> tuple[int, int]: ...
    def hex(self) -> str: ...
    def is_integer(self) -> bool: ...
    @classmethod
    def fromhex(cls, __s: str) -> float: ...
    ...

출처: collections/__init__.pyi

class OrderedDict(dict[_KT, _VT], Reversible[_KT], Generic[_KT, _VT]):
    def popitem(self, last: bool = ...) -> tuple[_KT, _VT]: ...
    def move_to_end(self, key: _KT, last: bool = ...) -> None: ...
    def copy(self: Self) -> Self: ...
    def __reversed__(self) -> Iterator[_KT]: ...
    def keys(self) -> _OrderedDictKeysView[_KT, _VT]: ...
    def items(self) -> _OrderedDictItemsView[_KT, _VT]: ...
    def values(self) -> _OrderedDictValuesView[_KT, _VT]: ...
    # The signature of OrderedDict.fromkeys should be kept in line with `dict.fromkeys`, modulo positional-only differences.
    # Like dict.fromkeys, its true signature is not expressible in the current type system.
    # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963.
    @classmethod
    @overload
    def fromkeys(cls, iterable: Iterable[_T], value: None = ...) -> OrderedDict[_T, Any | None]: ...
    @classmethod
    @overload
    def fromkeys(cls, iterable: Iterable[_T], value: _S) -> OrderedDict[_T, _S]: ...
    # Keep OrderedDict.setdefault in line with MutableMapping.setdefault, modulo positional-only differences.
    @overload
    def setdefault(self: OrderedDict[_KT, _T | None], key: _KT) -> _T | None: ...
    @overload
    def setdefault(self, key: _KT, default: _VT) -> _VT: ...

 

numpy 사례

출처: github numpy
가장 인기 많은 파이썬 팩키지 중 하나인 numpy에서도 staticmethod와 classmethod 사례를 찾을 수 있다. 위 예제처럼 사용하고 있는 것을 볼 수 있다.

class SortGenerator:
    # The size of the unsorted area in the "random unsorted area"
    # benchmarks
    AREA_SIZE = 100
    # The size of the "partially ordered" sub-arrays
    BUBBLE_SIZE = 100

    @staticmethod
    @memoize
    def random(size, dtype):
        """
        Returns a randomly-shuffled array.
        """
        arr = np.arange(size, dtype=dtype)
        np.random.shuffle(arr)
        return arr
    
    @staticmethod
    @memoize
    def ordered(size, dtype):
        """
        Returns an ordered array.
        """
        return np.arange(size, dtype=dtype)

    @staticmethod
    @memoize
    def reversed(size, dtype):
        """
        Returns an array that's in descending order.
        """
        return np.arange(size-1, -1, -1, dtype=dtype)

    @staticmethod
    @memoize
    def uniform(size, dtype):
        """
        Returns an array that has the same value everywhere.
        """
        return np.ones(size, dtype=dtype)

    @staticmethod
    @memoize
    def swapped_pair(size, dtype, swap_frac):
        """
        Returns an ordered array, but one that has ``swap_frac * size``
        pairs swapped.
        """
        a = np.arange(size, dtype=dtype)
        for _ in range(int(size * swap_frac)):
            x, y = np.random.randint(0, size, 2)
            a[x], a[y] = a[y], a[x]
        return a

    @staticmethod
    @memoize
    def sorted_block(size, dtype, block_size):
        """
        Returns an array with blocks that are all sorted.
        """
        a = np.arange(size, dtype=dtype)
        b = []
        if size < block_size:
            return a
        block_num = size // block_size
        for i in range(block_num):
            b.extend(a[i::block_num])
        return np.array(b)

    @classmethod
    @memoize
    def random_unsorted_area(cls, size, dtype, frac, area_size=None):
        """
        This type of array has random unsorted areas such that they
        compose the fraction ``frac`` of the original array.
        """
        if area_size is None:
            area_size = cls.AREA_SIZE

        area_num = int(size * frac / area_size)
        a = np.arange(size, dtype=dtype)
        for _ in range(area_num):
            start = np.random.randint(size-area_size)
            end = start + area_size
            np.random.shuffle(a[start:end])
        return a

    @classmethod
    @memoize
    def random_bubble(cls, size, dtype, bubble_num, bubble_size=None):
        """
        This type of array has ``bubble_num`` random unsorted areas.
        """
        if bubble_size is None:
            bubble_size = cls.BUBBLE_SIZE
        frac = bubble_size * bubble_num / size

        return cls.random_unsorted_area(size, dtype, frac, bubble_size)

 

언제 주로 사용하는가

굳이 객체를 만들 필요가 없는 유틸 성격의 메소드를 클래스로 묶어 놓고 싶을 때 사용할 수 있다. 
 

파이썬이 다른 언어에 비해서 staticmethod를 적게 사용하는 이유는?

유명 팩키지를 받아서 staticmethod로 찾아보면 생각보다 사용량이 적다. 그 이유는 파이썬 언어가 함수형 언어이기 때문이다. 객체 지향 언어인 자바의 경우 class를 반드시 만들어야 한다. 따라서 공통 기능을 모아둔 유틸 성격의 클래스의 경우 대부분 static 선언이 되어 있다. 파이썬은 아예 파일 자체로 구분할 수 있다. 예를 들면, re란 python 파일을 만들고 그 안에 정규표현식 관련 기능들을 모두 함수로 선언하는 것이다. 그렇게 하면 다른 언어의 유틸 성격의 클래스와 같은 효과를 거둘 수 있다. import re로 re 밑에 있는 함수들을 모두 불러서 쓸 수 있기 때문이다. 
 
re.pyi의 예

@overload
def compile(pattern: AnyStr, flags: _FlagsType = ...) -> Pattern[AnyStr]: ...
@overload
def compile(pattern: Pattern[AnyStr], flags: _FlagsType = ...) -> Pattern[AnyStr]: ...
@overload
def search(pattern: str | Pattern[str], string: str, flags: _FlagsType = ...) -> Match[str] | None: ...
@overload
def search(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = ...) -> Match[bytes] | None: ...
@overload
def match(pattern: str | Pattern[str], string: str, flags: _FlagsType = ...) -> Match[str] | None: ...

 
re 사용 예

import re
pattern = "\d"
text = "123"
print(re.match(pattern, text))

 

참고할 글

python documentation - @staticmethod
python documentation - @classmethod
Java Class methods