Python装饰器模式


在Python中,函数是一等公民,装饰器是强大的语法糖,利用这一功能给程序员提供了一种看似 "神奇 "的方式来构造函数和类的有用组合。
这是一个重要的语言特性,它使 Python 与传统的 OOP 语言如 C++ 和 Java 区别开来,后者实现这种功能需要更多的代码,或者更复杂的模板化代码。
与C++这样的语言相比,Python的这种动态特性产生了更多的运行时开销,但它使代码更容易编写和理解。
这对程序员和项目来说是一个胜利;在大多数现实世界的软件工程工作中,运行时性能不是一个瓶颈。

在 Python 中,函数是第一等公民:函数可以传递给其他函数,可以从函数中返回,并且可以即时创建。让我们看一个例子。

# 即时定义一个函数
pow2 = lambda x: x**2
print(pow2(2))

# 将一个函数作为参数
def print_twice(func: Callable, arg: Any):
    print(func(arg))
    print(func(arg))
print_twice(pow2, 3)

# 将一个函数作为参数,并返回一个新函数
def hello():
    print('Hello world!')
def loop(func: Callable, n: int):
    for _ in range(n):
        func()
loop(hello, 3)

输出:
4
9
9
Hello world!
Hello world!
Hello world!

Pythons中的装饰器是语法糖,用于将函数传递给函数并返回一个新函数。

这篇文章的代码在Github上。

@measure:没有参数的装饰器函数
让我们举一个有用的例子,测量执行一个函数需要多长时间。最好的情况是,我们可以很容易地注解一个现有的函数,并获得 "免费 "的测量。让我们来看看下面两个函数。

from timeit import default_timer as timer
from time import sleep

def measure(func: Callable):
    def inner(*args, **kwargs):
        print(f'---> Calling {func.__name__}()')
        start = timer()
        func(*args, **kwargs)
        elapsed_sec = timer() - start
        print(f'---> Done {func.__name__}(): {elapsed_sec:.3f} secs')
    return inner

def sleeper(seconds: int = 0):
    print('Going to sleep...')
    sleep(seconds)
    print('Done!')

measure()是一个函数,它接收一个函数func()作为参数,并返回一个在内部声明的函数inner()。inner()接收任何传入的参数并将它们传给func(),但将这个调用包裹在几行中,以测量并打印以秒为单位的经过时间。 sleeper()是一个测试函数,它明确地睡了一会儿,所以我们可以测量它。

鉴于这些,我们可以构建一个测量的sleeper()函数,如:

measured_sleeper = measure(sleeper)
measured_sleeper(3)
输出:

---> Calling sleeper()
Going to sleep...
Done!
---> Done sleeper(): 3.000 secs

这很有效,但如果我们已经在很多地方使用了sleeper(),我们就必须用measured_sleeper()来取代所有这些调用。相反,我们可以

sleeper = measure(sleeper)

这里我们替换了当前作用域中的sleeper引用,使之指向原始sleeper()函数的测量版本。这和在函数声明前面加上@decorator 是一回事。

@measure
def sleeper(seconds: int = 0):
    print('Going to sleep...')
    sleep(seconds)
    print('Done!')

因此,@decorators只是语法上的糖,将一个新定义的函数传递给一个现有的装饰器函数,该函数返回一个新的函数,并让原来的函数名指向这个新的函数。

@repeat:参数化的装饰器函数
在上面的例子中,我们采用了一个现有的函数sleeper(),并用一个函数的取值和返回函数的措施()来装饰它,即一个@装饰器。如果我们想把参数传递给装饰器函数本身呢?例如,设想我们有一个函数,我们想重复它n次。为了达到这个目的,我们只需要再添加一个内部函数。

