【汉化】【Python中级技巧】Python的.__call__()方法:创建可调用实例

原文链接:https://realpython.com/python-callable-instances/

by Leodanis Pozo Ramos May 24, 2023

目录:

  • 理解Python中的可调用对象
  • 检查一个对象是否可调用
  • 在Python里使用.__call__()创建可调用实例
  • 理解它们的不同:.__init__() vs .__call__()
  • 将Python的.__call__()付诸实践
    • 编写含状态信息的可调用对象
    • 缓存计算过的值
    • 创建清晰便捷的APIs
  • 探索.__call__的高级使用案例
    • 编写基于类的装饰器
    • 实现策略设计模式( strategy design pattern)
  • 结语

在Python里,一个可调用对象是能使用一对圆括号和一系列可选参数调用的对象。函数、类和方法都是Python里可调用对象的常见例子。除了这些,你还可以创建自定义的产生可调用实例的类。为了做到这一点,你得把**.__call__()**特殊方法加到你的类里。

含有.__call__()方法的类的实例就跟函数类似,让你能灵活便捷地给对象添加功能。作为一个Python开发者,理解如何创建并使用可调用对象是一项很有价值的技能。

在这个教程中,你将:

  • 理解Python里可调用对象的概念
  • 通过在你的类里添加**.__call__()方法创建可调用实例**
  • 理解**.__init__().__call__()**的区别
  • 编写一些使用可调用实例解决真实案例的代码

为了最大程度地吸收本教程,你需要熟悉Python的 object-oriented (面向对象)基本知识,包括如何定义和使用 classes 及其方法。如果对Python里的 decorators (装饰器)以及 strategy design pattern (策略设计模式)有了解就更好了。你也应该理解state(状态)的概念。

理解Python中的可调用对象

Python中的一个 callable (可调用对象)是任何你能用一对圆括号和一系列可选参数调用的对象。在你和Python的日常交互中,就会发现不同的可调用对象的例子。这里罗列了一些:

  • Built-in(内置)函数和类
  • 你使用 def 关键字创建的用户自定义 functions (函数)
  • 你使用 lambda 关键字创建的匿名函数
  • 你的自定义 classes 里的 constructors (构造器)
  • Instance, class和 static 方法
  • 实现了 .__call__() 方法的类的实例
  • 你的函数返回的Closures(闭包)
  • 你使用 yield 关键字定义的 Generator 函数
  • 你使用 async 关键字创建的 Asynchronous(异步)函数和方法

所有这些不同的可调用对象都有共同点。他们实现了.__call__()特殊方法。为了验证这一点,你可以使用内置的 dir() 函数,这个函数接收一个对象作为参数,以列表形式 returns 对象的属性和方法:

>>> dir(abs)
[
    '__call__',
    '__class__',
    ...
]

>>> dir(all)
[
    '__call__',
    '__class__',
    ...
]

>>> def greet():
...     print("Hello, World!")
...

>>> dir(greet)
[
    '__annotations__',
    '__builtins__',
    '__call__',
    ...
]

在前两个例子中,你将内置函数 abs()all() 作为参数传给dir()并调用,可以看到.__call__()方法都呈现在了输出里。

在最后一个例子里,你定义了一个 prints 信息到屏幕上的函数。这个函数也有.__call__()。注意你可以用这个方法调用函数:

>>> greet.__call__()
Hello, World!

注意在上面这个例子里用.__call__()就跟直接用greet()调用的效果是一样的。

**注意:**虽然你可以直接调用.__call__()这样的特殊方法,但不建议真这么干。相反,像你平常那样调用函数就行。

现在,这些内部是怎么运作的呢?当你运行了类似这样的东西callable_object(*args, **kwargs),Python内部把运算符翻译成了callable_object.__call__(*args, **kwargs)。传给常规函数的参数就是用在.__call__()里的参数。换句话说,不论何时你调用一个可调用对象,Python都会在幕后,用你传进来的参数自动运行它的.__call__()方法。

