ZetCode

Python __call__ 方法

最后修改日期:2025年3月25日

本综合指南探讨了 Python 的 __call__ 方法,该方法使实例能够像函数一样被调用。我们将通过详细的实际示例来研究它的目的、用例和高级模式。

理解 __call__ 基础

__call__ 方法允许将类实例像函数一样调用。通过在类中定义 __call__,其实例将成为可调用对象。

basic_call.py
class Adder:
    """A class that adds a fixed value to an input."""
    def __init__(self, n):
        self.n = n
        
    def __call__(self, x):
        return self.n + x

add5 = Adder(5)
print(add5(10))  # Output: 15

在这个基本示例中,我们创建了一个 Adder 类,它在初始化期间存储一个数字 n__call__ 方法接受一个参数 x 并返回 nx 的总和。然后我们用 5 实例化 add5,并用 10 调用它,调用 __call__ 产生 15。

__call__ 方法使实例的行为类似于函数。它可以接受任意数量的参数,类似于常规函数,并通过实例属性维护状态。每当使用括号调用实例时,它都会被触发。

有状态的函数对象

__call__ 方法允许创建在调用之间保留状态的类似函数的对象。此示例演示了一个计数器,用于跟踪它被调用的次数。

stateful_call.py
class CallCounter:
    """Tracks the number of times it’s called."""
    def __init__(self):
        self.count = 0
        
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count}")
        return f"Result from call {self.count}"

counter = CallCounter()
print(counter())  # Call 1
print(counter())  # Call 2
print(counter())  # Call 3

此示例展示了几个关键概念。 CallCounter 通过其 count 属性维护状态,每次调用时都会递增它。 __call__ 方法使用 *args**kwargs 接受可变参数,模仿常规函数行为,并且实例在调用之间保留其状态。

这种方法具有实际应用。它可用于跟踪和记录函数调用、实现记忆或缓存装饰器、创建有状态的函数包装器以及设计保留先前调用记忆的回调。

使用 __call__ 创建装饰器

__call__ 方法对于构建基于类的装饰器至关重要。此示例实现了一个计时器装饰器,用于测量和记录函数的执行时间。

decorator_call.py
import time

class Timer:
    """Measures execution time of a decorated function."""
    def __init__(self, func):
        self.func = func
        
    def __call__(self, *args, **kwargs):
        start = time.time()
        result = self.func(*args, **kwargs)
        end = time.time()
        print(f"{self.func.__name__} took {end - start:.4f}s")
        return result

@Timer
def calculate(n):
    return sum(i * i for i in range(n))

print(calculate(1000000))

此装饰器实现在初始化期间捕获函数 func__call__ 方法包装该函数,测量执行前后的时间,然后打印持续时间并返回结果。在这里,calculate 有效地计算平方和。

基于类的装饰器具有多个优点。与函数装饰器不同,它们可以在调用之间维护状态,并提供了一种更简单的方法来实现复杂的装饰逻辑。可以通过 __init__ 进行配置,并且装饰逻辑与包装函数保持清晰的分离。

实现函数记忆

通过将 __call__ 与状态相结合,我们可以实现记忆来缓存函数结果。此示例优化了递归斐波那契函数。

memoization.py
class Memoize:
    """Caches function results for faster execution."""
    def __init__(self, func):
        self.func = func
        self.cache = {}
        
    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

@Memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # Much faster with memoization

此记忆实现在实例中存储原始函数和缓存字典。 __call__ 方法使用参数作为键检查缓存中是否已存在结果,仅在必要时计算和存储新结果,然后返回缓存的值。

这种方法非常有效,因为每次递归调用都受益于记忆的结果。缓存持续存在于对同一实例的调用中,使用元组参数作为可哈希键。这通过避免冗余计算,显着提高了斐波那契等递归函数的性能。

创建仿函数(函数对象)