def repeat(n: int = 1):
    def decorator(func: Callable):
        def inner(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return inner
    return decorator

@repeat(n=3)
def hello(name: str):
    print(f'Hello {name}!')

hello('world')

输出:

Hello world!
Hello world!
Hello world!

@trace: 用一个函数来装饰一个类
我们还可以对类进行装饰,而不仅仅是函数。举个例子,假设我们有一个现有的类Foo,我们想对它进行追踪,也就是说,在每次调用某个方法时得到一个print(),而不需要手动改变每个方法。所以我们希望能够在类的定义前加上@trace,这样就可以免费获得这个功能,比如:

@trace
class Foo:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'


trace()是什么样子的?它必须接受一个cls参数(新定义的类,在我们的例子中是Foo),并返回一个新的/修改过的类(加入了跟踪功能)。

def trace(cls: type):
    def make_traced(cls: type, method_name: str, method: Callable):
        def traced_method(*args, **kwargs):
            print(f'Executing {cls.__name__}::{method_name}...')
            return method(*args, **kwargs)
        return traced_method
    for method_name, method in getmembers(cls, ismethod):
        setattr(cls, method_name, make_traced(cls, method_name, method))
    return cls


这个实现是非常直接的。我们遍历 cls.__dict__.items() 中的所有方法,并用一个包装好的方法来替换,我们用内部的 make_traced() 函数制造这个方法。它是有效的。

f1 = Foo()
f2 = Foo(4)
f1.increment()
print(f1)
print(f2)

输出:

Executing Foo::__init__...
Executing Foo::__init__...
Executing Foo::increment...
Executing Foo::__str__...
This is a Foo object with i = 1
Executing Foo::__str__...
This is a Foo object with i = 4


@singleton:单例模式
用函数来装饰一个类的第二个例子是实现常见的单子模式。

在软件工程中,单子模式是一种软件设计模式,它将一个类的实例化限制为一个 "单一 "的实例。当完全需要一个对象来协调整个系统的行动时,这很有用。

我们的实现是一个Python装饰器@singleton:

def singleton(cls: type):
    def __new__singleton(cls: type, *args, **kwargs):
        if not hasattr(cls, '__singleton'):
            cls.__singleton = object.__new__(cls) # type: ignore
        return cls.__singleton                    # type: ignore
    cls.__new__ = __new__singleton                # type: ignore
    return cls

正如在Enum文章中提到的,在调用__init__()对新创建的实例进行初始化之前,会调用__new__()类方法来构造新的对象。所以,为了得到单子行为,我们只需要覆盖__new__(),使其总是返回一个单一的实例。让我们来测试一下。

@singleton
class Foo:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'

@singleton
class Bar:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'

f1 = Foo()
f2 = Foo(4)
f1.increment()
b1 = Bar(9)
print(f1)
print(f2)
print(b1)
print(f1 is f2)
print(f1 is b1)

输出:

This is a Foo object with i = 5
This is a Foo object with i = 5
This is a Bar object with i = 9
True
False

@Count:用一个类来装饰一个类
上面的代码之所以有效,是因为在 Python 中,类的声明实际上只是一个函数的语法糖,这个函数构造一个新的类型对象。例如,上面声明的类 Foo 也可以通过编程方式来定义,比如:

def make_class(name):
    cls = type(name, (), {})
    setattr(cls, 'i', 0)
    def __init__(self, i): self.i = i
    setattr(cls, '__init__', __init__)
    def increment(self): self.i += 1
    setattr(cls, 'increment', increment)
    def __str__(self): return f'This is a {self.__class__.__name__} object with i = {self.i}'
    setattr(cls, '__str__', __str__)
    return cls

Foo = make_class('Foo')

但是,如果是这样的话,我们不仅可以用函数来装饰一个函数,用函数来装饰一个类,还可以用类来装饰一个类。让我们看一个@Count模式的例子,我们想计算创建的实例的数量。我们有一个现有的类,我们希望能够在类的定义前加上@Count,然后得到一个 "免费 "的创建实例的数量,然后我们可以使用装饰器Count类来访问。解决办法是:

class Count:
    instances: DefaultDict[str, int] = defaultdict(int) # we will use this as a class instance
    def __call__(self, cls): # here cls is either Foo or Bar
        class Counted(cls): # here cls is either Foo or Bar
            def __new__(cls: type, *args, **kwargs): # here cls is Counted
                Count.instances[cls.__bases__[0].__name__] += 1
                return super().__new__(cls) # type: ignore
        Counted.__name__ = cls.__name__
        # without this ^ , self.__class__.__name__ would
        # be 'Counted' in the __str__() functions below
        return Counted


诀窍在于,当一个类被用Count装饰时,它的__call__()方法会被运行时调用,并且该类被作为cls传入。在内部,我们构造了一个新的类 Counted,它的父类是 cls,但重写了 __new__(),并在 Count 类的变量实例中增加了一个计数器(但除此之外还创建了一个新的实例并返回)。然后,新构造的 Counted 类(其名称被重写)被返回,并取代了原来定义的类。让我们看看它的操作。

@Count()
class Foo:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'
@Count()
class Bar:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'

f1 = Foo()
f2 = Foo(6)
f2.increment()
b1 = Bar(9)
print(f1)
print(f2)
print(b1)
for class_name, num_instances in Count.instances.items():
    print(f'{class_name} -> {num_instances}')

输出:
This is a Foo object with i = 0
This is a Foo object with i = 7
This is a Bar object with i = 9
Foo -> 2
Bar -> 1


@app.route: 通过装饰函数构建类似Flask的应用对象
最后,我们中的许多人都使用过Flask,并按照以下的思路编写过HTTP处理函数:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

这是对装饰器模式的又一次创造性使用。在这里,我们通过添加我们的自定义处理函数来建立一个应用对象,但我们不必担心定义我们自己的从Flask派生的类,我们只需编写我们装饰的平面函数。这个功能可以直接复制成一个玩具Router类。

class Router:
    routes: dict[str, Callable] = {}

    def route(self, prefix: str):
        def decorator(func: Callable):
            self.routes[prefix] = func
        return decorator

    def default_handler(self, path):
        return f'404 (path was {path})'

    def handle_request(self, path):
        longest_match, handler_func = 0, None
        for prefix, func in self.routes.items():
            if path.startswith(prefix) and len(prefix) > longest_match:
                longest_match, handler_func = len(prefix), func
        if handler_func is None:
            handler_func = self.default_handler
        print(f'Response: {handler_func(path)}')

这里唯一的诀窍是Router::route()可以像一个装饰器一样行事,并返回一个函数。使用实例:

app = Router()

@app.route('/')
def hello(_):
    return 'Hello to my server!'

@app.route('/version')
def version(_):
    return 'Version 0.1'

app.handle_request('/')
app.handle_request('/version')
app.handle_request('does-not-exist')
输出:

Response: Hello to my server!
Response: Version 0.1
Response: 404 (path was does-not-exist)

@decorator vs @decorator()
在@measure的例子中,我们写道。

@measure
def sleeper(seconds: int = 0):
    ...


我们是否也可以在def前写上@measure()?不可以! 我们会得到一个错误:

measure() missing 1 required positional argument: 'func'

但是,在app.route()的例子中,我们确实写了()的括号。
很简单:@decorator def func被func = decorator(func)所取代。
如果我们写@decorator() def func,它就会被func = decorator()(func)取代。
所以在后一种情况下,decorator()被运行,它需要返回一个接受一个函数作为参数的函数,并返回一个函数。

这就是所有装饰器接受一个参数的例子的结构方式。