现在看下面这个自定义类:

>>> class SampleClass:
...     def method(self):
...         print("You called method()!")
...

>>> type(SampleClass)
<class 'type'>

>>> dir(type)
[
    '__abstractmethods__',
    '__annotations__',
    '__base__',
    '__bases__',
    '__basicsize__',
    '__call__',
    ...
]

>>> sample_instance = SampleClass()
>>> dir(sample_instance.method)
[
    '__call__',
    '__class__',
    ...
]

在Python里,所有东西都是对象。像SampleClass这样的类是type的对象,你可以通过将这个类的对象作为参数传给type()并调用,或是查看它的.__class__属性来确认。

SampleClass的 class constructor 回退到使用type.__call__()。这就是为什么你能调用SampleClass()来得到一个新实例。所以,类构造器是返回新的底层类实例的可调用对象。

在上面的例子中,你可以看出方法对象,例如sample_instance.method,也有一个把它们变成可调用对象的.__call__()特殊方法。重点就是,一个对象得有.__call__()方法才能成为可调用对象。

检查一个对象是否可调用

如果你需要检查一个Python对象是否可调用,那么你可以使用内置的 callable() 函数,就像下面这样:

>>> callable(abs)
True
>>> callable(all)
True

>>> callable(greet)
True

>>> callable(SampleClass)
True

>>> callable(sample_instance)
False

callable()函数将一个对象作为参数,如果这个对象可调用,就返回True。否则返回False

**注意:**由于dir()会检查给定对象是否有.__call__()方法,你可以使用它来检查。虽然dir()方法在你测试代码和案例时很有用,但在你需要快速检查一个对象是否可调用时没有太大帮助。相反,callable()函数是你能直接在 Boolean context (布尔上下文)中使用的 predicate (谓词)函数。

在上面这些例子中,sample_instance以外的所有被测试的对象都可调用。这是可想而知的,因为SampleClass并没有为它的实例实现.__call__()方法。是的,你猜对了!通过编写一个.__call__()方法,就可以使自定义类的实例变成可调用的。在后续章节,你会学到把类的实例变成可调用对象的基础知识。

但是首先,一定要注意到有时callable()会导致假阳性(即把不可调用当成可调用):

>>> class NonCallable:
...     def __call__(self):
...         raise TypeError("not really callable")
...

>>> instance = NonCallable()
>>> callable(instance)
True

>>> instance()
Traceback (most recent call last):
    ...
TypeError: not really callable

在这个例子中,callable()返回True。然而,自定义类的实例并不可调用,如果你试图调用就会报错。所以,callable()只能保证实例所属的类实现了.__call__()方法。

在Python里使用.__call__()创建可调用实例

如果你希望给定类的实例可调用,就需要在底层类实现.__call__()特殊方法。这个方法使你可以像调用Python里的常规函数一样调用你的类的实例。

不像别的特殊方法,.__call__()没有对传入参数的硬性要求。在某种意义上,它就和任何别的方法一样,接收self作为第一个参数,然后接收任意数量的额外参数。

这是一个具有.__call__()方法的类的实例如何运作的案例:

# counter.py

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1

    def __call__(self):
        self.increment()

在这个Counter类里,你有一个.count instance attribute (实例/对象属性)来追踪当前计数。然后你有一个.increment()方法在每次调用时给计数加1。最后,你加了一个.__call__()方法。在这个例子中,.__call__()回退到调用.increment(),是一种执行增长操作的快捷手段。

看看这个类实际怎么运作的:

>>> from counter import Counter

>>> counter = Counter()

>>> counter.increment()
>>> counter.count
1

>>> counter()
>>> counter.count
2
>>> counter()
>>> counter.count
3

在创建了Counter的一个实例后,你调用.increment()。这次调用把.count属性增加了1,你可以通过访问属性来确认。在后续的例子里,由于你的类里有一个.__call__()方法,你方便地调用实例来直接增加计数。

