Fundamentals 19 min read

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.

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

partial

works 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)))  # 97424613

Cache

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)))  # 55

Function 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())
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.

cachingdecoratorsHigher-Order Functionsfunctoolspartial
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.