这里讲一讲前面提到的python装饰器,@classmethod和@staticmethod是python内置装饰器,在了解什么是装饰器之前首先要了解函数的几个特征。
1. 有关函数的几个概念
1.1 函数可以接收另一个函数作为参数传入
高阶函数可以接收另一个函数作为传入的参数:
1 | def func1(a, b): |
从上面例子可以看到,在执行 func2函数的时候,函数对象func1作为参数被传入func2,返回func1(1, 2)的执行结果也就是3.
1.2 函数可以把另一个函数作为结果返回
高阶函数也可以将函数作为结果返回:
1 | # 高阶函数,把函数作为结果返回 |
上面的例子可以看到,外围函数 func1将内层函数 func2的引用赋值给a,此时a就有了内层函数func2 的方法,此时打印的a是函数的存储地址,执行a() 就可以执行func2 函数的功能。
1.3 嵌套函数可以引用外层函数的变量
稍稍修改1.2的例子,在外层函数添加局部变量msg:
1 | def func1(): # 外围函数 |
这里先引用闭包的概念:
闭包:指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。概念比较晦涩,简单来说就是嵌套函数引用了外层函数的变量。
这个例子和上个例子唯一的区别是,msg是一个在外围函数中的局部变量,在print_msg()函数执行之后应该就不会存在了。但是嵌套函数引用了这个变量,将这个局部变量封闭在了嵌套函数中,这样就形成了一个闭包。
有了以上关于高阶函数和闭包的概念后,就可以开始理解什么是装饰器以及装饰器的作用了。
2. 装饰器decorator
装饰器的本质就是一个闭包,把一个函数当做参数然后返回一个替代版函数(函数的引用)。
2.1 @标识符将装饰器应用到函数
下面将用代码方式简单演示装饰器是怎么应用的。
1 | def func1(func3): |
上面这个例子就是不用@标识符的装饰器,首先我定义了一个函数func1,它只有一个func3参数,这个函数里面定义了一个嵌套函数func2。func2的作用是调用func3前打印一串字符,然后执行被装饰的函数func3,结束之后再打印一串字符。
我们再定义一个测试函数funcx,功能是打印一段“函数正在运行”的字符串。
在1处,函数func1中传入函数funcx,返回函数func2的引用赋值给变量a,此时并没有执行函数,也不会有打印结果。在2处执行了func2函数,前面传入的函数funcx作为参数在原先的func3处执行,这个时候就会依次输出三行字符串。
将@标识符应用到函数上,只需要在函数定义前加上@和装饰器的名称即可。
1 | def func1(func3): |
这里@func1就是装饰器,它接受被装饰的函数作为参数传入,返回内部嵌套函数的引用(注意这个时候并没有执行函数),内部嵌套函数func2持有被装饰函数func3的引用。
可以看到@语法只是将函数传入装饰器函数,并不是什么特别难理解的概念,主要作用就是节省代码量(避免了再一次的赋值操作)。
2.2 带参数的装饰器
前面示范的是不带参数的装饰器,带参数的装饰器也是类似的,我们只要知道装饰器最终返回的一定是嵌套函数的引用。在前面的参数传递博文中,我们说过*args和**kargs可以以包裹传递的方式传递不定长参数,这里也是一样的。
1 | def func1(func3): |
上面的装饰器带的参数都是我们后面自定义函数里的参数,装饰器的语法允许我们在调用时提供其他参数。
1 | #import functools |
先不看导入的模块,后面再解释。
上面的例子看上去很复杂,可以一层一层剥开理解。func1是允许带参数的装饰器,实际上是原有装饰器decorator的再一次封装,并且返回了这个装饰器,可以理解为含有一个形参text的闭包。当我们使用@func1(text = “Phantom”)时,python解释器将我们的实参“Phantom”传入到装饰器的环境。
而嵌套函数func2在检查到传入的text参数与字符串“Phantom”相同时,就会执行后面的打印函数名、funcx传入的实参和func1传入到decorate的实参。
通过特殊属性__name__可以看到,funcx函数指向了装饰器内部定义的func2函数,也就是经过装饰器装饰后丢失了原函数的元信息,我们真正调用的是装饰后生成的新函数。那么是不是每次都要使用func2.__name__ = func.__name__
这样的代码来保留原函数信息呢?并不是,我们可以使用functools库中的@functools.wraps()来保留原函数的属性,其实这种保留只是将原始被装饰的函数的属性拷贝给了装饰函数,如果不干这件事,有些依赖函数签名的代码执行就会出错,感兴趣的小伙伴可以继续探究~
2.3 内置装饰器
上面说的@functools.wraps()其实也是内置装饰器,下面介绍其他几个常用的内置装饰器。
2.3.1 @property
这个内置装饰器用来装饰类函数,被装饰的类函数不可以在类被实例化后调用,只能通过访问与函数同名的属性进行调用(也就是把类的方法伪装成属性)。
1 | class A(): |
我们知道属性是可以被赋值的,但是经过property装饰的方法不可以像普通属性那样被赋值。
这个特性很有意思,我们可以实现对python类私有属性的安全访问(再次强调不存在严格意义的私有属性)。
1 | class A: |
2.3.2 @classmethod
直接翻译,这个装饰器就是用来定义类方法的,被装饰的函数必须有一个cls参数用来绑定类本身,隐式地将类作为对象,传递给方法,调用地时候不需要进行实例化。
如果不加这个装饰器,必须要使用self参数,隐式地将类实例传递给方法,也就是说必须要实例化。
强调一点,这里地cls和self只是为了方便编程的时候一眼看出来绑定的是类还是对象,都可以用别的xxx名字代替(但是不建议)。
1 | class A(): |
由于被classmethod装饰的函数强制暴露了类自身,所以我们可以通过被classmethod装饰的函数对类的静态变量进行一定操作,在实例化之前和类进行交互。还有类方法可以通过实例对象或者类对象去访问,所以有一个用途就是通过实例调用类方法实现对类属性的修改(点击见第三篇博客例子)。
2.3.3 @staticmethod
前面博客介绍过,这个装饰器是声明静态方法的,静态方法和上面的类方法一样,不需要实例化就可以直接调用,但是这个方法不强制要求传递参数,无法直接使用任何类变量、类方法或者实例方法、实例变量(这里要注意,只有主动传参才可以调用,因为主动传参是可以按照逻辑去找需要的参数的)。
1 | class People(): |
staticmethod更像是与实例无关但与类封装功能有关的函数,如果有一个功能实现的方法比较独立,可以考虑用静态方法来实现。
在继承类中,staticmethod和classmethod有以下区别
子类的实例继承了父类的@staticmethod静态方法,调用该方法,还是调用的父类的方法和类属性。
子类的实例继承了父类的@classmethod类方法,调用该方法,调用的是子类的方法和子类的类属性。
1 | class A(): |
上面这个例子可以看出来,@classmethod装饰后的func1函数实际上已经和父类没什么关系了,尽管在父类方法里但也可以当作是个独立的函数,不管子类的实例化还是父类的实例化都是调用同一个函数,输出结果一致。而@classmethod装饰后的func2函数,cls参数绑定了类本身,子类在实例化后继承了父类@classmethod类方法,但是调用的是子类的方法和类属性。
所有装饰器存在的意义都是为函数扩展功能,总结以下几点:
装饰器通过高级函数、嵌套函数和闭包实现
装饰器返回闭包函数的引用,这个闭包函数引用中有被装饰函数的引用
装饰器通过语法糖 @ 修饰
装饰器不修改原函数和调用方式(调用的是装饰后的新函数)