在这个例子中,.__call__()给计数增长提供了一个快捷手段。这种特性赋予你的类一个方便的、用户友好型的接口。

上面这个例子中的.__call__()方法没有接收任何参数。这个方法也不会显式地返回任何值。然而,在自定义类里如何编写.__call__()方法并没有严格要求。所以,你可以让它们接收参数、返回值,甚至像你的Counter例子一样引发 side effects (附加作用)。

继续看第二个例子,考虑下面这个类,它允许你创建可调用对象来计算不同的幂:

# power.py

class PowerFactory:
    def __init__(self, exponent=2):
        self.exponent = exponent

    def __call__(self, base):
        return base**self.exponent

在这个例子中,你的 PowerFactory 类接收一个 exponent 参数,你之后将用这个参数来进行不同的幂运算。这个.__call__()方法接收一个base参数然后用之前提供的指数计算它的幂。最后,这个方法返回计算后的结果。

你的类是这样运作的:

>>> from power import PowerFactory

>>> square_of = PowerFactory(2)
>>> square_of(3)
9
>>> square_of(6)
36

>>> cube_of = PowerFactory(3)
>>> cube_of(3)
27
>>> cube_of(6)
216

在这里,你使用 PowerFactory 来创建两个不同的可调用实例。第一个实例计算数的平方,第二个实例计算三次方。

在这个例子中,你需要传入在调用 square_ofcube_of 时传入一个 base 参数,因为这些调用实际上是回退到调用 .__call__() 。最后,注意从每次调用中你都获得了幂运算的结果,这是因为 .__call__() 返回了指定幂的计算结果。

在自定义类中定义一个 .__call__() 方法让你能把这些类的实例当普通Python函数使用。这个特性在一些情况下很方便,你将在后续的将Python的.__call__()付诸实践章节学到。

在进入可调用实例的一般使用案例前,你将探索.__init__().__call__()方法的不同。这两个方法以及他们在Python类中互相关联的角色特点会让很多正开始学习Python的人感到困惑。

理解它们的不同:.__init__() vs .__call__()

在Python类中区分.__init__().__call__两者的角色对刚开始学习编程语言或其面向对象特点的开发者来说是一项很容易混淆的任务。

然而,这两个方法还是挺不一样的,每个都有特定的目的。

.__init__()方法是 instance initializer 。Python在你调用类的构造器创建一个类的实例时自动调用这个方法。.__init__()的参数和构造这个类时用的参数一样,这些参数为类的属性提供初始值。

同时,.__call__()方法将实例变成可调用对象。正如你已经学到的,Python会在你调用一个具体的类的实例时自动调用该方法。

为了阐明这两个方法的不同,考虑下面这个案例类:

>>> class Demo:
...     def __init__(self, attr):
...         print(f"Initialize an instance of {self.__class__.__name__}")
...         self.attr = attr
...         print(f"{self.attr = }")
...
...     def __call__(self, arg):
...         print(f"Call an instance of {self.__class__.__name__} with {arg}")
...

Demo类实现了.__init__().__call__()。在.__init__()中,你打印一段信息并初始化.attr属性。在.__call__()中,你仅仅打印一段信息,好在这个方法以给定参数被调用时能知晓。

这个类是这样起作用的:

>>> demo = Demo("Some initial value")
Initialize an instance of Demo
self.attr = 'Some initial value'

>>> demo("Hello!")
Call an instance of Demo with Hello!

正如你看到的,在你的类里,每个方法扮演的角色是不同的。.__init__()方法在你创建类的实例时被调用,它的主要目的是用合理的初始值初始化实例的属性。

你在所有的Python类里都能找到.__init__()方法。一些类有显式的实现,别的则 inherit (继承自)父类。在许多情况下,object 类提供了这个方法:

>>> dir(object)
[
    ...
    '__gt__',
    '__hash__',
    '__init__',
    ...
]

记住 object 类是所有Python类的父类。所以,即使你不在自定义类里显式地定义.__init__()方法,这个类也会继承 object 类里的默认实现。

