Master Python’s functools: Decorators, Partial, Caching, and More
This article introduces Python’s functools module, explaining how decorators, partial objects, update_wrapper, total_ordering, cmp_to_key, lru_cache, reduce, and singledispatch work, and provides clear code examples that demonstrate their practical use in everyday programming.
The functools module provides tools for adjusting or extending functions and other callables without completely rewriting them.
Decorators
The partial class is the primary tool offered by functools; it can wrap a callable with default arguments, producing an object that remains callable and behaves like the original function with some arguments pre‑filled. Using partial instead of a lambda preserves unspecified arguments.
Partial objects
Below are two partial objects created for a function myfunc. The helper show_details() prints the func, args, and keywords attributes of a partial object.
import functools
def myfunc(a, b=2):
"""Docstring for myfunc()."""
print(' 传入参数:', (a, b))
def show_details(name, f, is_partial=False):
"""Show details of a callable object."""
print(f"{name}:")
print(' object:', f)
if not is_partial:
print(' __name__:', f.__name__)
if is_partial:
print(' func:', f.func)
print(' args:', f.args)
print(' keywords:', f.keywords)
# original call
show_details('myfunc', myfunc)
myfunc('a', 3)
# partial with a new default for b
p1 = functools.partial(myfunc, b=4)
show_details('partial 修改关键字参数', p1, True)
p1('传入 a')
p1('重写 b', b=5)
# partial with defaults for both a and b
p2 = functools.partial(myfunc, '默认 a', b=99)
show_details('partial 设置默认参数', p2, True)
p2()
p2(b='重写 b')
# calling without required argument raises an error
p1()Getting function attributes
By default a partial object lacks __name__ and __doc__. To preserve these for debugging, use functools.update_wrapper() to copy or add attributes from the original function to the partial object.
import functools
def myfunc(a, b=2):
"""Docstring for myfunc()."""
print(' 传入参数:', (a, b))
def show_details(name, f):
print(f"{name}:")
print(' object:', f)
print(' __name__:', getattr(f, '__name__', '(no __name__)'))
print(' __doc__', repr(getattr(f, '__doc__', None)))
print()
show_details('myfunc', myfunc)
p1 = functools.partial(myfunc, b=4)
show_details('raw wrapper', p1)
print('Updating wrapper:')
print(' assign:', functools.WRAPPER_ASSIGNMENTS)
print(' update:', functools.WRAPPER_UPDATES)
functools.update_wrapper(p1, myfunc)
show_details('updated wrapper', p1)Other callable objects
partialworks with any callable, not only plain functions. The example below creates a partial from an instance of a class that implements __call__.
import functools
class MyClass:
"""Demonstration class for functools"""
def __call__(self, e, f=6):
"""Docstring for MyClass.__call__"""
print(' called object with:', (self, e, f))
def show_details(name, f):
print(f"{name}:")
print(' object:', f)
print(' __name__:', getattr(f, '__name__', '(no __name__)'))
print(' __doc__', repr(getattr(f, '__doc__', None)))
print()
o = MyClass()
show_details('instance', o)
o('e goes here')
p = functools.partial(o, e='default for e', f=8)
functools.update_wrapper(p, o)
show_details('instance wrapper', p)
p()Methods and functions
partial()returns a directly callable object, while partialmethod() returns an unbound method prepared for a class. The following demonstrates adding a standalone function to a class both as a partialmethod (which receives the instance automatically) and as a partial (which requires an explicit self argument).
import functools
def standalone(self, a=1, b=2):
"""独立函数"""
print(' called standalone with:', (self, a, b))
if self is not None:
print(' self.attr =', self.attr)
class MyClass:
def __init__(self):
self.attr = 'instance attribute'
method1 = functools.partialmethod(standalone)
method2 = functools.partial(standalone)
o = MyClass()
print('standalone')
standalone(None)
print('
method1 as partialmethod')
o.method1()
print('
method2 as partial')
try:
o.method2()
except TypeError as err:
print('ERROR:', err)Using in decorators
When decorating functions, preserving the original function’s metadata is useful. The functools.wraps() decorator internally calls update_wrapper() to copy attributes such as __name__ and __doc__ to the wrapper.
from functools import wraps
def logged1(func):
def with_login(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_login
@logged1
def f1(x):
"""function doc"""
return x + x * 1
def logged2(func):
@wraps(func)
def with_login(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_login
@logged2
def f2(x):
"""function doc"""
return x + x * 1
print('Without functools.wraps:')
print('__name__:', f1.__name__)
print('__doc__:', f1.__doc__)
print('
With functools.wraps:')
print('__name__:', f2.__name__)
print('__doc__:', f2.__doc__)Comparison
Before Python 3, classes could define __cmp__. Python 3 dropped it in favor of rich comparison methods ( __lt__, __le__, __eq__, __ne__, __gt__, __ge__). The functools.total_ordering decorator lets you implement only a subset (e.g., __eq__ and one ordering method) and automatically supplies the rest.
import functools, inspect
from pprint import pprint
@functools.total_ordering
class MyObject:
def __init__(self, val):
self.val = val
def __eq__(self, other):
print(f' testing __eq__({self.val}, {other.val})')
return self.val == other.val
def __gt__(self, other):
print(f' testing __gt__({self.val}, {other.val})')
return self.val > other.val
print("MyObject's Methods:")
pprint(inspect.getmembers(MyObject, inspect.isfunction))
a = MyObject(1)
b = MyObject(2)
for expr in ['a < b', 'a <= b', 'a == b', 'a >= b', 'a > b']:
print(f'
{expr:<6}:')
result = eval(expr)
print(f' result of {expr}: {result}')Sorting rules
Python 3 removed the cmp argument from sorted(), min(), and max(). The functools.cmp_to_key() helper converts an old‑style comparison function into a key function usable with the modern sorting APIs.
from functools import cmp_to_key
L = [97, 13, 4, 246]
def my_cmp(a, b):
"""Compare two numbers by concatenating them as strings."""
return int(str(b) + str(a)) - int(str(a) + str(b))
L.sort(key=cmp_to_key(my_cmp))
print(''.join(map(str, L))) # 97424613Cache
The functools.lru_cache() decorator implements a least‑recently‑used cache. It stores results keyed by the function arguments, provides cache_info() to inspect hits/misses, and cache_clear() to reset the cache. The optional maxsize limits memory usage, and typed distinguishes arguments of different types.
import functools
@functools.lru_cache()
def demo(a):
print(f'called demo with {a}')
return a ^ 2
MAX = 2
print('First run:')
for i in range(MAX):
demo(i)
print(demo.cache_info())
print('
Second run:')
for i in range(MAX + 1):
demo(i)
print(demo.cache_info())
demo.cache_clear()
print('
After clearing:')
print(demo.cache_info())
print('
Run again:')
for i in range(MAX):
demo(i)
print(demo.cache_info())Reduce method
In Python 3 the built‑in reduce() was moved to functools. Import it from there to perform cumulative reductions.
from functools import reduce
print(reduce(lambda a, b: a + b, range(11))) # 55Function overloading
The functools.singledispatch() decorator enables generic functions that dispatch to type‑specific implementations based on the first argument’s type, mimicking function overloading in statically typed languages.
import functools
@functools.singledispatch
def myfunc(arg):
print(f'default myfunc({arg!r})')
@myfunc.register(int)
def myfunc_int(arg):
print(f'myfunc_int({arg})')
@myfunc.register(list)
def myfunc_list(arg):
print('myfunc_list(' + ' '.join(arg) + ')')
myfunc('string argument')
myfunc(1)
myfunc(2.3)
myfunc(['a', 'b', 'c'])When a type has no exact registration, the dispatcher follows the inheritance hierarchy to choose the nearest registered implementation.
import functools
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(C, D): pass
@functools.singledispatch
def myfunc(arg):
print(f'default myfunc({arg.__class__.__name__})')
@myfunc.register(A)
def myfunc_A(arg):
print(f'myfunc_A({arg.__class__.__name__})')
@myfunc.register(B)
def myfunc_B(arg):
print(f'myfunc_B({arg.__class__.__name__})')
@myfunc.register(C)
def myfunc_C(arg):
print(f'myfunc_C({arg.__class__.__name__})')
myfunc(A())
myfunc(B())
myfunc(C())
myfunc(D())
myfunc(E())Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
