详解Python装饰器

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。

[巴西]Luciano Ramalho [译]安道 吴珂Fluent Python

闭包

在了解装饰器之前先讲一下闭包,闭包是一种特殊的函数,这种函数由多个函数的嵌套组成,且称之为外函数和内函数,外函数返回值是内函数的引用,此时就构成了闭包。构建一个闭包有以下几点要求: 1. 多个函数的嵌套 2. 必须返回一个函数对象 3. 返回的那个函数必须引用外部变量 闭包的优点: - 闭包可以保存运行环境,即在闭包内的变量是不能被轻易修改的; - 闭包的好处:提高代码的可复用性。 举个栗子:

def out_func(n):
    def in_func(num):
        return n*num
    return in_func

demo = out_func(3)
res = demo(4)
print(res)

装饰器

装饰器也是一种闭包,只不过它的参数是被装饰的函数。举个栗子,定义一个装饰器,计算函数的执行时间:

import time
# 定义装饰器
def time_calc(func):
    def wrapper(*args, **kargs):        
        start_time = time.time()
        
        f = func(*args,**kargs) 
        
        exec_time = time.time() - start_time
        return f    
    return wrapper   
    
# 使用装饰器
@time_calc    
def add(a, b):
    return a + b
    
@time_calc
def sub(a, b):    
    return a - b

上述的装饰器与add = time_calc(add)含义相同。但是这样的装饰器仍有缺陷,执行print(add.__name__)语句会发现输出的并不是add,而是wrapper,是因为这个函数重写了add的名字和注释文档。幸运的是,Python中functools.wraps函数可以解决这个问题。

from functools import wraps
def time_calc(func):
    @wraps(func)
    def wrapper(*args, **kargs):
        start_time = time.time()
        f = func(*args,**kargs)
        exec_time = time.time() - start_time
        print('RunTime',exec_time)
        return f
    return wrapper   

@time_calc    
def add(a, b):
    return a + b

而上面的例子除了可以用函数来定义,也可以用类来定义。

from functools import wraps
class time_calc(object):
    def __call__(self,func):
        @wraps(func)
        def wrapper(*args, **kargs):
            start_time = time.time()
            f = func(*args,**kargs)
            exec_time = time.time() - start_time
            print('RunTime',exec_time)
            return f
        return wrapper 
    def notify(self):
        pass

@time_calc()
def add(a, b):
    return a + b
再来个与它类似的,找不同!
from functools import wraps
class time_calc(object):
    def __init__(self, func):
        self._func = func
    def __call__(self):
        @wraps(self._func)
        def wrapper(*args, **kargs):
            start_time = time.time()
            f = self._func(*args,**kargs)
            exec_time = time.time() - start_time
            print('RunTime',exec_time)
            return f
        return wrapper 
    def notify(self):
        pass

@time_calc
def add(a, b):
    return a + b

内置装饰器

常见的有三种:@property@staticmethod@classmethod

@property

把类内方法当成属性来使用,必须要有返回值,相当于getter;假如没有定义 @func.setter 修饰方法的话,就是只读属性

# 将 property 函数用作装饰器可以很方便的创建只读属性
# 下面的代码将 voltage() 方法转化成同名只读属性的 getter 方法。
class Parrot(object):
    def __init__(self):
        self._voltage = 100000
 
    @property
    def voltage(self):
        """Get the current voltage."""
        return self._voltage

# property 的 getter,setter 和 deleter 方法同样可以用作装饰器
# 下面两个例子是完全相同的
class C(object):
    def __init__(self):
        self._x = None
    def getx(self):
        return self._x
    def setx(self, value):
        self._x = value
    def delx(self):
        del self._x
    x = property(getx, setx, delx, "I'm the 'x' property.")

class C(object):
    def __init__(self):
        self._x = None
    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x
    @x.setter
    def x(self, value):
        self._x = value
    @x.deleter
    def x(self):
        del self._x

@staicmethod

静态方法,不需要表示自身对象的self和自身类的cls参数,就跟使用函数一样。

@classmethod

类方法,不需要self参数,但第一个参数需要是表示自身类的cls参数。

class Demo(object):

    text = "三种方法的比较"
    
    def instance_method(self):
        print("调用实例方法")

    @classmethod
    def class_method(cls):
        print("调用类方法")
        print("在类方法中 访问类属性 text: {}".format(cls.text))
        print("在类方法中 调用实例方法 instance_method: {}".format(cls().instance_method()))

    @staticmethod
    def static_method():
        print("调用静态方法")
        print("在静态方法中 访问类属性 text: {}".format(Demo.text))
        print("在静态方法中 调用实例方法 instance_method: {}".format(Demo().instance_method()))

if __name__ == "__main__":
    # 实例化对象
    d = Demo()
    
    # 对象可以访问 实例方法、类方法、静态方法
    # 通过对象访问text属性
    print(d.text)
    
    # 通过对象调用实例方法
    d.instance_method()
    
    # 通过对象调用类方法
    d.class_method()
    
    # 通过对象调用静态方法
    d.static_method()
    
    # 类可以访问类方法、静态方法
    # 通过类访问text属性
    print(Demo.text)
    
    # 通过类调用类方法
    Demo.class_method()
    
    # 通过类调用静态方法
    Demo.static_method()

区别

在定义静态类方法和类方法时,@staticmethod 装饰的静态方法里面,想要访问类属性或调用实例方法,必须需要把类名写上; 而@classmethod装饰的类方法里面,会传一个cls参数,代表本类,这样就能够避免手写类名的硬编码。 在调用静态方法和类方法时,实际上写法都差不多,一般都是通过 类名.静态方法() 或 类名.类方法()。 也可以用实例化对象去调用静态方法和类方法,但为了和实例方法区分,最好还是用类去调用静态方法和类方法。

使用场景

假如不需要用到与类相关的属性或方法时,就用静态方法@staticmethod; 假如需要用到与类相关的属性或方法,然后又想表明这个方法是整个类通用的,而不是对象特异的,就可以使用类方法@classmethod。