相反,.__call__()方法在调用其所属类的具体实例时会执行,就像这个例子中的demo.__call__() 的目的是把你的实例变成可调用对象。换句话说,它的目的是创建出像普通函数一样供你调用的对象。大多数Python类都没实现这个方法。只有当你需要把自定义类的实例当函数用时才需要去实现它。

好了!在澄清了.__call__().__init__()的不同点后,你已经准备好继续探索在Python代码中使用.__call__()带来的益处了。

将Python的.__call__()付诸实践

在一些情况下,写一个能产生可调用实例的类是很有用的。比如说,在以下这些情景你会受益:

  • 在调用之间保留状态
  • 缓存之前计算的结果
  • 实现直截了当、便捷的API

虽然你可以以更普通的方式,用函数或类解决上面的问题,使用可调用对象在某些情境下也是个不错的选择。当你已经有了一个类并且需要它具有函数式的行为时尤其如此。

在接下来的章节里,你将编写一些阐明上述每个使用情景的实际案例。

编写含状态信息的可调用对象

有时,你可能希望写出在数次调用之间保存了 state (状态)的可调用对象,一般称之为stateful的可调用对象。例如,你想写一个可调用对象,它可以从数据流中接收连续不断的数值然后计算它们的累积平均值。在数次调用之间,这个可调用对象必须持续关注之前传入的值。

为了解决这个问题,你可以用一个像这样的 closure (闭包):

>>> def cumulative_average():
...     data = []
...     def average(new_value):
...         data.append(new_value)
...         return sum(data) / len(data)
...     return average
...

>>> stream_average = cumulative_average()

>>> stream_average(12)
12.0
>>> stream_average(13)
12.5
>>> stream_average(11)
12.0
>>> stream_average(10)
11.5

cumulative_average() 中,你使用了 data 这个局部变量来储存数次调用的数据。然后你定义了一个叫 average() 的内层函数。这个函数在每次调用时接收一个新值并追加到 data 里。然后这个函数计算并返回当前储存数据的平均值。

最后, cumulative_average() 返回内层函数。实际上,返回了一个闭包,这是一个将 average() 函数和它的非局部作用域打包在一起的特殊对象。在这个案例中,闭包中包含了 data 变量。

你编写完 cumulative_average() 之后,就可以创建像 stream_average 这样的自定义闭包了。这个对象可调用,所以你可以把它当函数用,来计算数据流的累积平均值,正如你在上面的最终实例中做的那样。

即使闭包允许你保存多次调用之间的状态,这个工具可能显得较难理解、编写。在这种情境下,写一个具有 .__call__() 方法的类对你的任务更加有益,也会使你的代码可读性更强、更清晰。

**注意:**要深入了解Python中的闭包和作用域,看这里: Exploring Scopes and Closures in Python 。

这是你用带有 .__call__() 方法的类解决上面问题的例子:

# cumulative_average.py

class CumulativeAverager:
    def __init__(self):
        self.data = []

    def __call__(self, new_value):
        self.data.append(new_value)
        return sum(self.data) / len(self.data)

在这个例子中,你的类用一个 .data 对象属性来保存数据。 .__call__() 方法在每次调用时接收一个新的值,然后追加到 .data 里,最后计算并返回平均值。

在这个案例里,你的代码可读性很强。 .data 属性保存了多次调用的状态, .__call__() 方法计算了累积平均值。看看这个类实际如何起作用的:

>>> from cumulative_average import CumulativeAverager

>>> stream_average = CumulativeAverager()
>>> stream_average(12)
12.0
>>> stream_average(13)
12.5
>>> stream_average(11)
12.0
>>> stream_average(10)
11.5
>>> stream_average.data
[12, 13, 11, 10]

CumulativeAverager 的实例是可调用对象,在每次调用时,都保存了之前所有值并计算累积平均值。这种方式让你的代码更容易理解。要写出这个类,你不需要理解Python闭包复杂的工作原理。

