2022-02-21 09:36:51

Python方法和装饰器

Python方法和装饰器
[TOC]

前言

装饰器真的很重要,再怎么强调都不为过。

装饰器

装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。
关键就是修改这块,可以做一些通用处理,以扩大原函数的功能。感觉有点类似java中的切片。

装饰是为函数和类指定管理代码的一种方式。装饰器本身的形式是处理其他的可调用对象的可调用对象(如函数)。
装饰器提供了一种方法,在函数和类定义语句的末尾插入自动运行代码。

通过针对随后的调用安装包装器对象可以实现:

  1. 函数装饰器安装包装器对象,以在需要的时候拦截随后的函数调用并处理它们。
  2. 类装饰器安装包装器对象,以在需要的时候拦截随后的实例创建调用并处理它们。

为什么使用装饰器?

  1. 装饰器有一种非常明确的语法,这使得它们比那些可能任意地远离主体函数或类的辅助函数调用更容易为人们发现。
  2. 当主体函数或类定义的时候,装饰器应用一次;在对类或函数的每次调用的时候,不必添加额外的代码。
  3. 由于前面两点,装饰器使得一个API的用户不太可能忘记根据API需要扩展一个函数或类。

装饰器本质

函数装饰器是一种关于函数的运行时声明,函数的定义需要遵守此声明。
装饰器在紧挨着定义一个函数或方法的def语句之前的一行编写,并且它由@符号以及紧随其后的对于元函数的一个引用组成–这是管理另一个函数的一个函数。

在编码方面,函数装饰器自动将如下的语法:

@decorator #Decorate function def F(arg): ... F(99) #调用函数

映射为这一对等的形式,其中装饰器是一个单参数的可调用对象,它返回与F具有相同数目的参数的一个可调用对象:

def F(arg): ... F = decorator(F) #rebind function name to decorator result F(99) # Essentially calls decorator(F)(99)

这一自动名称重绑定在def语句上有效,不管它针对一个简单的函数或是类中的一个方法。当随后调用F函数的时候,它自动调用装饰器所返回的对象,该对象可能是实现了所需的包装逻辑的另一个对象,或者是最初的函数本身。

装饰器自身是一个返回可调用对象的可调用对象

有一种常用的编码模式–装饰器返回了一个包装器,包装器把最初的函数保持到一个封闭的作用域中:

def decorator(F): def wrapper(*args, **kwargs): #使用F和参数做一些扩展功能,如权限判断等 #调用原函数 return wrapper @decorator def func(x,y): ... func(6,7)

当随后调用名称func的时候,它硬实调用装饰器所返回的包装器函数;随后包装器函数可能会运行最初的func,因为它在一个封闭的作用域中仍然可以使用。当以这种方式编码的时候,每个装饰器的函数都会产生一个新的作用域来保持状态。

functools和inspect

但这样因为改了函数签名等,所以有了functools提供一个wraps装饰器来帮忙保持原函数的函数名和文档字符串。
看看源代码:

WRAPPER_ASSIGNMENTS = ('__module__','__name__','__qualname__','__doc__','__annotations__') WRAPPER_UPDATES = ('__dict__') def update_wrapper(wrapper, wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): wrapper.__wrapped__ = wrapped for attr in assigned: try: value = getattr(wrapped,attr) except AttributeError: pass else: setattr(wrapper, attr, value) for attr in updated: getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

使用@functools_wraps时实际就是用的这个函数。

inspect模块可以提取函数的签名,把位置参数和关键字参数统一成一个key/value的字典。
从而方便使用,而不必关心到底是位置参数还是关键字参数。

示例如下:

import functools import inspect def check_is_admin(f): @functools.wraps(f) def wrapper(*args, **kwargs): func_args = inspect.getcallargs(f,*args,**kwargs) if func_args.get('username') != 'admin': raise Exception("This user is not allowed to get food") return f(*args,**kwargs) return wrapper

使用类实现装饰器