__call__ 方法有助于创建仿函数 - 行为类似于函数但保留状态的对象。 此示例模拟多项式求值器。

functor.py
class Polynomial:
    """Evaluates a polynomial for a given x."""
    def __init__(self, *coefficients):
        self.coeffs = coefficients
        
    def __call__(self, x):
        return sum(coeff * (x ** i)
                   for i, coeff in enumerate(self.coeffs))

quadratic = Polynomial(1, 2, 1)  # 1x² + 2x + 1
print(quadratic(2))  # 1*(2²) + 2*2 + 1 = 9

此多项式仿函数使用表示多项式项的可变数量的系数进行初始化。当使用 x 值调用时,__call__ 计算多项式的结果,在调用之间维护系数,并且行为类似于数学函数。

仿函数具有多个优点。它们将数据和行为组合在一个对象中,比普通函数提供更大的灵活性。它们可以作为回调传递,并且能够实现复杂的类似函数的行为,使其成为 Python 编程中的通用工具。

实现命令模式

使用 __call__,我们可以实现命令设计模式来创建可执行的命令对象。此示例控制灯的状态。

command_pattern.py
class Command:
    """Wraps a receiver method as a callable command."""
    def __init__(self, receiver):
        self.receiver = receiver
        
    def __call__(self, *args):
        return self.receiver(*args)

class Light:
    """Simulates a light with on/off states."""
    def on(self):
        print("Light is ON")
    def off(self):
        print("Light is OFF")

light = Light()
turn_on = Command(light.on)
turn_off = Command(light.off)

turn_on()   # Light is ON
turn_off()  # Light is OFF

此命令模式实现创建一个通用的 Command 类,该类在初始化期间采用接收器方法。 __call__ 方法在调用时执行接收器,从而可以创建具体的命令(如打开或关闭灯),然后像函数一样调用这些命令。

这种方法具有显着的优势。它将调用者与接收者分离,允许对命令进行排队或记录。它简化了撤消/重做功能的实现,并将命令视为一等公民,从而提高了设计的灵活性。

动态 API 端点

__call__ 方法可以创建处理各种请求的动态 API 端点。此示例模拟了一个简单的用户 API,其中包含方法处理程序。

api_endpoint.py
class APIEndpoint:
    """Manages dynamic API endpoints with handlers."""
    def __init__(self, name):
        self.name = name
        self.handlers = {}
        
    def add_handler(self, method, handler):
        self.handlers[method] = handler
        
    def __call__(self, method, *args):
        if method not in self.handlers:
            raise ValueError(f"Unsupported method: {method}")
        return self.handlers[method](*args)

user_endpoint = APIEndpoint("users")
user_endpoint.add_handler("GET", lambda: "User list retrieved")
user_endpoint.add_handler("POST", lambda name: f"User {name} created")

print(user_endpoint("GET"))          # User list retrieved
print(user_endpoint("POST", "John")) # User John created

此 API 端点实现创建支持多种 HTTP 方法的命名端点。它使用 __call__ 通过从字典中选择并执行适当的处理程序来处理请求,从而为动态响应提供干净的接口。

这种技术具有实际应用。它对于 Web 框架路由处理、创建动态 RPC 接口、实现插件系统以及构建协议适配器非常有用,从而提供了一种灵活的方式来管理请求处理。

最佳实践和陷阱

使用 __call__ 时,请考虑以下准则

结论

__call__ 方法是一个强大的 Python 功能,使实例能够像函数一样运行。它允许使用 obj() 语法调用对象,支持创建有状态的函数对象,并且对于基于类的装饰器至关重要。此外,它有助于实现各种设计模式,并且可用于制作灵活的动态接口。

当您需要跨调用维护状态的对象,或者当类似函数的语法增强了 API 的直观性时,请使用 __call__

来源

作者

我叫 Jan Bodnar,我是一位充满热情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。我在编程教学方面拥有超过十年的经验。

列出所有 Python 教程