在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()被运行,它需要返回一个接受一个函数作为参数的函数,并返回一个函数。
这就是所有装饰器接受一个参数的例子的结构方式。