Fundamentals 19 min read

Master Python’s functools: Decorators, partial, caching, and more

This article explores Python’s functools module, demonstrating how to create and use decorators, partial functions, update_wrapper, total_ordering, cmp_to_key, lru_cache, reduce, and singledispatch, with clear code examples that illustrate each utility and its practical applications.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Master Python’s functools: Decorators, partial, caching, and more

The functools module provides tools for adjusting or extending functions and other callables without rewriting them.

Decorators

The partial class is the main tool offered by functools; it can "wrap" a callable with default arguments. The resulting object is itself callable and behaves like the original function, allowing extra positional or keyword arguments. Using partial instead of a lambda lets you set default parameters while preserving unspecified arguments.

Partial objects

The example below creates two partial objects for myfunc and uses show_details to display their func, args, and keywords attributes:

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)
    return

myfunc('a', 3)

# 给'b'重新设置一个不同的默认参数,调用时仍需提供参数'a'
p1 = functools.partial(myfunc, b=4)
show_details('partial 修改关键字参数', p1, True)
p1('传入 a')
p1('重写 b', b=5)

# 给 'a' 和 'b' 都设置默认参数
p2 = functools.partial(myfunc, '默认 a', b=99)
show_details('partial 设置默认参数', p2, True)
p2()
p2(b='重写 b')

print('参数缺失时:')
# This will raise TypeError because 'a' is missing
# p1()

The final call to the first partial object without providing a value for a raises a TypeError.

Getting function attributes

By default a partial object lacks __name__ and __doc__, which makes debugging harder. functools.update_wrapper() can 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

partial

works with any callable, not only standalone functions. The following example uses a class instance with a __call__ method to create a partial object.

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 specific object. The example shows both being added to a class.

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:
    """functools 示例类"""
    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 attribute information is useful. functools.wraps() (which internally calls update_wrapper()) copies selected attributes from the original function 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("不使用functools.wraps时:")
print("__name__: " + f1.__name__)
print("__doc__: ", end='')
print(f1.__doc__)

print("使用functools.wraps时:")
print("__name__: " + f2.__name__)
print("__doc__: ", end='')
print(f2.__doc__)

Comparison

Before Python 3, classes could define __cmp__ for rich comparisons. Python 3 uses rich comparison methods ( __lt__, __le__, __eq__, __ne__, __gt__, __ge__). The functools.total_ordering decorator lets a class define only __eq__ and one other comparison method; the rest are generated automatically.

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)

print('
Comparisons:')
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

In Python 3 the built‑in sorted(), min(), and max() no longer accept a cmp argument. functools.cmp_to_key() converts a classic comparator into a key function.

# Python 2 style (for illustration only)
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(cmp=my_cmp)
print(''.join(map(str, L)))  # 97424613

# Python 3 style using cmp_to_key
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)))  # 97424613

Cache

The functools.lru_cache() decorator implements a least‑recently‑used cache. It stores results keyed by the function arguments, provides cache_info() (hits, misses, maxsize, currsize) and cache_clear() to reset the cache.

import functools

@functools.lru_cache()
def demo(a):
    print(f'called demo with {a}')
    return a ^ 2

MAX = 2
print('初次调用:')
for i in range(MAX):
    demo(i)
print(demo.cache_info())

print('
第二次调用:')
for i in range(MAX + 1):
    demo(i)
print(demo.cache_info())

print('
清空缓存后:')
demo.cache_clear()
print(demo.cache_info())

print('
再次调用:')
for i in range(MAX):
    demo(i)
print(demo.cache_info())

Reduce method

In Python 3 the global reduce() function was moved to functools. Import it from there to use it.

from functools import reduce
print(reduce(lambda a, b: a + b, range(11)))  # 55 (sum of 0‑10)

Function overloading

The functools.singledispatch() decorator registers a generic function and type‑specific implementations, enabling overload‑like behavior based on the first argument's type.

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())
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Pythoncachingdecoratorsfunctoolspartialsingledispatchtotal_ordering
MaGe Linux Operations
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.