functools — 고차 함수와 호출 가능한 객체에 대한 연산
functools — 고차 함수와 호출 가능한 객체에 대한 연산
functools 모듈은 고차 함수를 위한 것이에요 — 다른 함수에 동작하거나 다른 함수를 돌려주는 함수죠. 일반적으로 이 모듈의 목적상 어떤 호출 가능한 객체든 함수로 취급할 수 있어요.
출처: Python 표준 라이브러리
본문
functools 모듈은 다음 함수들을 정의해요.
@functools.cache(user_function)
간단하고 가벼운 무제한 함수 캐시예요. 때로 "memoize"라고도 불러요.
lru_cache(maxsize=None)와 같은 결과를 돌려주면서, 함수 인자에 대한 딕셔너리 조회의 얇은 래퍼를 만들어요. 옛 값을 내보낼 필요가 없으므로 크기 제한이 있는 @lru_cache보다 더 작고 빠르죠.
예를 들어:
@cache
def factorial(n):
return n * factorial(n-1) if n else 1
>>> factorial(10) # no previously cached result, makes 11 recursive calls
3628800
>>> factorial(5) # no new calls, just returns the cached result
120
>>> factorial(12) # two new recursive calls, factorial(10) is cached
479001600
캐시는 스레드 안전해서 래핑된 함수를 여러 스레드에서 쓸 수 있어요. 즉 기본 데이터 구조가 동시 업데이트 중에도 일관성을 유지한다는 뜻이에요.
다른 스레드가 초기 호출이 완료·캐시되기 전에 추가 호출을 하면 래핑된 함수가 두 번 이상 호출될 수 있어요.
버전 3.9에 추가됨.
@functools.cached_property(func)
클래스의 메서드를, 값이 한 번 계산된 다음 인스턴스 수명 동안 일반 속성으로 캐시되는 프로퍼티로 변환해요. 캐싱이 추가됐다는 점을 제외하면 @property와 비슷해요. 그렇지 않으면 사실상 불변인 인스턴스의 비싼 계산 속성에 유용해요.
예시:
class DataSet:
def __init__(self, sequence_of_numbers):
self._data = tuple(sequence_of_numbers)
@cached_property
def stdev(self):
return statistics.stdev(self._data)
@cached_property의 메커니즘은 @property와 다소 달라요. 일반 프로퍼티는 setter를 정의하지 않으면 속성 쓰기를 차단해요. 반대로 cached_property는 쓰기를 허용해요.
cached_property 데코레이터는 조회 시, 그리고 같은 이름의 속성이 존재하지 않을 때만 실행돼요. 실행되면 cached_property가 같은 이름의 속성에 써요. 이후의 속성 읽기·쓰기는 cached_property 메서드보다 우선하며 일반 속성처럼 동작해요.
캐시된 값은 속성을 삭제해서 지울 수 있어요. 그러면 cached_property 메서드가 다시 실행될 수 있어요.
cached_property는 다중 스레드 사용에서 가능한 경쟁 조건을 막지 않아요. getter 함수가 같은 인스턴스에서 두 번 이상 실행될 수 있고, 가장 최근 실행이 캐시 값을 설정해요. 캐시된 프로퍼티가 멱등이거나 인스턴스에서 두 번 이상 실행돼도 해롭지 않으면 괜찮아요. 동기화가 필요하면 데코레이트된 getter 함수 안이나 캐시된 프로퍼티 접근 주변에서 필요한 잠금을 구현하세요.
이 데코레이터는 PEP 412 키 공유 딕셔너리의 동작을 방해한다는 점에 주의하세요. 즉 인스턴스 딕셔너리가 평소보다 더 많은 공간을 차지할 수 있어요.
또한 이 데코레이터는 각 인스턴스의 __dict__ 속성이 가변 매핑이어야 해요. 즉 메타클래스(type 인스턴스의 __dict__ 속성은 클래스 네임스페이스의 읽기 전용 프록시라서)나, __slots__에 __dict__를 포함하지 않고 지정하는 클래스(그런 클래스는 __dict__ 속성을 전혀 제공하지 않으므로) 같은 일부 타입에서는 작동하지 않아요.
가변 매핑을 쓸 수 없거나 공간 효율적인 키 공유가 필요하면, @lru_cache 위에 @property를 쌓아 @cached_property와 비슷한 효과를 낼 수 있어요. 이것이 @cached_property와 어떻게 다른지 자세한 내용은 메서드 호출을 어떻게 캐시하나요?를 참고하세요.
버전 3.8에 추가됨.
버전 3.12에서 변경: Python 3.12 이전에는 @cached_property가 다중 스레드 사용에서 getter 함수가 인스턴스당 정확히 한 번만 실행되도록 보장하는 문서화되지 않은 잠금을 포함했어요. 그러나 그 잠금은 인스턴스별이 아니라 프로퍼티별이라 용납할 수 없을 정도로 높은 잠금 경합을 초래할 수 있었어요. Python 3.12+에서는 이 잠금이 제거됨.
functools.cmp_to_key(func)
옛날 스타일 비교 함수를 키 함수로 변환해요. 키 함수를 받는 도구(sorted(), min(), max(), heapq.nlargest(), heapq.nsmallest(), itertools.groupby())와 함께 쓰여요. 이 함수는 주로 비교 함수 사용을 지원하던 Python 2에서 전환되는 프로그램용 전환 도구로 쓰여요.
비교 함수는 두 인자를 받아, 작으면 음수, 같으면 0, 크면 양수를 돌려주는 아무 callable이에요. 키 함수는 한 인자를 받아 정렬 키로 쓸 다른 값을 돌려주는 callable이에요.
예시:
sorted(iterable, key=cmp_to_key(locale.strcoll)) # locale-aware sort order
정렬 예시와 간단한 정렬 튜토리얼은 정렬 기법을 참고하세요.
버전 3.2에 추가됨.
@functools.lru_cache(user_function)
@functools.lru_cache(maxsize=128, typed=False)
함수를 최근 maxsize개 호출을 저장하는 memoizing callable로 감싸는 데코레이터예요. 비싸거나 I/O 바운드인 함수를 같은 인자로 주기적으로 호출할 때 시간을 절약할 수 있어요.
캐시는 스레드 안전해서 래핑된 함수를 여러 스레드에서 쓸 수 있어요. 즉 기본 데이터 구조가 동시 업데이트 중에도 일관성을 유지한다는 뜻이에요.
다른 스레드가 초기 호출이 완료·캐시되기 전에 추가 호출을 하면 래핑된 함수가 두 번 이상 호출될 수 있어요.
결과를 캐시하는 데 딕셔너리가 사용되므로, 함수의 위치·키워드 인자는 해시 가능해야 해요.
구별되는 인자 패턴은 별도의 캐시 항목이 있는 구별되는 호출로 간주될 수 있어요. 예를 들어 f(a=1, b=2)와 f(b=2, a=1)은 키워드 인자 순서가 달라 두 개의 별도 캐시 항목을 가질 수 있어요.
user_function이 지정되면 callable이어야 해요. 그러면 lru_cache 데코레이터를 사용자 함수에 직접 적용할 수 있고 maxsize는 기본값 128로 남아요.
@lru_cache
def count_vowels(sentence):
return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')
maxsize를 None으로 설정하면 LRU 기능이 비활성화되고 캐시가 무한정 커질 수 있어요.
typed가 true로 설정되면 다른 타입의 함수 인자가 별도로 캐시돼요. typed가 false면 구현이 보통 그것들을 동등한 호출로 간주하고 단일 결과만 캐시해요. (str와 int 같은 일부 타입은 typed가 false여도 별도로 캐시될 수 있어요.)
타입 특이성은 함수의 즉시 인자에만 적용되지 내용에는 적용되지 않는다는 점에 주의하세요. 스칼라 인자 Decimal(42)와 Fraction(42)는 구별되는 결과를 가진 구별되는 호출로 취급돼요. 반대로 튜플 인자 ('answer', Decimal(42))와 ('answer', Fraction(42))는 동등한 것으로 취급돼요.
래핑된 함수는 maxsize와 typed의 값을 보여 주는 새 dict를 돌려주는 cache_parameters() 함수로 계측돼요. 이것은 정보 제공용일 뿐이에요. 값을 변경해도 효과가 없어요.
캐시의 효과를 측정하고 maxsize 매개변수를 조정하기 위해, 래핑된 함수는 hits, misses, maxsize, currsize를 보여 주는 명명된 튜플을 돌려주는 cache_info() 함수로 계측돼요.
데코레이터는 캐시를 지우거나 무효화하는 cache_clear() 함수도 제공해요.
원래 기본 함수는 __wrapped__ 속성으로 접근할 수 있어요. 이것은 인트로스펙션, 캐시 우회, 또는 함수를 다른 캐시로 다시 감싸는 데 유용해요.
캐시는 인자와 반환 값이 캐시에서 나이를 먹거나 캐시가 지워질 때까지 그것들에 대한 참조를 유지해요.
메서드가 캐시되면 self 인스턴스 인자가 캐시에 포함돼요. 메서드 호출을 어떻게 캐시하나요?를 참고하세요.
LRU(least recently used) 캐시는 최근 호출이 다가올 호출을 가장 잘 예측할 때 가장 잘 작동해요(예: 뉴스 서버의 가장 인기 있는 기사는 매일 바뀌는 경향이 있어요). 캐시의 크기 제한은 웹 서버 같은 장수 프로세스에서 캐시가 무한정 커지지 않게 보장해요.
일반적으로 LRU 캐시는 이전에 계산된 값을 재사용하고 싶을 때만 써야 해요. 따라서 부수 효과가 있는 함수, 매 호출마다 구별되는 가변 객체를 만들어야 하는 함수(제너레이터와 비동기 함수처럼), 또는 time()이나 random() 같은 불순 함수를 캐시하는 것은 말이 안 돼요.
정적 웹 콘텐츠용 LRU 캐시 예시:
@lru_cache(maxsize=32)
def get_pep(num):
'Retrieve text of a Python Enhancement Proposal'
resource = f'https://peps.python.org/pep-{num:04d}'
try:
with urllib.request.urlopen(resource) as s:
return s.read()
except urllib.error.HTTPError:
return 'Not Found'
>>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
... pep = get_pep(n)
... print(n, len(pep))
>>> get_pep.cache_info()
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)
캐시를 사용해 동적 프로그래밍 기법을 구현해 피보나치 수를 효율적으로 계산하는 예시:
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)
버전 3.2에 추가됨.
버전 3.3에서 변경: typed 옵션 추가.
버전 3.8에서 변경: user_function 옵션 추가.
버전 3.9에서 변경: cache_parameters() 함수 추가.
@functools.total_ordering
하나 이상의 리치 비교 정렬 메서드를 정의하는 클래스가 주어지면, 이 클래스 데코레이터가 나머지를 제공해요. 가능한 모든 리치 비교 연산을 지정하는 수고를 단순화해요.
클래스는 __lt__(), __le__(), __gt__(), __ge__() 중 하나를 정의해야 해요. 게다가 클래스는 __eq__() 메서드를 제공해야 해요.
예를 들어:
@total_ordering
class Student:
def _is_valid_operand(self, other):
return (hasattr(other, "lastname") and
hasattr(other, "firstname"))
def __eq__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
if not self._is_valid_operand(other):
return NotImplemented
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))
참고 — 이 데코레이터는 잘 작동하는 완전히 정렬된 타입을 쉽게 만들지만, 파생된 비교 메서드에 대해 더 느린 실행과 더 복잡한 스택 트레이스라는 비용이 들어요. 성능 벤치마킹이 특정 애플리케이션의 병목이라고 나타나면, 여섯 개의 리치 비교 메서드를 모두 구현하는 것이 쉬운 속도 향상을 제공할 가능성이 높아요.
이 데코레이터는 클래스나 그 상위 클래스에 선언된 메서드를 오버라이드하려 하지 않아요. 즉 상위 클래스가 비교 연산자를 정의하면, 원래 메서드가 추상이어도
total_ordering이 그것을 다시 구현하지 않아요.
버전 3.2에 추가됨.
버전 3.4에서 변경: 인식되지 않는 타입에 대해 기본 비교 함수가 NotImplemented를 돌려주는 것이 이제 지원됨.
functools.Placeholder
partial()와 partialmethod()를 호출할 때 위치 인자를 위해 자리를 예약하는 센티널로 사용되는 싱글턴 객체예요.
버전 3.14에 추가됨.
functools.partial(func, /, *args, **keywords)
호출되면 위치 인자 args와 키워드 인자 keywords로 호출된 func처럼 동작하는 새 partial 객체를 돌려줘요. 호출에 더 많은 인자가 공급되면 args에 추가돼요. 추가 키워드 인자가 공급되면 keywords를 확장하고 오버라이드해요. 대략 다음과 같아요.
def partial(func, /, *args, **keywords):
def newfunc(*more_args, **more_keywords):
return func(*args, *more_args, **(keywords | more_keywords))
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
partial() 함수는 함수 인자·키워드의 일부를 "고정"해 단순화된 시그니처를 가진 새 객체를 만드는 부분 함수 적용에 사용돼요. 예를 들어 partial()은 base 인자가 2를 기본으로 하는 int() 함수처럼 동작하는 callable을 만드는 데 쓸 수 있어요.
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
Placeholder 센티널이 args에 있으면 partial()이 호출될 때 먼저 채워져요. 그래서 partial() 호출 하나로 어떤 위치 인자든 미리 채울 수 있어요. Placeholder가 없으면 선택된 개수의 선행 위치 인자만 미리 채울 수 있어요.
Placeholder 센티널이 있으면 호출 시 모두 채워져야 해요.
>>> say_to_world = partial(print, Placeholder, Placeholder, "world!")
>>> say_to_world('Hello', 'dear')
Hello dear world!
say_to_world('Hello')를 호출하면 위치 인자가 하나만 제공되는데 채워야 할 플레이스홀더가 두 개라서 TypeError가 발생해요.
partial()을 기존 partial 객체에 적용하면 입력 객체의 Placeholder 센티널이 새 위치 인자로 채워져요. 플레이스홀더는 이전 Placeholder가 차지한 자리에 새 Placeholder 센티널을 넣어 유지할 수 있어요.
>>> from functools import partial, Placeholder as _
>>> remove = partial(str.replace, _, _, '')
>>> message = 'Hello, dear dear world!'
>>> remove(message, ' dear')
'Hello, world!'
>>> remove_dear = partial(remove, _, ' dear')
>>> remove_dear(message)
'Hello, world!'
>>> remove_first_dear = partial(remove_dear, _, 1)
>>> remove_first_dear(message)
'Hello, dear world!'
Placeholder는 partial()에 키워드 인자로 전달할 수 없어요.
버전 3.14에서 변경: 위치 인자의 Placeholder 지원 추가.
class functools.partialmethod(func, /, *args, **keywords)
partial처럼 동작하지만 직접 호출 가능하기보다는 메서드 정의로 사용되도록 설계된 새 partialmethod 디스크립터를 돌려줘요.
func는 디스크립터거나 callable이어야 해요(일반 함수처럼 둘 다인 객체는 디스크립터로 처리돼요).
func가 디스크립터(일반 Python 함수, classmethod(), staticmethod(), abstractmethod(), 또는 다른 partialmethod 인스턴스 같은)면 __get__ 호출이 기본 디스크립터에 위임되고, 결과로 적절한 partial 객체가 돌아와요.
func가 비-디스크립터 callable이면 적절한 바인딩 메서드가 동적으로 만들어져요. 이것은 메서드로 사용될 때 일반 Python 함수처럼 동작해요: self 인자가 partialmethod 생성자에 제공된 args와 keywords보다 앞서 첫 번째 위치 인자로 삽입돼요.
예시:
>>> class Cell:
... def __init__(self):
... self._alive = False
... @property
... def alive(self):
... return self._alive
... def set_state(self, state):
... self._alive = bool(state)
... set_alive = partialmethod(set_state, True)
... set_dead = partialmethod(set_state, False)
...
>>> c = Cell()
>>> c.alive
False
>>> c.set_alive()
>>> c.alive
True
버전 3.4에 추가됨.
functools.reduce(function, iterable, /[, initial])
두 인자 함수를 iterable의 항목에 왼쪽에서 오른쪽으로 누적 적용해 iterable을 단일 값으로 줄여요. 예를 들어 reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])는 ((((1+2)+3)+4)+5)를 계산해요. 왼쪽 인자 x는 누적 값이고 오른쪽 인자 y는 iterable의 업데이트 값이에요. 선택적 initial이 있으면 계산에서 iterable 항목 앞에 놓이고, iterable이 비어 있을 때 기본값 역할을 해요. initial이 주어지지 않고 iterable이 항목을 하나만 담고 있으면 첫 번째 항목이 돌아와요.
대략 다음과 같아요.
initial_missing = object()
def reduce(function, iterable, /, initial=initial_missing):
it = iter(iterable)
if initial is initial_missing:
value = next(it)
else:
value = initial
for element in it:
value = function(value, element)
return value
모든 중간 값을 생성하는 이터레이터는 itertools.accumulate()를 참고하세요.
버전 3.14에서 변경: initial이 이제 키워드 인자로 지원됨.
@functools.singledispatch
함수를 단일 디스패치(single-dispatch) 제네릭 함수로 변환해요.
제네릭 함수를 정의하려면 @singledispatch 데코레이터로 장식해요. @singledispatch로 함수를 정의할 때, 디스패치가 첫 번째 인자의 타입에서 일어난다는 점에 주의하세요.
>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
... if verbose:
... print("Let me just say,", end=" ")
... print(arg)
함수에 오버로드 구현을 추가하려면 제네릭 함수의 register() 속성을 사용하는데, 데코레이터로 쓸 수 있어요. 타입으로 어노테이트된 함수의 경우 데코레이터가 첫 번째 인자의 타입을 자동으로 추론해요.
>>> @fun.register
... def _(arg: int, verbose=False):
... if verbose:
... print("Strength in numbers, eh?", end=" ")
... print(arg)
...
>>> @fun.register
... def _(arg: list, verbose=False):
... if verbose:
... print("Enumerate this:")
... for i, elem in enumerate(arg):
... print(i, elem)
typing.Union도 쓸 수 있어요.
>>> @fun.register
... def _(arg: int | float, verbose=False):
... if verbose:
... print("Strength in numbers, eh?", end=" ")
... print(arg)
...
>>> from typing import Union
>>> @fun.register
... def _(arg: Union[list, set], verbose=False):
... if verbose:
... print("Enumerate this:")
... for i, elem in enumerate(arg):
... print(i, elem)
타입 어노테이션을 쓰지 않는 코드의 경우 적절한 타입 인자를 데코레이터 자체에 명시적으로 전달할 수 있어요.
>>> @fun.register(complex)
... def _(arg, verbose=False):
... if verbose:
... print("Better than complicated.", end=" ")
... print(arg.real, arg.imag)
컬렉션 타입(예: list)에서 디스패치하면서 컬렉션의 항목(예: list[int])을 타입 힌트하고 싶은 코드의 경우, 디스패치 타입을 데코레이터 자체에 명시적으로 전달하고 타입 힌트는 함수 정의에 넣어야 해요.
>>> @fun.register(list)
... def _(arg: list[int], verbose=False):
... if verbose:
... print("Enumerate this:")
... for i, elem in enumerate(arg):
... print(i, elem)
참고 — 런타임에는 리스트 안에 담긴 타입과 무관하게 리스트 인스턴스에서 함수가 디스패치돼요. 즉
[1,2,3]은["foo", "bar", "baz"]와 똑같이 디스패치돼요. 이 예시에서 제공된 어노테이션은 정적 타입 검사기 전용이고 런타임 영향은 없어요.
람다와 기존 함수를 등록할 수 있도록 register() 속성은 함수형으로도 쓸 수 있어요.
>>> def nothing(arg, verbose=False):
... print("Nothing.")
...
>>> fun.register(type(None), nothing)
register() 속성은 데코레이트되지 않은 함수를 돌려줘요. 이렇게 하면 데코레이터 스태킹, 피클링, 각 변형에 대한 단위 테스트 생성을 독립적으로 할 수 있어요.
>>> @fun.register(float)
... @fun.register(Decimal)
... def fun_num(arg, verbose=False):
... if verbose:
... print("Half of your number:", end=" ")
... print(arg / 2)
...
>>> fun_num is fun
False
호출되면 제네릭 함수는 첫 번째 인자의 타입에서 디스패치해요.
>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615
특정 타입에 대한 등록 구현이 없으면, 그 타입의 메서드 결정 순서(MRO)로 더 일반적인 구현을 찾아요. @singledispatch로 장식된 원래 함수는 기본 object 타입에 등록돼, 더 나은 구현을 찾지 못하면 그것이 사용돼요.
추상 기본 클래스에 구현이 등록되면, 기본 클래스의 가상 하위 클래스가 그 구현으로 디스패치돼요.
>>> from collections.abc import Mapping
>>> @fun.register
... def _(arg: Mapping, verbose=False):
... if verbose:
... print("Keys & Values")
... for key, value in arg.items():
... print(key, "=>", value)
...
>>> fun({"a": "b"})
a => b
제네릭 함수가 주어진 타입에 대해 어떤 구현을 선택할지 확인하려면 dispatch() 속성을 사용하세요.
>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict) # note: default implementation
<function fun at 0x103fe0000>
등록된 모든 구현에 접근하려면 읽기 전용 registry 속성을 사용하세요.
>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
<class 'decimal.Decimal'>, <class 'list'>,
<class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>
버전 3.4에 추가됨.
버전 3.7에서 변경: register() 속성이 이제 타입 어노테이션 사용을 지원함.
버전 3.11에서 변경: register() 속성이 이제 typing.Union을 타입 어노테이션으로 지원함.
class functools.singledispatchmethod(func)
메서드를 단일 디스패치 제네릭 함수로 변환해요.
제네릭 메서드를 정의하려면 @singledispatchmethod 데코레이터로 장식해요. @singledispatchmethod로 메서드를 정의할 때, 디스패치가 첫 번째 비-self 또는 비-cls 인자의 타입에서 일어난다는 점에 주의하세요.
class Negator:
@singledispatchmethod
def neg(self, arg):
raise NotImplementedError("Cannot negate a")
@neg.register
def _(self, arg: int):
return -arg
@neg.register
def _(self, arg: bool):
return not arg
@singledispatchmethod는 @classmethod 같은 다른 데코레이터와의 중첩을 지원해요. dispatcher.register가 가능하려면 singledispatchmethod가 가장 바깥 데코레이터여야 한다는 점에 주의하세요. neg 메서드가 인스턴스가 아니라 클래스에 바인딩된 Negator 클래스는 다음과 같아요.
class Negator:
@singledispatchmethod
@classmethod
def neg(cls, arg):
raise NotImplementedError("Cannot negate a")
@neg.register
@classmethod
def _(cls, arg: int):
return -arg
@neg.register
@classmethod
def _(cls, arg: bool):
return not arg
같은 패턴을 @staticmethod, @abc.abstractmethod 등 다른 유사한 데코레이터에도 쓸 수 있어요.
버전 3.8에 추가됨.
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
래퍼 함수를 래핑된 함수처럼 보이게 업데이트해요. 선택적 인자는 튜플로, 원래 함수의 어떤 속성이 래퍼 함수의 일치하는 속성에 직접 할당되고, 래퍼 함수의 어떤 속성이 원래 함수의 해당 속성으로 업데이트되는지 지정해요. 이 인자들의 기본값은 모듈 레벨 상수 WRAPPER_ASSIGNMENTS(래퍼 함수의 __module__, __name__, __qualname__, __annotations__, __type_params__, __doc__(문서 문자열)에 할당)와 WRAPPER_UPDATES(래퍼 함수의 __dict__, 즉 인스턴스 딕셔너리를 업데이트)예요.
인트로스펙션과 그 외 목적(예: @lru_cache 같은 캐싱 데코레이터 우회)으로 원래 함수에 접근할 수 있게, 이 함수는 래핑되는 함수를 가리키는 __wrapped__ 속성을 래퍼에 자동으로 추가해요.
이 함수의 주요 의도된 용도는 장식된 함수를 감싸고 래퍼를 돌려주는 데코레이터 함수예요. 래퍼 함수가 업데이트되지 않으면 반환된 함수의 메타데이터가 원래 함수 정의가 아니라 래퍼 정의를 반영하는데, 이는 보통 별로 도움이 안 돼요.
update_wrapper()는 함수가 아닌 callable에도 쓸 수 있어요. assigned나 updated에 이름이 있지만 래핑되는 객체에서 빠진 속성은 무시돼요(즉 이 함수가 그것들을 래퍼 함수에 설정하려 하지 않아요). 그래도 래퍼 함수 자체에 updated에 이름이 있는 속성이 없으면 AttributeError가 발생해요.
버전 3.2에서 변경: __wrapped__ 속성이 이제 자동으로 추가됨. __annotations__ 속성이 이제 기본적으로 복사됨. 빠진 속성이 더 이상 AttributeError를 발생시키지 않음.
버전 3.4에서 변경: __wrapped__ 속성이 이제 함수가 __wrapped__ 속성을 정의했어도 항상 래핑된 함수를 가리킴(bpo-17482 참고).
버전 3.12에서 변경: __type_params__ 속성이 이제 기본적으로 복사됨.
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
래퍼 함수를 정의할 때 update_wrapper()를 함수 데코레이터로 호출하기 위한 편의 함수예요. partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)과 같아요. 예를 들어:
>>> from functools import wraps
>>> def my_decorator(f):
... @wraps(f)
... def wrapper(*args, **kwds):
... print('Calling decorated function')
... return f(*args, **kwds)
... return wrapper
...
>>> @my_decorator
... def example():
... """Docstring"""
... print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'
이 데코레이터 팩토리를 쓰지 않으면 example 함수의 이름이 'wrapper'였을 것이고, 원래 example()의 docstring도 잃었을 거예요.
partial 객체
partial 객체는 partial()이 만든 호출 가능 객체예요. 세 개의 읽기 전용 속성이 있어요.
partial.func
호출 가능 객체 또는 함수. partial 객체에 대한 호출은 새 인자·키워드와 함께 func로 전달돼요.
partial.args
partial 객체 호출에 제공되는 위치 인자 앞에 붙을 가장 왼쪽 위치 인자들.
partial.keywords
partial 객체가 호출될 때 공급될 키워드 인자들.
partial 객체는 호출 가능하고, 약하게 참조 가능하며, 속성을 가질 수 있다는 점에서 함수 객체와 비슷해요. 다만 몇 가지 중요한 차이가 있어요. 예를 들어 __name__과 __doc__ 속성은 자동으로 생성되지 않아요.