定义一个合适的可调用对象

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习 Python 中的可调用对象。可调用对象可以像函数一样使用 object() 语法进行调用。虽然 Python 函数本质上是可调用的,但你可以通过实现 __call__ 方法来创建自定义的可调用对象。

你还将学习如何使用 __call__ 方法实现一个可调用对象,并将函数注解与可调用对象结合使用以进行参数验证。在这个实验过程中,你将修改 validate.py 文件。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/BasicConceptsGroup -.-> python/variables_data_types("Variables and Data Types") python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") subgraph Lab Skills python/variables_data_types -.-> lab-132513{{"定义一个合适的可调用对象"}} python/type_conversion -.-> lab-132513{{"定义一个合适的可调用对象"}} python/function_definition -.-> lab-132513{{"定义一个合适的可调用对象"}} python/classes_objects -.-> lab-132513{{"定义一个合适的可调用对象"}} python/constructor -.-> lab-132513{{"定义一个合适的可调用对象"}} end

理解验证器类

在这个实验中,我们将基于一组验证器类来创建一个可调用对象。在开始构建之前,理解 validate.py 文件中提供的验证器类非常重要。这些类将帮助我们进行类型检查,而类型检查是确保代码按预期工作的关键部分。

让我们先在 WebIDE 中打开 validate.py 文件。这个文件包含了我们将使用的验证器类的代码。要打开它,请在终端中运行以下命令:

code /home/labex/project/validate.py

打开文件后,你会发现它包含了几个类。下面简要概述每个类的作用:

  1. Validator:这是一个基类。它有一个 check 方法,但目前这个方法没有任何操作。它是其他验证器类的起点。
  2. Typed:这是 Validator 的子类。它的主要作用是检查一个值是否为特定类型。
  3. IntegerFloatString:这些是从 Typed 继承的特定类型验证器。它们分别用于检查一个值是否为整数、浮点数或字符串。

现在,让我们看看这些验证器类在实际中是如何工作的。我们将创建一个名为 test.py 的新文件来测试它们。要创建并打开这个文件,请运行以下命令:

code /home/labex/project/test.py

test.py 文件打开后,添加以下代码。这段代码将测试 IntegerString 验证器:

from validate import Integer, String, Float

## Test Integer validator
print("Testing Integer validator:")
try:
    Integer.check(42)
    print("✓ Integer check passed for 42")
except TypeError as e:
    print(f"✗ Error: {e}")

try:
    Integer.check("Hello")
    print("✗ Integer check incorrectly passed for 'Hello'")
except TypeError as e:
    print(f"✓ Correctly raised error: {e}")

## Test String validator
print("\nTesting String validator:")
try:
    String.check("Hello")
    print("✓ String check passed for 'Hello'")
except TypeError as e:
    print(f"✗ Error: {e}")

在这段代码中,我们首先从 validate.py 文件中导入 IntegerStringFloat 验证器。然后,我们通过尝试检查一个整数值 (42) 和一个字符串值 ("Hello") 来测试 Integer 验证器。如果对整数的检查通过,我们打印一条成功消息。如果对字符串的检查错误地通过了,我们打印一条错误消息。如果对字符串的检查正确地引发了 TypeError,我们打印一条成功消息。我们对 String 验证器进行了类似的测试。

添加代码后,使用以下命令运行测试文件:

python3 /home/labex/project/test.py

你应该会看到类似以下的输出:

Testing Integer validator:
✓ Integer check passed for 42
✓ Correctly raised error: Expected <class 'int'>

Testing String validator:
✓ String check passed for 'Hello'

如你所见,这些验证器类使我们能够轻松地进行类型检查。例如,当你调用 Integer.check(x) 时,如果 x 不是整数,它将引发 TypeError

现在,让我们考虑一个实际场景。假设我们有一个函数,要求其参数为特定类型。以下是这样一个函数的示例:

def add(x, y):
    Integer.check(x)  ## Make sure x is an integer
    Integer.check(y)  ## Make sure y is an integer
    return x + y

这个函数可以正常工作,但存在一个问题。每次我们想要进行类型检查时,都必须手动添加验证器检查。这可能会很耗时且容易出错,特别是对于较大的函数或项目。

