深度解析Python内置装饰器
引入
还记得初学 C++ 或者 Java 时关于面向对象的三大思想吗?封装,继承,多态。你会发现,C++ 和 Java 都有相应的语法层面的机制来实现这三大思想,但是 Python 好像没有虚类(抽象类)吗?C++ 的静态函数好像 Python 中也无法实现吗?
别急,Python 提供的内置装饰器可以帮助你实现上面的特性,而且还带有一些神奇的效果。
装饰器
- property
- staticmethod
- classmethod
- abstractmethod (所属模块 abc)
- wraps (所属模块functools)
property
porperty 用于装饰类函数,被装饰的类函数不可在类被实例化后被调用,只能通过访问与函数同名的属性进行调用。
class Test:
num = 1
@property
def num_val(self):
return self.num
test = Test()
print(test.num_val)
>>> 1
我们不妨解构这个类,来看看num_val到底是什么:
for i in dir(test):
if not i.startswith("__"):
print(i, type(getattr(test, 1)))
>>> num <class 'int'>
>>> nul_val <class 'int'>
可以看到,装饰过的 num_val 确实变成了一个属性。但是它与普通属性有很大的区别,就是它没有定义 set 方法,也就是它不能被赋值,这是一个只读变量:
test.num_val = 1
>>> Traceback (most recent call last):
File "e:\python draft\test.py", line 9, in <module>
test.num_val = 1
AttributeError: can't set attribute
利用这个特性,我们可以实现对 Python 类成员的安全访问,还记得 Python 的私有成员怎么写的吗?通过双下划线前缀可以将一个类属性定义为私有属性。我们利用这些特性就可以实现下述的例子:
class Test:
__number = 1
@property
def number(self):
return self.__number
test = Test()
try:
print(test.__numer)
except:
print("访问 私有成员 失败")
try:
print(test.number)
print("访问 类属性 成功")
except:
pass
try:
test.number = 1
except:
print("修改 类属性 失败")
>>> 访问 私有成员 失败
>>> 1
>>> 访问 类属性 成功
>>> 修改 类属性 失败
staticmethod
被这个装饰器过类函数就会被声明成一个函数的静态函数。静态函数不需要类实例化就能直接调用。被装饰的函数不需要代表对象实例的 self 参数。
class Test:
@staticmethod
def add(x, y):
return x + y
@staticmethod
def minus(x, y):
return x - y
print(Test.add(1, 1))
print(test.minus(1, 2))
>>> 2
>>> -1
在staticmethod下,你不仅可以实现曾经 C++,Java 中熟悉的 static ,还可以直接实现 Java 中的接口机制或者 C++ 中命名空间机制,比如这样:
class Game:
win = 0
loss = 0
@staticmethod
def I_win():
Game.win += 1
@staticmethod
def I_loss():
Game.loss += 1
Game.I_win()
Game.I_lose()
Game.I_win()
Game.I_win()
Game.I_lose()
print(Game.win, Game.loss)
>>> 3 2
直接定义在 Python 类中的属性为静态变量,比如上面的 win 和 loss
如果没有 staticmehod 装饰器,我们也能实现上面的效果:
- 创建文件名为 Game.py
- 定义各个变量和函数
- 在入口文件中引入 Game.py
- 剩余操作完全一样
但是很显然,这样做比较麻烦,而且如果我们单个接口类实现成本低,那么就会创建若干 Python 文件,在运行项目时,还增加了读入这些文件的 IO 成本,使用 staticmethod 装饰器无疑更加灵活。
classmethod
这个装饰器很有意思,它与 staticmethod 的使用效果非常像,被装饰的类函数也是可以在没有被实例下直接定义的,只不过被 clasmethod 装饰的函数必须要有一个 cls 参数,代表类本身。来看一个实例:
class Test:
n = 1
def __init__(self, a) -> None:
self.a = a
@classmethod
def add_a(cls):
print(cls)
print(cls == Test)
print(cls.n)
Test.add_a()
由于被 classmethod 装饰的函数强制暴露了类自身,所以我们可以通过被 classmethod 装饰的函数对类的静态变量进行一定的操作(staticmethod 中也可以)
与staticmethod 不同的是,classmethod 更多是关乎这个类的静态变量的操作,而 staticmethod 则是与实例无关但与类封装功能有关。
结合 Python 的多态(比如鸭子类型),使用 classmethod 使得在 python 中开发抽象程序高于 class 的实体成为了可能。在许多 Python 内置库的原语实现代码中,经常能看到 classmethod 的影子。
abs.abstractmethod
初学 Python 时,大家肯定很疑惑:为啥 Python 没有抽象类机制呢?Python 中的虚函数怎么定义呀?
带着这些疑惑,恭喜你,今年迎来解答:为了补充 Python 原语无法实现抽象类的问题,Python提供了内置库 abc 来提供抽象类的机制,如果读者喜欢翻一些 Python 库的源代码,那么你应该经常会看到 abc 这个库。
abc 是 abstract base class 的缩写
由于本文只是讲内置装饰器的,所以不谈抽象类的实现,后面会有文章具体谈到。
abc模块提供了几个装饰器用于将 Python 父类需要实现的方法申明为虚函数,比如 abstractmethod ,它所在的类必须被申明为抽象类之后才能生效。
[abc -- 抽象基类]https://docs.python.org/zh-cn/3/library/abc.html
简单看一个例子:
from abc import abstractmethod, ABC
class Animal(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def get_home(self):
pass
class Dog(Animal):
...
dog = Dog("bob")
dog.get_home()
>>>
Traceback (most recent call last):
File "e:\python draft\test.py" line 14, in <module>
dog = Dog("bob")
TypeError: Can't instantiate abstract class Dog with abstract methods get_home
abstractmethod 将父类中的 get_home 声明为一个虚函数,由于子类中未实现虚函数,所以报错,符合预期。
只需要在Dog类中实现父类的所有虚函数就不会报错:
class Dog(Animal):
def get_home(self):
print("陆地")
>>>
陆地
同样的,之前谈到的所有原生装饰器,都有抽象版本的,比如:
- property -> abstractproperty
- staticmethod -> abstractstaticmethod
- classmethod -> abstractclassmethod
但是在Python3.3 之后,这三个额为的装饰器就可以丢弃了,因为@property 等原生装饰器可以在 @abstractmethod 上面堆叠,组成更为复杂的装饰效果。
不过,堆叠的时候一定要记住 @abstractmethod 在最下面
functools.wraps
在讲这个之前,我们先回顾一下装饰器的基本使用,比如我们现在完成一个装饰器,被它装饰的函数会执行之前打印函数的名称:
def printname(func):
def inner(*args, **kwargs):
print("execute", func.__name__)
return func(*args, **kwargs)
return inner
@printname
def add(x, y):
return x + y
print(add(1, 1))
>>>
execute add
2
嗯,看似没有问题,但是如果你想访问add本身,你就会发现问题了
print(add.__name__)
>>>
inner
我们发现,装饰器的行为污染了被装饰器函数的成员变量,这是我们不希望看到的,那么有什么方法可以解决这个问题?答案就是使用functools 中 wraps 来包装传入装饰器的函数,我们只需要如此修改:
from functools import wraps
def printname(func):
@wraps(func)
def inner(*args, **kwargs):
print("execute", func.__name__)
return func(*args, **kwargs)
return inner
@printname
def add(x, y):
return x + y
print(add.__name__)
>>>
add