跳到主要内容

Python装饰器:将装饰器定义为类

最近在强化补齐 Python 的基础知识,向高阶玩家进发。主攻《Python CookBook 3rd》,上面的知识点比较多,我挑重点看这几个方面。

  • yield + 协程
  • 装饰器
  • 多线程
  • 面向对象

本篇博客讨论的主题是 装饰器 ,装饰器其实也不是什么新鲜的用法,就是传入的参数是 函数 ,这一点上,它更像是一个语法糖。装饰器的作用是可以给我们原本编写好的函数再一次的加上额外的功能(比如统计时间,打日志)。

装饰器可以给我们的函数加上武器,使它们更加强大。如果是利用 Python 进行 Web 开发的小伙伴对装饰器并不陌生,在 FlaskDjango@ 符号应该不少见了

用类来实现装饰器

Python 对某个对象是否能通过装饰器(@decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象。类也可以通过实现 __call__ 方法,变得和函数一样可调用

class Foo:
def __call__(self):
print('__call__ has been called')

foo = Foo()
# OUTPUT: __call__ has been called
foo()
# OUTPUT: True
print(callable(foo))

这样的话,我们就可以直接开始用类来实现装饰器了

class Profiled:
def __init__(self, func):
self.func = func
self.ncalls = 0

def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.func(*args, **kwargs)

@Profiled
def add(x, y):
return x + y
# 3
add(1,2)
# 7
add(3,4)
# OUTPUT:2 总共调用了2次装饰器
print(add.ncalls)

问题一:装饰器无法装饰类内的方法

这时候出现了第一个问题,当装饰器尝试装饰类内的方法的时候,我们常常会出现 参数输入不正确的提示

class Spam:
@Profiled
def bar(self, x):
print(self, x)

s = Spam()
# TypeError: bar() missing 1 required positional argument: 'x'
s.bar(1)

这个问题会让很多人感到困惑,明明我已经输入了参数x,赋值为1,但是却提示没有收到函数。我当时困惑了很久。终于发现了一个我们不容易注意到的细节:函数和方法的功能相似,当时实现方式不同

我的理解是:**与实例绑定的函数,叫做过程。不与实例绑定的叫做函数。**这里的实例,在代码里面就是 self 类内的方法,需要多传一个 self ,它本质上可以看做是一个指向实例的指针。

# 我们在 __call__ 里面多加一行,看看输出会是什么
def __call__(self, *args, **kwargs):
self.ncalls += 1
+ print(self, *args, **kwargs)
return self.func(*args, **kwargs)
# <__main__.Profiled object at 0x10faf1128> 1

self 就是 <__main__.Profiled object at 0x10faf1128> ,而输入的参数是 1

{% blockquote %} Self 到低是一个什么玩意? {% endblockquote %}

为了探寻 self 的本质,我又多做了一个实验。 发现 self Spam.bars.bar 都是一个东西,相互替换的话,也是OK的,它就是类的一个实例。需要注意的是@Profiled 等价于 bar = Profiled(bar) ,所以这个实例也就是 Spam.bar

self
<__main__.Profiled object at 0x10aaf5470>
Spam.bar
<__main__.Profiled object at 0x10aaf5470>
s.bar
<__main__.Profiled object at 0x10aaf5470>

下面的例子,可以帮助我们更好的理解 对象的方法类的方法

# 带 self 的意思是对象的方法,所以我们必须传入一个对象
class Spam:
def bar(self, x):
print(self, x)

# 正常用法,实例化对象,然后调用对象的方法
# OUTPUT: <__main__.Spam object at 0x10b3470b8> 111
s = Spam()
s.bar(111)

# 直接使用 Spam.bar 方法
Spam.bar(123) # 错误用法,因为这里当做类的方法去使用了
# OUTPUT: <__main__.Spam object at 0x10b3470b8> 123
Spam.bar(Spam(),123) # 正确用法

更进一步,我们发现,定义类的方法的时候,类并不关心你传入的实例到底是什么,可以是 self ,也可以是任何类型的实例。

# <__main__.Spam object at 0x107bcc128> 123
Spam.bar(Spam(),123)
# <__main__.Spam object at 0x107bc2e80> 123
Spam.bar(s,123)
# <function Spam.bar at 0x107badea0> 123
Spam.bar(Spam.bar,123)
# 任何类型的对象都可以 123
Spam.bar("任何类型的对象都可以",123)

解决方案

回到正题,我们终于找到了原先错误的原因,就是漏掉了 self

class Profiled:
def __init__(self, func):
self.func = func
self.ncalls = 0

def __call__(self, *args, **kwargs):
self.ncalls += 1
- return self.func(*args, **kwargs)
+ return self.func(self, *args, **kwargs)

class Spam:
@Profiled
def bar(self, x):
print(self, x)

s = Spam()
s.bar(1)
s.bar(2)
print(Spam.bar.ncalls)
# OUTPUT
<__main__.Profiled object at 0x104168eb8> 1
<__main__.Profiled object at 0x104168eb8> 2
2

解决完这个问题,附带的我们对 self 的理解就加深了一层。

类方法 和 实例方法

class Spam:
"""实例方法"""
def bar(self, x):
print(self, x)

class Swam:
"""类方法"""
@classmethod
def bar(cls, x):
print(cls, x)
# OUTPUT: <__main__.Spam object at 0x1102080f0> 1
Spam().bar(1)
# OUTPUT: <class '__main__.Swam'> 1
Swam.bar(1)

问题二:如何兼顾类内类外的函数

刚刚的问题一解决后,我们发现,对原来类外的函数又失效了,原因是它并没有 self 这个参数。为了解决这个问题,我们需要花一番功夫,其实简单来说,有实例的情况下,我们需要填充实例到 self 参数的位置。

这里我们借助 types.MethodTpye__get__

解决方法(完美版)

import types
from functools import wraps

class Profiled:
def __init__(self, func):
self.func = func
self.ncalls = 0

def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.func(*args, **kwargs)

def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)