在接下来的步骤中,我们将通过创建一个可调用对象来解决这个问题。这个对象将能够根据函数注解自动应用这些类型检查。这样,我们就不必每次都手动添加检查了。

创建基本的可调用对象

在 Python 中,可调用对象是一种可以像函数一样使用的对象。你可以把它想象成一个在后面加上括号就能“调用”的东西,就像调用普通函数一样。要让 Python 中的类表现得像可调用对象,我们需要实现一个名为 __call__ 的特殊方法。当你使用带括号的对象时,这个方法会自动被调用,就像调用函数一样。

让我们从修改 validate.py 文件开始。我们将在这个文件中添加一个名为 ValidatedFunction 的新类,这个类将作为我们的可调用对象。要在代码编辑器中打开该文件,请在终端中运行以下命令:

code /home/labex/project/validate.py

文件打开后,滚动到文件末尾并添加以下代码:

class ValidatedFunction:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Calling', self.func)
        result = self.func(*args, **kwargs)
        return result

让我们来分析一下这段代码的作用。ValidatedFunction 类有一个 __init__ 方法,它是构造函数。当你创建这个类的实例时,需要传入一个函数。这个函数随后会作为实例的一个属性存储起来,属性名为 self.func

__call__ 方法是使这个类可调用的关键部分。当你调用 ValidatedFunction 类的实例时,__call__ 方法会被执行。以下是它的具体执行步骤:

  1. 它会打印一条消息,告诉你正在调用哪个函数。这对于调试和理解程序运行情况很有帮助。
  2. 它会使用你调用实例时传入的参数来调用存储在 self.func 中的函数。*args**kwargs 允许你传入任意数量的位置参数和关键字参数。
  3. 它会返回函数调用的结果。

现在,让我们来测试这个 ValidatedFunction 类。我们将创建一个名为 test_callable.py 的新文件来编写测试代码。要在代码编辑器中打开这个新文件,请运行以下命令:

code /home/labex/project/test_callable.py

test_callable.py 文件中添加以下代码:

from validate import ValidatedFunction

def add(x, y):
    return x + y

## Wrap the add function with ValidatedFunction
validated_add = ValidatedFunction(add)

## Call the wrapped function
result = validated_add(2, 3)
print(f"Result: {result}")

## Try another call
result = validated_add(10, 20)
print(f"Result: {result}")

在这段代码中,我们首先从 validate.py 文件中导入 ValidatedFunction 类。然后定义了一个简单的函数 add,它接受两个数字并返回它们的和。

我们创建了 ValidatedFunction 类的一个实例,并将 add 函数作为参数传入。这样就把 add 函数“包装”在了 ValidatedFunction 实例中。

然后我们两次调用这个包装后的函数,一次传入参数 23,另一次传入 1020。每次调用包装后的函数时,ValidatedFunction 类的 __call__ 方法都会被调用,进而调用原始的 add 函数。

要运行测试代码,请在终端中执行以下命令:

python3 /home/labex/project/test_callable.py

你应该会看到类似以下的输出:

Calling <function add at 0x7f2d1c3a9940>
Result: 5
Calling <function add at 0x7f2d1c3a9940>
Result: 30

这个输出表明我们的可调用对象正在按预期工作。当我们调用 validated_add(2, 3) 时,实际上是在调用 ValidatedFunction 类的 __call__ 方法,该方法随后调用原始的 add 函数。

目前,我们的 ValidatedFunction 类只是打印一条消息并将调用传递给原始函数。在下一步中,我们将改进这个类,使其能够根据函数的注解进行类型验证。

✨ 查看解决方案并练习

使用函数注解实现类型验证

在 Python 中,你可以为函数参数添加类型注解。这些注解用于表明参数和函数返回值的预期数据类型。默认情况下,它们不会在运行时强制检查类型,但可用于验证目的。

让我们看一个例子:

def add(x: int, y: int) -> int:
    return x + y

在这段代码中,x: inty: int 表明参数 xy 应该是整数。末尾的 -> int 表示函数 add 返回一个整数。这些类型注解存储在函数的 __annotations__ 属性中,这是一个将参数名映射到其注解类型的字典。

