大数跨境
0
0

functools — 操作函数的工具

functools — 操作函数的工具 我爱数据科学
2025-09-13
0
导读:目的:操作其他函数的函数。functools 模块提供了用于调整或扩展函数和其他可调用对象的工具,而无需完全重写它们。

目的:操作其他函数的函数。

functools 模块提供了用于调整或扩展函数和其他可调用对象的工具,而无需完全重写它们。

装饰器

functools 模块提供的主要工具是 partial 类,它可用于使用默认参数“包装”一个可调用对象。生成的对象本身也是可调用的,并且可以像原始函数一样对待。它接受与原始函数完全相同的参数,也可以使用额外的位置参数或命名参数进行调用。在某些参数未指定的情况下,可以使用 partial 代替 lambda 来为函数提供默认参数。

Partial 对象

此示例展示了两个用于函数 myfunc() 的简单 partial 对象。show_details() 的输出包括 partial 对象的 funcargs 和 keywords 属性。

functools_partial.py

import functools


def myfunc(a, b=2):
    "Docstring for myfunc()."
    print('  called myfunc with:', (a, b))


def show_details(name, f, is_partial=False):
    "Show details of a callable object."
    print('{}:'.format(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


show_details('myfunc', myfunc)
myfunc('a'3)
print()

# 为 'b' 设置不同的默认值,但要求调用者提供 'a'。
p1 = functools.partial(myfunc, b=4)
show_details('partial with named default', p1, True)
p1('passing a')
p1('override b', b=5)
print()

# 为 'a' 和 'b' 都设置默认值。
p2 = functools.partial(myfunc, 'default a', b=99)
show_details('partial with defaults', p2, True)
p2()
p2(b='override b')
print()

print('Insufficient arguments:')
p1()

在示例末尾,创建的第一个 partial 对象在没有传递 a 值的情况下被调用,导致异常。

$ python3 functools_partial.py

myfunc:
  object: <function myfunc at 0x1007a6a60>
  __name__: myfunc
  called myfunc with: ('a', 3)

partial with named default:
  object: functools.partial(<function myfunc at 0x1007a6a60>, b=4)
  func: <function myfunc at 0x1007a6a60>
  args: ()
  keywords: {'b': 4}
  called myfunc with: ('passing a', 4)
  called myfunc with: ('override b', 5)

partial with defaults:
  object: functools.partial(<function myfunc at 0x1007a6a60>, 'default a', b=99)
  func: <function myfunc at 0x1007a6a60>
  args: ('default a',)
  keywords: {'b': 99}
  called myfunc with: ('default a', 99)
  called myfunc with: ('default a''override b')

Insufficient arguments:
Traceback (most recent call last):
  File "functools_partial.py", line 51, in <module>
    p1()
TypeError: myfunc() missing 1 required positional argument: 'a'

获取函数属性

partial 对象默认没有 __name__ 或 __doc__ 属性,没有这些属性,装饰后的函数更难调试。使用 update_wrapper(),可以将原始函数的属性复制或添加到 partial 对象。

functools_update_wrapper.py

import functools


def myfunc(a, b=2):
    "Docstring for myfunc()."
    print('  called myfunc with:', (a, b))


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    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)
print()

functools.update_wrapper(p1, myfunc)
show_details('updated wrapper', p1)

添加到包装器的属性在 WRAPPER_ASSIGNMENTS 中定义,而 WRAPPER_UPDATES 列出了要修改的值。

$ python3 functools_update_wrapper.py

myfunc:
  object: <function myfunc at 0x1018a6a60>
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

raw wrapper:
  object: functools.partial(<function myfunc at 0x1018a6a60>, b=4)
  __name__: (no __name__)
  __doc__ 'partial(func, *args, **keywords) - new function with partial application\n    of the given arguments and keywords.\n'

Updating wrapper:
  assign: ('__module__''__name__''__qualname__''__doc__''__annotations__')
  update: ('__dict__',)

updated wrapper:
  object: functools.partial(<function myfunc at 0x1018a6a60>, b=4)
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

其他可调用对象

Partials 可以与任何可调用对象一起使用,而不仅仅是独立函数。

functools_callable.py

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):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    return


o = MyClass()

show_details('instance', o)
o('e goes here')
print()

p = functools.partial(o, e='default for e', f=8)
functools.update_wrapper(p, o)
show_details('instance wrapper', p)
p()

此示例从一个具有 __call__() 方法的类的实例创建 partials。

$ python3 functools_callable.py

instance:
  object: <__main__.MyClass object at 0x1011b1cf8>
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x1011b1cf8>, 'e goes here', 6)