另一个让人感兴趣的优点是,你可以通过 .data 属性直接访问到当前数据。

缓存计算过的值

另一个可调用对象的常见用例是需要一个含状态信息的可调用对象以便能 caches (缓存)多次调用间计算过的值。当你需要优化算法时这很有用。

比如说,你想计算给定数值的阶乘,由于你打算多次运行这个计算,所以得讲究效率。一种方式是把算过的值缓存下来,这样你就不用总是算它们了。

这里有一个使用 .__call__() 实现了这种效果的类:

# factorial.py

class Factorial:
    def __init__(self):
        self.cache = {0: 1, 1: 1}

    def __call__(self, number):
        if number not in self.cache:
            self.cache[number] = number * self(number - 1)
            
        return self.cache[number]

在这个类里,你使用一个 dictionary 来缓存已经算过的阶乘值。这个字典的键是已经传入过的数,值是已经计算过的阶乘。

.__call__() 方法检查了当前输入是否在 .cache 字典里。如果是的话,就返回相关的值,不需要再计算。这就优化了你的算法,让它更快。

如果当前输入的数没有在 .cache 字典里,那这个方法就递归地计算阶乘,缓存结果,返回最终的值。

看看这个类如何起作用:

>>> from factorial import Factorial

>>> factorial_of = Factorial()

>>> factorial_of(4)
24
>>> factorial_of(5)
120
>>> factorial_of(6)
720

>>> factorial_of.cache
{0: 1, 1: 1, 2: 2, 3: 6, 4: 24, 5: 120, 6: 720}

每次调用 Factorial 的实例都会检查缓存看看已经计算过的值。实例只会计算还没算过的数的阶乘。注意最终所有的输入值和阶乘都会存在 .cache 字典里。

创建清晰便捷的APIs

编写能产生可调用实例的类还让你能在库、 packages, and modules(包、模组)中设计出方便、用户友好型的 application programming interfaces (APIs) 。

比如说,你在编写一个新的、很炫酷的库,用来创建 GUI applications (图形化用户接口应用程序)。你的库有一个 MainWindow 类提供了创建GUI程序主窗口的所有功能。

这个类将有一些方法,包括一个在屏幕上绘制出窗口的 .show() 方法。在这种情况下,你可以像这样提供一个 .__call__() 方法:

class MainWindow:
    def show(self):
        print("Showing the app's main window...")

    def __call__(self):
        self.show()

    # ...

在这个例子中,.__call__()方法回退到调用 .show() 方法。这种实现让你在调用 .show() 或实例本身时都能展示出主窗口:

window = MainWindow()
window()  # Or window.show()

在这个例子中,.__call__() 提供了一个快捷方式来在屏幕上展示程序窗口。这能提升你的用户的使用体验。所以,这种技巧非常有助于在Python项目中创建用户友好型和易懂的接口。

另外一个能提升你API的案例是,你有个类的首要目的是提供单一的功能或行为。比如说,你想要一个 Logger 类往文件里记录信息:

# logger.py

class Logger:
    def __init__(self, filename):
        self.filename = filename

    def __call__(self, message):
        with open(self.filename, mode="a", encoding="utf-8") as log_file:
            log_file.write(message + "\n")

在这个例子中,Logger 的主要目的就是往你提供的日志文件里写入信息。通过实现 .__call__() 方法,只要像函数一样调用对象,就能快捷地访问这个功能。

探索.__call__的高级使用案例

至今,你已经学到了很多在类里使用 .__call__() 方法创建可调用实例的知识。在Python里,这个方法也有一些高级用法。其中之一是创建 class-based decorators (基于类的装饰器)。在这种情况下,只有用.__call__() 方法,因为它能确保对象可调用。

另一个 .__call__() 的有趣用法是当你想在Python里实现 strategy design pattern (策略设计模式)。这时,用.__call__() 来创建实现了不同策略的类就大有好处。