@Profiled
def add(x, y):
return x + y

class Spam:
@Profiled
def bar(self, x):
print(self, x)
add(1,2)
add(3,4)
print(add.ncalls)

s = Spam()
s.bar(1)
s.bar(2)
print(Spam.bar.ncalls)

__get__ 是怎么使用

当一个类中实现了任意的 __get__() __set__()__delete__() 三个特殊的方法后, 这个类就是一个描述器类。当这个描述器在另一个类中被调用的时候,就会调用以上的三个方法。

# 定义一个描述器类
class Integer:
def __init__(self, name):
self.name = name

def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]

def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
instance.__dict__[self.name] = value

def __delete__(self, instance):
del instance.__dict__[self.name]

为了使用一个描述器,这个类必须作为另外一个类的属性

# x, y 都是 Point 的描述器属性
# 需要注意的是: x, y 是类的属性,需要在方法前定义
class Point:
x = Integer('x')
y = Integer('y')

def __init__(self, x, y):
self.x = x
self.y = y

# 下面的定义是错误的
class Point:
def __init__(self, x, y):
self.x = Integer('x') # No! Must be a class variable
self.y = Integer('y')
self.x = x
self.y = y

使用方法如下

>>> p = Point(2, 3)
>>> p.x # Calls Point.x.__get__(p,Point)
2
>>> p.y = 5 # Calls Point.y.__set__(p, 5)

我们会发现 __get__ 方法实现起来比较复杂

def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]

self, instance, cls 分别代表什么意思?

self 是 Integer 类的实例,这里 x、y 都可以看做是self。 instance 是 Point 类的实例,也就是 p。 cls(也可以写成 owner)是类本身,这里就是 Point 类。

如果一个描述器被当做一个类变量来访问,那么 instance 参数被设置成 None 。 这种情况下,标准做法就是简单的返回这个描述器本身即可(尽管你还可以添加其他的自定义操作)。

>>> p.x # Calls Point.x.__get__(p, Point)
2
>>> Point.x # Calls Point.x.__get__(None, Point)
<__main__.Integer object at 0x100671890>

types.MethodType 详解

Python 是动态的编程语言,可以在执行的过程中,给类动态的添加方法。

下面举一个经典用法

import types  

def fn_get_name(self):
return self.name

class Person(object):
def __init__(self, name, score):
self.name = name
self.score = score

添加函数到 Person 类的方法上。

>>> p1 = Person('Bob', 90)  
>>> p1.get_name = types.MethodType(fn_get_name, p1)
>>> print p1.get_name()
Bob
>>> p2 = Person('Alice', 65)
>>> print p2.get_name()
# ERROR: AttributeError: 'Person' object has no attribute 'get_name'
# 因p2实例没有绑定get_name方法,所以出现错误。

下面我们深入理解一下 p1.get_name = types.MethodType(fn_get_name, p1) types.MethodType 接受两个参数(好像Python2 是3 个参数?)

  • 第一个参数:绑定的函数
  • 第二个参数:需要绑定的实例

孤立的看两个参数和实现的结果并不能很好的理解其中的原理,实际上,所谓的绑定的实质上是:第二个实例是作为参数,传入到绑定函数(现在是类的过程)的 self 中去

如果理解了这一句话,那么应该不难理解下面的程序。明明是 p1 调用自己的方法,最后输出的是 p2 的结果

p1 = Person('Bob', 90)  
p2 = Person('Tom', 0)
p1.get_name = types.MethodType(fn_get_name, p2)
print (p1.get_name())
# OUTPUT: Tom

原因是我们的类最终是变成了下面的样子

import types  

def fn_get_name(self):
return self.name

class Person(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_name(self):
self = p2
return self.name
p1 = Person('Bob', 90)
p2 = Person('Tom', 0)
print (p1.get_name())
# OUTPUT: TOM

回顾

有了上面的各种铺垫后,我们再一次的回顾 __get__, __call__ 两个方法,就瞬间了然。

 def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance) # 该实例会作为参数传入 self 中
def __call__(self, *args, **kwargs):
# 当实例发生调用的时候,__call__(self, self, 1)
# args = (self, 1)
self.ncalls += 1
return self.func(*args, **kwargs)
# self.func(self, 1)

利用 wrapper 再次包装一下

wrapper 的作用非常简单,就是让复制被包装函数的元信息。下面的代码就是 Python Cookbook 给出的答案。

import types
from functools import wraps

class Profiled:
def __init__(self, func):
- self.func = func
+ wraps(func)(self)
self.ncalls = 0

def __call__(self, *args, **kwargs):
self.ncalls += 1
- return self.func(*args, **kwargs)
+ return self.__wrapped__(*args, **kwargs)

def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)

小结

花了相当长的一个篇幅去讨论 Python Cookbook 的一个章节,虽然有点小题大做。不过也是把很多不懂的地方理清楚了。特别是对于 函数和方法 的认识更深一步了。

后续会利用装饰器去做一个应用,计划是做一个 requests 包的一个封装,使得 request 请求的时候自动加上代理,把它封装成一个 Util 工具。

参考资料

Python 工匠:使用装饰器的技巧

python中函数和方法区别,以及如何给python类动态绑定方法和属性(涉及types.MethodType()和__slots__)

Python Cookbook 8.9 创建新的类或实例属性

Python Cookbook 9.9 将装饰器定义为类