instance wrapper:
  object: functools.partial(<__main__.MyClass object at 0x1011b1cf8>, f=8, e='default for e')
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x1011b1cf8>, 'default for e', 8)

方法和函数

虽然 partial() 返回一个可直接使用的可调用对象,但 partialmethod() 返回一个可作为对象未绑定方法使用的可调用对象。在以下示例中,同一个独立函数被两次添加为 MyClass 的属性,一次使用 partialmethod() 作为 method1(),另一次使用 partial() 作为 method2()

functools_partialmethod.py

import functools


def standalone(self, a=1, b=2):
    "Standalone function"
    print('  called standalone with:', (self, a, b))
    if self is not None:
        print('  self.attr =', self.attr)


class MyClass:
    "Demonstration class for functools"

    def __init__(self):
        self.attr = 'instance attribute'

    method1 = functools.partialmethod(standalone)
    method2 = functools.partial(standalone)


o = MyClass()

print('standalone')
standalone(None)
print()

print('method1 as partialmethod')
o.method1()
print()

print('method2 as partial')
try:
    o.method2()
except TypeError as err:
    print('ERROR: {}'.format(err))

method1() 可以从 MyClass 的实例调用,并且实例作为第一个参数传递,就像正常定义的方法一样。method2() 并未设置为绑定方法,因此必须显式传递 self 参数,否则调用将导致 TypeError

$ python3 functools_partialmethod.py

standalone
  called standalone with: (None, 1, 2)

method1 as partialmethod
  called standalone with: (<__main__.MyClass object at 0x1007b1d30>, 1, 2)
  self.attr = instance attribute

method2 as partial
ERROR: standalone() missing 1 required positional argument: 'self'

为装饰器获取函数属性

在装饰器中使用时,更新包装的可调用对象的属性特别有用,因为转换后的函数最终会具有原始“裸”函数的属性。

functools_wraps.py

import functools


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    print()


def simple_decorator(f):
    @functools.wraps(f)
    def decorated(a='decorated defaults', b=1):
        print('  decorated:', (a, b))
        print('  ', end=' ')
        return f(a, b=b)
    return decorated


def myfunc(a, b=2):
    "myfunc() is not complicated"
    print('  myfunc:', (a, b))
    return


# The raw function
show_details('myfunc', myfunc)
myfunc('unwrapped, default b')
myfunc('unwrapped, passing b'3)
print()

# Wrap explicitly
wrapped_myfunc = simple_decorator(myfunc)
show_details('wrapped_myfunc', wrapped_myfunc)
wrapped_myfunc()
wrapped_myfunc('args to wrapped'4)
print()


# Wrap with decorator syntax
@simple_decorator
def decorated_myfunc(a, b):
    myfunc(a, b)
    return


show_details('decorated_myfunc', decorated_myfunc)
decorated_myfunc()
decorated_myfunc('args to decorated'4)

functools 提供了一个装饰器 wraps(),它将 update_wrapper() 应用于被装饰的函数。

$ python3 functools_wraps.py

myfunc:
  object: <function myfunc at 0x101241b70>
  __name__: myfunc
  __doc__ 'myfunc() is not complicated'

  myfunc: ('unwrapped, default b', 2)
  myfunc: ('unwrapped, passing b', 3)

wrapped_myfunc:
  object: <function myfunc at 0x1012e62f0>
  __name__: myfunc
  __doc__ 'myfunc() is not complicated'

  decorated: ('decorated defaults', 1)
     myfunc: ('decorated defaults', 1)
  decorated: ('args to wrapped', 4)
     myfunc: ('args to wrapped', 4)

decorated_myfunc:
  object: <function decorated_myfunc at 0x1012e6400>
  __name__: decorated_myfunc
  __doc__ None

  decorated: ('decorated defaults', 1)
     myfunc: ('decorated defaults', 1)
  decorated: ('args to decorated', 4)
     myfunc: ('args to decorated', 4)

比较

在 Python 2 中,类可以定义一个 __cmp__() 方法,该方法根据对象是小于、等于还是大于正在比较的项返回 -1、0 或 1。Python 2.1 引入了丰富的比较方法 API(__lt__()__le__()__eq__()__ne__()__gt__() 和 __ge__()),它执行单个比较操作并返回布尔值。Python 3 已弃用 __cmp__(),转而使用这些新方法,functools 提供了工具,使编写符合 Python 3 新比较要求的类更容易。

丰富比较