在接下来的章节里,你会学习如何使用 .__call__() 来创建基于类的装饰器,以及在Python里实现策略模式。

编写基于类的装饰器

Python的 decorators (装饰器)是可调用对象,把其他的可调用对象作为参数,并不显式地修改代码,却拓展了它们的行为。装饰器提供了一个很好的工具来给现有的可调用对象增加功能。

看到别人写、或是自己编写基于函数的装饰器都很常见。然而,通过充分利用 .__call__() 特殊方法,你也可以编写基于类的装饰器

为了说明如何做到这一点,就比如说你想创建一个测量自定义函数执行时间的装饰器。下面这段代码展示了该怎么基于类编写装饰器:

# timing.py

import time

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

    def __call__(self, *args, **kwargs):
        start = time.perf_counter()
        result = self.func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{self.func.__name__}() took {(end - start) * 1000:.4f} ms")
        return result

ExecutionTimer 类在初始化时,接收一个函数对象作为参数。.__call__() 方法作用于这个函数对象。在这个例子中,.__call__() 使用*args and **kwargs 泛型参数来处理任何输入的那个函数需要的参数。

接下来,你用 time.perf_counter() 来获取输入的函数运行前后的时间。然后你打印出函数名和毫秒级执行时间。最后一步是返回输入的函数调用后的结果。

**注意:**如果想更深入地了解Python中给代码计时的最佳实践,看这个 Python Timer Functions: Three Ways to Monitor Your Code。

有了这个类,你就可以立马开始测量你的Python函数的执行时间了:

>>> from timing import ExecutionTimer

>>> @ExecutionTimer
... def square_numbers(numbers):
...     return [number ** 2 for number in numbers]
...

>>> square_numbers(list(range(100)))
square_numbers() took 0.0069 ms
[
    0,
    1,
    4,
    9,
    16,
    25,
    ...
]

在这个代码段里,你有一个接收列表并返回平方后的列表的函数。你想测量这个函数的执行时间,为此使用了 ExecutionTimer 装饰器。

一旦这个函数被装饰了,不论何时你运行它,都会收到一则含有函数名和毫秒级执行时间的信息。然后你收到函数的返回值。

现在,比如说你想加一个 repetitions 到装饰器里。这个参数让你能反复运行输入函数,计算平均执行时间:

# timing.py

import time

class ExecutionTimer:
    def __init__(self, repetitions=1):
        self.repetitions = repetitions

    def __call__(self, func):
        def timer(*args, **kwargs):
            result = None
            total_time = 0
            print(f"Running {func.__name__}() {self.repetitions} times")
            for _ in range(self.repetitions):
                start = time.perf_counter()
                result = func(*args, **kwargs)
                end = time.perf_counter()
                total_time += end - start
            average_time = total_time / self.repetitions
            print(
                f"{func.__name__}() takes "
                f"{average_time * 1000:.4f} ms on average"
            )
            return result

        return timer

这个升级版本的 ExecutionTimer 和最初的实现有很大不同。这个类的初始器接收一个你提供的 repetitions 参数作为装饰器调用的一部分。

.__call__() 内部,你接收输入函数作为参数。然后创建一个内层函数来执行输入函数。在内层函数里,你使用 for loop (for循环)来多次运行输入函数然后计算总的执行时间。

接下来,你计算平均执行时间并像往常一样打印信息。最后,你返回输入函数的运算结果。注意 .__call__() 返回了 timer 代表的那个函数。

有了这些改动,继续试试 ExecutionTimer 。注意要访问新版的 ExecutionTimer ,你需要先 reload (重载)timing.py 文件或者重启你当前的 interactive session:

>>> from timing import ExecutionTimer

>>> @ExecutionTimer(repetitions=100)
... def square_numbers(numbers):
...     return [number ** 2 for number in numbers]
...

>>> square_numbers(list(range(100)))
Running square_numbers() 100 times
square_numbers() takes 0.0073 ms on average
[
    0,
    1,
    4,
    9,
    16,
    25,
    ...
]