我们也可以通过对类来重载__call__方法,从而把类转成一个可调用对象,并且使用实例属性而不是封闭的作用域:

class decorator: def __init__(self,func): self.func = func def __call__(self,*args): #使用self.func和args来做扩展功能 #self.func(*args)调用原来的函数 @decorator def func(x,y): ... func(6,7)

有一点需要注意,通过类实现的装饰器对象并不能工作在类方法上

因为:当一个方法名绑定只是绑定到一个简单的函数时,Python向self传递了隐含的主体实例;当它是一个可调用类的实例的时候,就传递这个类的实例。
从技术上讲,当方法是一个简单函数的时候,Python只是创建了一个绑定的方法对象,其中包含了主体实例。
反而是利用封闭作用域的嵌套函数工作的更好,既能支持简单函数,也能支持实例方法。

类装饰器

类装饰器和函数装饰器很类似,只不过管理的是类。
通过函数实现,返回了一个包装器类。

def decorator(cls): class Wrapper: def __init__(self, *args): self.wrapped = cls(*args) def __getattr__(self, name): return getattr(self.wrapped, name) return Wrapper @decorator class C: def __init__(self,x,y): self.attr = 'spam' x = C(6,7) print(x.attr)

每个被装饰的类都创建一个新的作用域,它记住了最初的类。

工厂函数通常在封闭的作用域引用中保持状态,类通常在属性中保持状态。

需要注意通过类实现的类装饰器,看如下的错误示例:

class Decorator:
    def __init__(self, C):
        self.C = C
    
    def __call__(self,*args):
        self.wrapped = self.C(*args)
        return self
    
    def __getattr__(self, attrname):
        return getattr(self.wrapped, attrname)

@Decorator
class C:... #class C实际变成了Decorator的一个实例,只是通过属性保留了原来的C。

x = C() #调用Decorator实例,其实就是调用__call__方法。
y = C()#同上

每个被装饰的类都返回了一个Decorator的实例。
但是对给定的类创建多个实例时出问题了—会对一个Decorator实例反复调用__call__方法,从而后面的的实例创建调用都覆盖了前面保存的实例。。(也许我们可以利用这个特性来实现单例模式??)

装饰器嵌套

为了支持多步骤的扩展,装饰器语法允许我们向一个装饰的函数或方法添加包装器逻辑的多个层。
这种形式的装饰器语法:

@A @B @C def f(...): ...

如下这样运行:

def f(...): ... f = A(B(C(f)))

类装饰器类似。。

装饰器参数

函数装饰器和类装饰器似乎都能接受参数,尽管实际上这些参数传递给了真正返回装饰器的一个可调用对象,而装饰器反过来又返回了一个可调用对象。例如,如下代码:

@decorator(A,B) def F(arg): ... F(99)

自动地映射到其对等的形式,其中装饰器是一个可调用对象,它返回实际的装饰器。返回的装饰器反过来返回可调用的对象,这个对象随后运行以调用最初的函数名:

def F(arg): ... F = decorator(A,B)(F) #Rebind F to result of decorator's return value F(99) #Essentially calls decorator(A,B)(F)(99)

装饰器参数在装饰发生之前就解析了,并且它们通常用来保持状态信息供随后的调用使用。
例如,这个例子中的装饰器函数,可能采用如下的形式:

def decorator(A,B): #save or use A,B def actualDecorator(F): #Save or use function F #Return a callable:nested def, class with __call__, etc. return callable return actualDecorator

换句话说,装饰器参数往往意味着可调用对象的3个层级:

  1. 接受装饰器参数的一个可调用对象,它返回一个可调用对象以作装饰器,
  2. 实际的装饰器,
  3. 该装饰器返回一个可调用对象来处理对最初的函数或类的调用。
    这3个层级的每一个都可能是一个函数或类,并且可能以作用域或类属性的形式保存了状态。

装饰器管理函数和类