丰富的比较 API 旨在允许具有复杂比较的类以最有效的方式实现每个测试。然而,对于比较相对简单的类,手动创建每个丰富的比较方法没有意义。total_ordering() 类装饰器接受一个提供了一些方法的类,并添加其余的方法。

functools_total_ordering.py

import functools
import inspect
from pprint import pprint


@functools.total_ordering
class MyObject:

    def __init__(self, val):
        self.val = val

    def __eq__(self, other):
        print('  testing __eq__({}, {})'.format(
            self.val, other.val))
        return self.val == other.val

    def __gt__(self, other):
        print('  testing __gt__({}, {})'.format(
            self.val, other.val))
        return self.val > other.val


print('Methods:\n')
pprint(inspect.getmembers(MyObject, inspect.isfunction))

a = MyObject(1)
b = MyObject(2)

print('\nComparisons:')
for expr in ['a < b''a <= b''a == b''a >= b''a > b']:
    print('\n{:<6}:'.format(expr))
    result = eval(expr)
    print('  result of {}: {}'.format(expr, result))

该类必须提供 __eq__() 和另一个丰富比较方法的实现。装饰器通过使用提供的比较添加其余方法的实现。如果无法进行比较,该方法应返回 NotImplemented,以便在完全失败之前尝试使用另一个对象上的反向比较运算符进行比较。

$ python3 functools_total_ordering.py

Methods:

[('__eq__', <function MyObject.__eq__ at 0x10139a488>),
 ('__ge__', <function _ge_from_gt at 0x1012e2510>),
 ('__gt__', <function MyObject.__gt__ at 0x10139a510>),
 ('__init__', <function MyObject.__init__ at 0x10139a400>),
 ('__le__', <function _le_from_gt at 0x1012e2598>),
 ('__lt__', <function _lt_from_gt at 0x1012e2488>)]

Comparisons:

a < b :
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a < b: True

a <= b:
  testing __gt__(1, 2)
  result of a <= b: True

a == b:
  testing __eq__(1, 2)
  result of a == b: False

a >= b:
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a >= b: False

a > b :
  testing __gt__(1, 2)
  result of a > b: False

排序顺序

由于旧式的比较函数在 Python 3 中已弃用,像 sort() 这样的函数的 cmp 参数也不再受支持。使用比较函数的旧程序可以使用 cmp_to_key() 将其转换为返回排序键的函数,该函数用于确定在最终序列中的位置。

functools_cmp_to_key.py

import functools


class MyObject:

    def __init__(self, val):
        self.val = val

    def __str__(self):
        return 'MyObject({})'.format(self.val)


def compare_obj(a, b):
    """Old-style comparison function.
    """

    print('comparing {} and {}'.format(a, b))
    if a.val < b.val:
        return -1
    elif a.val > b.val:
        return 1
    return 0


# Make a key function using cmp_to_key()
get_key = functools.cmp_to_key(compare_obj)

def get_key_wrapper(o):
    "Wrapper function for get_key to allow for print statements."
    new_key = get_key(o)
    print('key_wrapper({}) -> {!r}'.format(o, new_key))
    return new_key


objs = [MyObject(x) for x in range(50-1)]

for o in sorted(objs, key=get_key_wrapper):
    print(o)

通常 cmp_to_key() 会直接使用,但在此示例中引入了一个额外的包装函数,以便在调用键函数时打印更多信息。

输出显示 sorted() 首先为序列中的每个项目调用 get_key_wrapper() 以生成键。cmp_to_key() 返回的键是 functools 中定义的一个类的实例,该类使用传入的旧式比较函数实现了丰富的比较 API。创建所有键后,通过比较键对序列进行排序。

$ python3 functools_cmp_to_key.py

key_wrapper(MyObject(5)) -> <functools.KeyWrapper object at 0x1011c5530>
key_wrapper(MyObject(4)) -> <functools.KeyWrapper object at 0x1011c5510>
key_wrapper(MyObject(3)) -> <functools.KeyWrapper object at 0x1011c54f0>
key_wrapper(MyObject(2)) -> <functools.KeyWrapper object at 0x1011c5390>
key_wrapper(MyObject(1)) -> <functools.KeyWrapper object at 0x1011c5710>
comparing MyObject(4) and MyObject(5)
comparing MyObject(3) and MyObject(4)
comparing MyObject(2) and MyObject(3)
comparing MyObject(1) and MyObject(2)
MyObject(1)
MyObject(2)
MyObject(3)
MyObject(4)
MyObject(5)

缓存

lru_cache() 装饰器将函数包装在最近最少使用的缓存中。函数的参数用于构建哈希键,然后将其映射到结果。使用相同参数的后续调用将从缓存中获取值,而不是调用函数。该装饰器还向函数添加了检查缓存状态(cache_info())和清空缓存(cache_clear())的方法。