现在,我们要增强 ValidatedFunction 类,使其利用这些类型注解进行验证。为此,我们需要使用 Python 的 inspect 模块。该模块提供了一些有用的函数,可用于获取关于实时对象(如模块、类、方法、函数等)的信息。在我们的例子中,我们将使用它来将函数参数与对应的参数名进行匹配。

首先,我们需要修改 validate.py 文件中的 ValidatedFunction 类。你可以使用以下命令打开该文件:

code /home/labex/project/validate.py

将现有的 ValidatedFunction 类替换为以下改进版本:

import inspect

class ValidatedFunction:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __call__(self, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(*args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(*args, **kwargs)

这个改进版本的类的功能如下:

  1. 使用 inspect.signature() 获取函数参数的信息,如参数名、默认值和注解类型。
  2. 使用签名的 bind() 方法将提供的参数与对应的参数名进行匹配。这有助于我们将每个参数与函数中正确的参数关联起来。
  3. 检查每个参数是否符合其类型注解(如果存在)。如果找到注解,则从注解中获取验证器类,并使用 check() 方法进行验证。
  4. 最后,使用经过验证的参数调用原始函数。

现在,让我们使用一些在类型注解中使用了我们的验证器类的函数来测试这个增强后的 ValidatedFunction 类。使用以下命令打开 test_validation.py 文件:

code /home/labex/project/test_validation.py

在文件中添加以下代码:

from validate import ValidatedFunction, Integer, String

def greet(name: String, times: Integer):
    return name * times

## Wrap the greet function with ValidatedFunction
validated_greet = ValidatedFunction(greet)

## Valid call
try:
    result = validated_greet("Hello ", 3)
    print(f"Valid call result: {result}")
except TypeError as e:
    print(f"Unexpected error: {e}")

## Invalid call - wrong type for 'name'
try:
    result = validated_greet(123, 3)
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for name: {e}")

## Invalid call - wrong type for 'times'
try:
    result = validated_greet("Hello ", "3")
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for times: {e}")

在这段代码中,我们定义了一个 greet 函数,其类型注解为 name: Stringtimes: Integer。这意味着 name 参数应该使用 String 类进行验证,times 参数应该使用 Integer 类进行验证。然后,我们使用 ValidatedFunction 类包装 greet 函数,以启用类型验证。

我们进行了三个测试用例:一个有效调用、一个 name 参数类型错误的无效调用,以及一个 times 参数类型错误的无效调用。每个调用都包裹在 try-except 块中,以捕获验证过程中可能引发的任何 TypeError 异常。

要运行测试文件,请使用以下命令:

python3 /home/labex/project/test_validation.py

你应该会看到类似于以下的输出:

Valid call result: Hello Hello Hello
Expected error for name: Expected <class 'str'>
Expected error for times: Expected <class 'int'>

这个输出表明,我们的 ValidatedFunction 可调用对象现在能够根据函数注解执行类型验证。当我们传入错误类型的参数时,验证器类会检测到错误并引发 TypeError。这样,我们可以确保函数使用正确的数据类型进行调用,有助于防止错误并使我们的代码更加健壮。

✨ 查看解决方案并练习

挑战:将可调用对象用作方法

在 Python 中,当你将可调用对象用作类中的方法时,会遇到一个独特的挑战。可调用对象是指像函数一样可以“调用”的对象,比如函数本身,或者带有 __call__ 方法的对象。当将其用作类方法时,由于 Python 会将实例(self)作为第一个参数传递,它并不总是能按预期工作。

让我们通过创建一个 Stock 类来探讨这个问题。这个类将表示一只股票,包含名称、股数和价格等属性。我们还将使用一个验证器来确保我们处理的数据是正确的。

首先,打开 stock.py 文件,开始编写我们的 Stock 类。你可以使用以下命令在编辑器中打开该文件:

code /home/labex/project/stock.py

现在,将以下代码添加到 stock.py 文件中。这段代码定义了 Stock 类,其中包含一个 __init__ 方法来初始化股票的属性,一个 cost 属性来计算总成本,以及一个 sell 方法来减少股数。我们还将尝试使用 ValidatedFunction 来验证 sell 方法的输入。

from validate import ValidatedFunction, Integer

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: Integer):
        self.shares -= nshares

    ## Try to use ValidatedFunction
    sell = ValidatedFunction(sell)

定义好 Stock 类后,我们需要对其进行测试,看看它是否能按预期工作。创建一个名为 test_stock.py 的测试文件,并使用以下命令打开它:

code /home/labex/project/test_stock.py

将以下代码添加到 test_stock.py 文件中。这段代码创建了一个 Stock 类的实例,打印出初始股数和成本,尝试卖出一些股票,然后打印出更新后的股数和成本。

from stock import Stock

try:
    ## Create a stock
    s = Stock('GOOG', 100, 490.1)

    ## Get the initial cost
    print(f"Initial shares: {s.shares}")
    print(f"Initial cost: ${s.cost}")

    ## Try to sell some shares
    s.sell(10)

    ## Check the updated cost
    print(f"After selling, shares: {s.shares}")
    print(f"After selling, cost: ${s.cost}")

except Exception as e:
    print(f"Error: {e}")

现在,使用以下命令运行测试文件:

python3 /home/labex/project/test_stock.py

你可能会遇到类似以下的错误:

Error: missing a required argument: 'nshares'

这个错误的出现是因为当 Python 调用像 s.sell(10) 这样的方法时,实际上在幕后调用的是 Stock.sell(s, 10)self 参数代表类的实例,它会自动作为第一个参数传递。然而,我们的 ValidatedFunction 没有正确处理这个 self 参数,因为它不知道自己被用作方法。

理解问题

当你在类中定义一个方法,然后用 ValidatedFunction 替换它时,实际上是在包装原始方法。问题在于,包装后的方法不能自动正确处理 self 参数。它期望参数的方式没有考虑到实例会作为第一个参数传递。

解决问题

为了解决这个问题,我们需要修改处理方法的方式。我们将创建一个名为 ValidatedMethod 的新类,它可以正确处理方法调用。将以下代码添加到 validate.py 文件的末尾:

import inspect

class ValidatedMethod:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __get__(self, instance, owner):
        ## This method is called when the attribute is accessed as a method
        if instance is None:
            return self

        ## Return a callable that binds 'self' to the instance
        def method_wrapper(*args, **kwargs):
            return self(instance, *args, **kwargs)

        return method_wrapper

    def __call__(self, instance, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(instance, *args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(instance, *args, **kwargs)

现在,我们需要修改 Stock 类,使用 ValidatedMethod 而不是 ValidatedFunction。再次打开 stock.py 文件:

code /home/labex/project/stock.py

Stock 类更新如下:

from validate import ValidatedMethod, Integer

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @ValidatedMethod
    def sell(self, nshares: Integer):
        self.shares -= nshares

ValidatedMethod 类是一个描述符(descriptor),它是 Python 中的一种特殊对象,可以改变属性的访问方式。当属性作为方法被访问时,__get__ 方法会被调用。它会返回一个可调用对象,该对象会正确地将实例作为第一个参数传递。

再次使用以下命令运行测试文件:

python3 /home/labex/project/test_stock.py

现在你应该会看到类似以下的输出:

Initial shares: 100
Initial cost: $49010.0
After selling, shares: 90
After selling, cost: $44109.0

这个挑战向你展示了可调用对象的一个重要方面。当将它们用作类中的方法时,需要特殊处理。通过使用 __get__ 方法实现描述符协议,我们可以创建既能作为独立函数又能作为方法正常工作的可调用对象。

总结

在这个实验中,你学习了如何在 Python 中创建合适的可调用对象。首先,你探索了用于类型检查的基本验证器类,并使用 __call__ 方法创建了一个可调用对象。接着,你对这个对象进行了增强,使其能够基于函数注解执行验证,并解决了将可调用对象用作类方法的挑战。

涵盖的关键概念包括可调用对象和 __call__ 方法、用于类型提示的函数注解、使用 inspect 模块检查函数签名,以及用于类方法的带有 __get__ 方法的描述符协议。这些技术使你能够创建强大的函数包装器,用于调用前和调用后的处理,这是装饰器和其他 Python 高级特性的基本模式。