装饰器不光可以管理随后对函数和类的调用,还能管理函数和类本身。如下所示,返回函数和类本身:

def decorator(o): #Save or augment function or class o return o @decorator def F():... #F=decorator(F) @decorator class C:... #C = decorator(C)

函数装饰器有几种办法来保持装饰的时候所提供的状态信息,以便在实际函数调用过程中使用:

  1. 实例属性。
class tracer: def __init__(self,func): self.calls = 0 self.func = func def __call__(self, *args, **kwargs): self.calls += 1 print('call %s to %s' % (self.calls, self.func.__name__)) @tracer def spam(a,b,c): print(a+b+c) @tracer def eggs(x,y): print(x ** y) spam(1,2,3) spam(a=4,b=5,c=6) eggs(2,16) eggs(4,y=4)
  1. 全局变量
calls = 0
def tracer(func):
    def wrapper(*args, **kwargs):
        global calls
        calls += 1
        print('call %s to %s' % (calls, func.__name__))
        return func(*args,**kwargs)
    return wrapper
    
@tracer
def spam(a,b,c):
    print(a+b+c)

spam(1,2,3)
  1. 非局部变量
def tracer(func): calls = 0 def wrapper(*args, **kwargs): nonlocal calls calls += 1 print('call %s to %s' % (calls, func.__name__)) return func(*args,**kwargs) return wrapper @tracer def spam(a,b,c): print(a+b+c) spam(1,2,3) spam(a=4,b=5,c=6)
  1. 函数属性
def tracer(func): def wrapper(*args, **kwargs): wrapper.calls += 1 print('call %s to %s' % (wrapper.calls, func.__name__)) return func(*args,**kwargs) wrapper.calls = 0 return wrapper

在运用描述符的情况下,我们也能把通过类实现的装饰器运用到 类方法上,只是有点复杂,如下所示:

class tracer(object): def __init__(self,func): self.calls = 0 self.func = func def __call__(self, *args, **kwargs): self.calls += 1 print('call %s to %s' % (self.calls, self.func.__name__)) return self.func(*args, **kwargs) def __get__(self,instance,owner): return wrapper(self, instance) class wrapper: def __init__(self, desc, subj): self.desc = desc self.subj = subj def __call__(self, *args, **kwargs): return self.desc(self.subj, *args, **kwargs) @tracer def spam(a,b,c): #spam = tracer(spam), 返回的tracer实例。当调用spam方法时,调用的就是tracer的__call__方法 ...same as prior... class Person: @tracer def giveRaise(self,percent): # giveRaise = tracer(giverRaise) ...same as prior...

经常装饰器后giveRaise变成了描述符对象。当person实际调用giveRaise的时候,当是获取giveRaise属性会触发描述符tracer的__get__调用。__get__返回了wrapper对象。而wrapper对象又保持了tracer实例和person实例。
当调用giveRaise(此时变成了wrapper对象)时,其实是调用的wrapper实例的__call__方法。wrapper实例的__call__方法又回调 tracer的__call__方法,利用wrapper保持的person实例把person实例当成参数也传了回去。
调用顺序如下:

person.giveRaise()->wrapper.__call__()->tracer.__call__()

这个例子中把wrapper类改成嵌套的函数也可以,而且代码量更少,如下:

class tracer(object): def __init__(self,func): self.calls = 0 self.func = func def __call__(self, *args, **kwargs): self.calls += 1 print('call %s to %s' % (self.calls, self.func.__name__)) return self.func(*args, **kwargs) def __get__(self,instance,owner): def wrapper(*args, **kwargs): return self(instance, *args, **kwargs) return wrapper

类装饰器(函数装饰器)的两个潜在缺陷:

  1. 类型修改。当插入包装器的时候,一个装饰器函数或类不会保持其最初的类型–其名称重新绑定到一个包装器对象,在使用对象名称或测试对象类型的程序中,这可能会很重要。
  2. 额外调用。通过装饰添加一个包装层,在每次调用装饰对象的时候,会引发一次额外调用所需要的额外性能成本–调用是相对耗费时间的操作。