你的装饰器现在让你可以运行目标函数特定次,然后计算平均运行时间,太好啦!

实现策略设计模式

策略设计模式允许你定义一系列类似的算法然后运行时可交替。换句话说,这个模式为特定种类的问题提供了不同的解决方案,每个解决方案绑定在一个特定对象里。然后,你可以动态地选择合适的解决方案。

**注意:**策略设计模式对那些函数不是 first-class citizens (一等公民)的语言来说也很有用。比如 C++ 或 Java,使用这种模式让你可以把函数作为参数传给其他函数。

举一个用 .__call__() 实现策略模式的例子,比如说你需要 serialize (序列化)一些数据,转成JSON 或者 YAML,取决于特定场景。这样,你就可以用策略模式。你用一个类把序列化数据转成JSON,另一个类把序列化数据转成YAML。

在接下来这个例子中,你会编写一个针对这个问题的解决方案。注意为了让这个例子运行起来,你得先 pip install pyyaml 因为Python标准库不支持任何处理YAML数据的工具。这是一块 missing battery 。

**注意:**为了安装pyyaml,你需要先创建一个Python virtual environment (虚拟环境),如果你还没有的话。这样,就可以避免在系统Python里挤满一堆平常用不上的包。

这是代码:

# serializing.py

import json

import yaml

class JsonSerializer:
    def __call__(self, data):
        return json.dumps(data, indent=4)

class YamlSerializer:
    def __call__(self, data):
        return yaml.dump(data)

class DataSerializer:
    def __init__(self, serializing_strategy):
        self.serializing_strategy = serializing_strategy

    def serialize(self, data):
        return self.serializing_strategy(data)

在这个例子中,你有 JsonSerializerYamlSerializer 类,代表了你的序列化策略。它们的 .__call__() 方法使用合适的工具来把输入数据序列化为JSON或YAML,视情况而定。

然后你有 DataSerializer 类,提供了更高级的类。你将使用这个类来序列化你的数据。首先,你需要提供一个具体的序列化类的可调用实例:

>>> from serializing import DataSerializer, JsonSerializer, YamlSerializer

>>> data = {
...     "name": "Jane Doe",
...     "age": 30,
...     "city": "Salt Lake City",
...     "job": "Python Developer",
... }

>>> serializer = DataSerializer(JsonSerializer())
>>> print(f"JSON:\n{serializer.serialize(data)}")
JSON:
{
    "name": "Jane Doe",
    "age": 30,
    "city": "Salt Lake City",
    "job": "Python Developer"
}

>>> # Switch strategy
>>> serializer.serializing_strategy = YamlSerializer()
>>> print(f"YAML:\n{serializer.serialize(data)}")
YAML:
age: 30
city: Salt Lake City
job: Python Developer
name: Jane Doe

在这段代码里,你有一个包含了样例数据的字典。为了处理这些数据,你以 JsonSerializer 为参数,创建了一个 DataSerializer 类的实例,在这之后,你的实例能把字典转化成JSON。

在最后那个例子里,你改变了序列化策略然后用你的数据序列化对象来把数据转成YAML编码。你还能想出其他有用的数据序列化对象吗?

结语

关于Python里的可调用实例,你学到了很多,尤其是如何在自定义类里借助 .__call__() 特殊方法创建它们。现在你知道了如何创建这些类,它们产生的对象让你能像普通函数一样调用。这使你的面向对象编程更加灵活、功能性更强。

在本篇教程中,你学到了:

  • 理解Python里可调用对象的概念
  • 通过在你的类里添加**.__call__()方法创建可调用实例**
  • 理解**.__init__().__call__()**的区别
  • 实现多种使用可调用实例解决真实问题的案例

有了这些知识,你就能在Python代码里设计、实现可调用对象。你可以解决多种常见问题,例如保存调用状态、缓存数据、编写基于类的装饰器等等。

你可能感兴趣的:(Python中级技巧,python,开发语言)