functools_lru_cache.py

import functools


@functools.lru_cache()
def expensive(a, b):
    print('expensive({}, {})'.format(a, b))
    return a * b


MAX = 2

print('First set of calls:')
for i in range(MAX):
    for j in range(MAX):
        expensive(i, j)
print(expensive.cache_info())

print('\nSecond set of calls:')
for i in range(MAX + 1):
    for j in range(MAX + 1):
        expensive(i, j)
print(expensive.cache_info())

print('\nClearing cache:')
expensive.cache_clear()
print(expensive.cache_info())

print('\nThird set of calls:')
for i in range(MAX):
    for j in range(MAX):
        expensive(i, j)
print(expensive.cache_info())

此示例在一个嵌套循环中多次调用 expensive()。第二次使用相同值进行调用时,结果会出现在缓存中。当缓存被清除并且循环再次运行时,必须重新计算值。

$ python3 functools_lru_cache.py

First set of calls:
expensive(0, 0)
expensive(0, 1)
expensive(1, 0)
expensive(1, 1)
CacheInfo(hits=0, misses=4, maxsize=128, currsize=4)

Second set of calls:
expensive(0, 2)
expensive(1, 2)
expensive(2, 0)
expensive(2, 1)
expensive(2, 2)
CacheInfo(hits=4, misses=9, maxsize=128, currsize=9)

Clearing cache:
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

Third set of calls:
expensive(0, 0)
expensive(0, 1)
expensive(1, 0)
expensive(1, 1)
CacheInfo(hits=0, misses=4, maxsize=128, currsize=4)

为了防止在长时间运行的进程中缓存无限制地增长,缓存被赋予了最大大小。默认值为 128 个条目,但可以使用 maxsize 参数为每个缓存更改此值。

functools_lru_cache_expire.py

import functools


@functools.lru_cache(maxsize=2)
def expensive(a, b):
    print('called expensive({}, {})'.format(a, b))
    return a * b


def make_call(a, b):
    print('({}, {})'.format(a, b), end=' ')
    pre_hits = expensive.cache_info().hits
    expensive(a, b)
    post_hits = expensive.cache_info().hits
    if post_hits > pre_hits:
        print('cache hit')


print('Establish the cache')
make_call(12)
make_call(23)

print('\nUse cached items')
make_call(12)
make_call(23)

print('\nCompute a new value, triggering cache expiration')
make_call(34)

print('\nCache still contains one old item')
make_call(23)

print('\nOldest item needs to be recomputed')
make_call(12)

在此示例中,缓存大小设置为 2 个条目。当使用第三组唯一参数 (3, 4) 时,缓存中最旧的项目会被丢弃并替换为新结果。

$ python3 functools_lru_cache_expire.py

Establish the cache
(1, 2) called expensive(1, 2)
(2, 3) called expensive(2, 3)

Use cached items
(1, 2) cache hit
(2, 3) cache hit

Compute a new value, triggering cache expiration
(3, 4) called expensive(3, 4)

Cache still contains one old item
(2, 3) cache hit

Oldest item needs to be recomputed
(1, 2) called expensive(1, 2)

由 lru_cache() 管理的缓存的键必须是可哈希的,因此所有传递给包装了缓存查找的函数的参数都必须是可哈希的。

functools_lru_cache_arguments.py

import functools


@functools.lru_cache(maxsize=2)
def expensive(a, b):
    print('called expensive({}, {})'.format(a, b))
    return a * b


def make_call(a, b):
    print('({}, {})'.format(a, b), end=' ')
    pre_hits = expensive.cache_info().hits
    expensive(a, b)
    post_hits = expensive.cache_info().hits
    if post_hits > pre_hits:
        print('cache hit')


make_call(12)

try:
    make_call([1], 2)
except TypeError as err:
    print('ERROR: {}'.format(err))

try:
    make_call(1, {'2''two'})
except TypeError as err:
    print('ERROR: {}'.format(err))

如果传递给函数的任何对象不可哈希,则会引发 TypeError

$ python3 functools_lru_cache_arguments.py

(1, 2) called expensive(1, 2)
([1], 2) ERROR: unhashable type'list'
(1, {'2''two'}) ERROR: unhashable type'dict'

缩减数据集

reduce() 函数接受一个可调用对象和一个数据序列作为输入,并根据使用序列中的值调用可调用对象并累积结果输出来产生单个值。

functools_reduce.py

import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