Python中方法的运行机制

方法是作为类属性保存的函数。

看下面的例子。

>>> class Pizza(): ... def __init__(self,size): ... self.size = size ... def get_size(self): ... return self.size ... >>> Pizza.get_size <function Pizza.get_size at 0x10a108488>

在python2中是unbound method,而在python3只已经完全删除了未绑定方法这个概念,它会提示get_size是一个函数。

>>>Pizza.get_size() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: get_size() missing 1 required positional argument: 'self'

当调用的时候,都会报错。因为少了声明时的self参数。

你明确传一个参数是可以的。

>>> Pizza.get_size(Pizza(42)) 42

这个做法在__init__中是最常用的。调用父类的初始化方法。
实际上上述代码等同于下面的代码。

Pizza(42).get_size()

这次没有传参,是因为Python会把Pizza(42)这个对象自动传给get_size的self参数。
这也是Python2.x时有绑定方法的原因。因为方法和某个对象实例绑定起来了,即self参数会自动变成绑定的对象实例。

静态方法

静态方法是属于类的方法,但实际上并非运行在类的实例上。

class Pizza(object): @staticmethod def mix_ingredients(x,y): return x+y

装饰器@staticmethod提供了以下几种功能:

  • Python不必为我们创建的每个Pizza对象实例化一个绑定方法。
  • 提高代码的可读性。当看到@staticmethod时,就知道这个方法不依赖于对象的状态。
  • 可以在子类中覆盖静态方法。

类方法

类方法是直接绑定到类而非它的实例的方法:

class Pizza(object): radius = 42 @classmethod def get_radius(cls): return cls.radius

因为第一个参数要求的是类实例,所以Pizza.get_radius就成为绑定方法了。

抽象方法

最朴素的抽象方法其实是父类抛出异常,让子类去重写。
标准库里提供了abc,请参考Python标准库abc介绍

混合使用静态方法、类方法和抽象方法

从Python3开始,已经支持在@abstractmethod之上使用@staticmethod和@classmethod

不过在基类中声明为抽象方法为类方法并不会强迫其子类也将其定义为类方法。
将其定义为静态方法也一样,没有办法强迫子类将抽象方法实现为某种特定类型的方法。

另外,在抽象方法中是可以有实现代码的,并且子类可以通过super引用到父类的实现。

关于super的真相

python是支持多继承的。那么super到底是谁?

看下面代码:

>>> def parent(): ... return object ... >>> class A(parent()): ... pass ... >>> A.mro() [<class '__main__.A'>, <class 'object'>]

不出所料,可以正常运行:类A继承自父类object。类方法mro()返回方法解析顺序用于解析属性。

super()函数实际上是一个构造器,每次调用它都会实例化一个super对象。它接收一个或两个参数,第一个参数是一个类,第二个参数是一个子类或第一个参数的一个实例。

构造器返回的对象就像是第一个参数的父类的一个代理。它有自己的__getattribute__方法去遍历MRO列表中的类并返回第一个满足条件的属性:

>>> class A(object): ... bar = 42 ... def foo(self): ... pass ... >>> class B(object): ... bar = 0 ... >>> class C(A,B): ... xyz = 'abc' ... >>> C.mro() [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>] >>> super(C,C()).bar 42 >>> super(C,C()).foo <bound method A.foo of <__main__.C object at 0x10a10fbe0>> >>> super(B).__self__ >>> super(B,B()).__self__ <__main__.B object at 0x10a10fc18> >>>

其实super就是利用的MRO嘛。
在Python3中,super()变得更加神奇:可以在一个方法中不传入任何参数调用它。但没有参数传给super()时,它会为它们自动搜索栈框架:

class B(A): def foo(self): super().foo()

本文链接:http://blog.go2live.cn/post/python-Decorator.html

-- EOF --