data = range(15)
print(data)
result = functools.reduce(do_reduce, data)
print('result: {}'.format(result))

此示例将输入序列中的数字相加。

$ python3 functools_reduce.py

range(1, 5)
do_reduce(1, 2)
do_reduce(3, 3)
do_reduce(6, 4)
result: 10

可选的 initializer 参数被放置在序列的前面,并与其他项目一起处理。这可用于使用新输入更新先前计算的值。

functools_reduce_initializer.py

import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


data = range(15)
print(data)
result = functools.reduce(do_reduce, data, 99)
print('result: {}'.format(result))

在此示例中,先前的总和 99 用于初始化 reduce() 计算的值。

$ python3 functools_reduce_initializer.py

range(1, 5)
do_reduce(99, 1)
do_reduce(100, 2)
do_reduce(102, 3)
do_reduce(105, 4)
result: 109

当没有 initializer 存在时,具有单个项目的序列会自动缩减为该值。空列表会生成错误,除非提供了 initializer

functools_reduce_short_sequences.py

import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


print('Single item in sequence:',
      functools.reduce(do_reduce, [1]))

print('Single item in sequence with initializer:',
      functools.reduce(do_reduce, [1], 99))

print('Empty sequence with initializer:',
      functools.reduce(do_reduce, [], 99))

try:
    print('Empty sequence:', functools.reduce(do_reduce, []))
except TypeError as err:
    print('ERROR: {}'.format(err))

由于 initializer 参数既作为默认值,又在输入序列不为空时与新值组合,因此在使用时需要仔细考虑是否使用它。当将默认值与新值组合没有意义时,最好捕获 TypeError 而不是传递 initializer

$ python3 functools_reduce_short_sequences.py

Single item in sequence: 1
do_reduce(99, 1)
Single item in sequence with initializer: 100
Empty sequence with initializer: 99
ERROR: reduce() of empty sequence with no initial value

通用函数

在像 Python 这样的动态类型语言中,通常需要根据参数的类型执行略有不同的操作,尤其是在处理项目列表和单个项目之间的差异时。直接检查参数的类型很简单,但在行为差异可以隔离到单独函数中的情况下,functools 提供了 singledispatch() 装饰器来注册一组通用函数,以便根据函数第一个参数的类型自动切换。

functools_singledispatch.py

import functools


@functools.singledispatch
def myfunc(arg):
    print('default myfunc({!r})'.format(arg))


@myfunc.register(int)
def myfunc_int(arg):
    print('myfunc_int({})'.format(arg))


@myfunc.register(list)
def myfunc_list(arg):
    print('myfunc_list()')
    for item in arg:
        print('  {}'.format(item))


myfunc('string argument')
myfunc(1)
myfunc(2.3)
myfunc(['a''b''c'])

新函数的 register() 属性作为另一个装饰器,用于注册替代实现。使用 singledispatch() 包装的第一个函数是默认实现,如果没有找到其他特定类型的函数,如本例中的浮点数情况。

$ python3 functools_singledispatch.py

default myfunc('string argument')
myfunc_int(1)
default myfunc(2.3)
myfunc_list()
  a
  b
  c

当找不到确切的类型匹配时,会评估继承顺序并使用最接近的匹配类型。

functools_singledispatch_mro.py

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('default myfunc({})'.format(arg.__class__.__name__))


@myfunc.register(A)
def myfunc_A(arg):
    print('myfunc_A({})'.format(arg.__class__.__name__))


@myfunc.register(B)
def myfunc_B(arg):
    print('myfunc_B({})'.format(arg.__class__.__name__))


@myfunc.register(C)
def myfunc_C(arg):
    print('myfunc_C({})'.format(arg.__class__.__name__))


myfunc(A())
myfunc(B())
myfunc(C())
myfunc(D())
myfunc(E())

在此示例中,类 D 和 E 与任何已注册的通用函数都不完全匹配,所选函数取决于类层次结构。

$ python3 functools_singledispatch_mro.py

myfunc_A(A)
myfunc_B(B)
myfunc_C(C)
myfunc_B(D)
myfunc_C(E)

【声明】内容源于网络
0
0
我爱数据科学
精通R语言及Python,传递数据挖掘及可视化技术,关注机器学习及深度学习算法及实现,分享大模型及LangChain的使用技巧。编著多本R语言、python、深度学习等书籍。
内容 322
粉丝 0
我爱数据科学 精通R语言及Python,传递数据挖掘及可视化技术,关注机器学习及深度学习算法及实现,分享大模型及LangChain的使用技巧。编著多本R语言、python、深度学习等书籍。
总阅读113
粉丝0
内容322