python-装饰器详解
1. 什么是装饰器
首先要说明的是,装饰器本身是一个函数
,它接收其他函数作为参数,并将其以一个新的修改后的函数进行替换.
在汉语词典中,对装饰的解释是在身体或物体的表面加些附属的东西,使美观.在 python 中,装饰器顾名思义,就是为 python 中的函数附加一些附属的东西,比如说插入日志,性能测试,参数检查,权限校验等等.这些附属的东西由装饰器进行实现,而 Python 中的其他函数只关心它自身的核心逻辑就好, 不会被装饰器所影响.
2. 创建一个装饰器
首先我们实现一个最简单的装饰器,它什么都不做,只返回原函数.
# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 21:44:52
author : sk
"""
def identity(f):
return f
@identity
def foo():
return "hello world"
if __name__ == "__main__":
print(foo())
在这里,我们定义了一个函数 foo()
, 它很简单,返回伟大的 “hello world” 字符串.然后我们定义了一个装饰器identity()
, 它也很简单,接受一个函数,然后返回这个函数.注意,装饰器接受的是一个函数
. @identity
这个东西,是 python 给到我们的语法糖,让我们便于书写.如果将这个语法糖翻译成我们习惯的样子,会是如下的形式.
# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 21:44:52
author : sk
"""
def identity(f):
return f
def foo():
return "hello world"
if __name__ == "__main__":
foo = identity(foo)
print(foo())
注意,我们把 identity()
这个东东转换成了代码 foo = identity(foo)
,很好理解,就是将 foo
函数作为参数传递给装饰器(就是一个函数)identity
,然后再指向foo
,相当于装饰器identity
将函数 foo
包装了一层,但是 foo
函数的核心逻辑不变,仍然返回伟大的 “hello world”.
3. 装饰器示例-参数检查
在第一节中,我们提到了装饰器的一些功能,比如插入日志,性能测试,参数检查,权限校验等等.现在,我们以参数检查为例,说明一下装饰器的作用.
考虑一个场景,你往银行卡里面取钱和存钱.一般的逻辑是,首先检查你输入的账号和密码是否正确,正确之后,才能进行取钱和存钱的操作.转换成 python 代码如下.
# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 22:25:01
author : sk
"""
class Store(object):
def __init__(self):
self.your_money = 10000
def get_money(self, user, passwd, money):
if user != "your-name" and passwd != "your-passwd":
raise Exception("账号密码不对,无法取钱")
self.your_money -= money
return money
def put_money(self, user, passwd, money):
if user != "your-name" and passwd != "your-passwd":
raise Exception("账号密码不对,无法取钱")
self.your_money += money
return "Success"
上面的代码中,我们创建了一个类 Store
.如果你实例化一个 Store
(即 store = Store()
),会创建一个你的银行卡,里面有 10000 块钱.然后你取钱和存钱的时候,有两行检测参数的代码.这样看的话,代码很冗余拖沓,一点也不美观.我们如何进行优化呢?
首先,很容易想到,我们将这两行代码抽离出来,做成一个通用的检测参数的逻辑函数,这样,会美观一点.
# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 22:25:01
author : sk
"""
def check_user(user, passwd):
if user != "your-name" and passwd != "your-passwd":
raise Exception("账号密码不对,无法取钱")
class Store(object):
def __init__(self):
self.your_money = 10000
def get_money(self, user, passwd, money):
check_user(user, passwd)
self.your_money -= money
return money
def put_money(self, user, passwd, money):
check_user(user, passwd)
self.your_money += money
return "Success"
这样看起来美观一点了.
如果让它更简洁美观呢?
我们可以让装饰器同学登场了.
# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 22:25:01
author : sk
"""
def check_user(f):
def wrapper(*args, **kwargs):
if kwargs.get("user") != "your-name" and kwargs.get("passwd") != "your-passwd":
raise Exception("账号密码不对,无法取钱")
return f(*args, **kwargs)
return wrapper
class Store(object):
def __init__(self):
self.your_money = 10000
@check_user
def get_money(self, user, passwd, money):
self.your_money -= money
return money
@check_user
def put_money(self, user, passwd, money):
self.your_money += money
return "Success"
if __name__ == "__main__":
store = Store()
# 下面两行会 raise Exception
# money = store.get_money(user="jia-de", passwd="jia-de", money=1000)
# print(money)
# 下面两行会返回 1000
money = store.get_money(user="your-name", passwd="your-passwd", money=1000)
print(money)
这样看起来代码整洁规范了很多.
有两点需要说明一下.
- 装饰器里面又包裹了一个函数.这个是叫做
函数柯里化
.懵逼中… - *args, **kwargs 是python里面的传递参数的两种方式. *args 用来将参数打包成tuple给函数体调用. **kwargs 打包关键字参数成dict给函数体调用.
4. 上述装饰器的缺陷以及改进
4.1 缺陷
正如前面所提到的,装饰器会用一个动态创建的新函数替换原来的。然而,新函数缺少很多原函数的属性,如 docstring 和 名字。
"""
create time : 2018-11-08 21:44:52
author : sk
"""
def identity(f):
print("hello identity")
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
@identity
def foo(name="python"):
""" docstring test
this is a test string
"""
return name
if __name__ == "__main__":
print(foo.__doc__)
print(foo.__name__)
上述代码中,返回的结果是
hello identity
None
wrapper
4.2 改进措施
幸好,python 内置的 functools
模块通过装饰器 wraps
解决了这个问题。它会复制这些属性给到这个包装器本身。
"""
create time : 2018-11-08 21:44:52
author : sk
"""
import functools
def identity(f):
print("hello identity")
@functools.wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
@identity
def foo(name="python"):
""" docstring test
this is a test string
"""
return name
if __name__ == "__main__":
print(foo.__doc__)
print(foo.__name__)
此时,程序的输出是我们预想的那样。
hello identity
docstring test
this is a test string
foo
- ps:
functools
还提供了一个update_wrapper
的函数来解决这个问题,但是我这边验证下来有错误。可能是俺调用的姿势不对。
5. 装饰器类
我们也可以将装饰器做成一个类的形式。如下所示。
"""
create time : 2018-11-08 21:44:52
author : sk
"""
import functools
class Identitf(object):
def __init__(self):
pass
def __call__(self, f):
print("hello identity")
@functools.wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
@Identitf()
def foo(name="python"):
""" docstring test
this is a test string
"""
return name
if __name__ == "__main__":
print(foo.__doc__)
print(foo.__name__)
使用类来实现一个装饰器的好处是 : 可以扩展功能
- 我们可以在类里面实现各种方法,来扩展装饰器的功能。
- 我们可以实现一个子类来继承父类,从而扩展装饰器的功能。
6. python 中的一些装饰器
6.1 静态方法 @ staticmethod
静态方法是属于类的方法,但是实际上并非运行在类的实例
上。静态方法是指类中无需实例参与即可调用的方法(不需要self参数),在调用过程中,无需将类实例化,直接在类之后使用.号运算符调用方法。在一个类中实现静态方法需要用 @staticmethod
装饰器。
"""
create time : 2018-11-08 21:44:52
author : sk
"""
class MathTest(object):
@staticmethod
def add(x,y):
return x+y
def test(self, info):
return info
if __name__ == "__main__":
print(MathTest.add(2,3))
print(MathTest().add(2,4))
print(MathTest().test("hello"))
print(MathTest.test("hello"))
输出的结果如下:
5
6
hello
Traceback (most recent call last):
... ...
TypeError: test() missing 1 required positional argument: 'info'
前面三个结果正常输入。而最后一个 print , 由于没有实例化对象,所以报错。而 add
是静态方法,所以第二个 print 没有报错。
6.2 类方法 @classmethod
类方法是直接绑定到类而非它的实例的方法。类方法在Python中使用比较少,类方法传入的第一个参数为cls
,是类本身。并且,类方法可以通过类直接调用,或通过实例直接调用。但无论哪种调用方式,最左侧传入的参数一定是类本身。
"""
create time : 2018-11-08 21:44:52
author : sk
"""
class MathTest(object):
@classmethod
def add(cls, x,y):
return x+y
def test(self, info):
return info
if __name__ == "__main__":
print(MathTest.add)
print(MathTest().add)
print(MathTest.add(2,3))
返回的结果是
<bound method MathTest.add of <class '__main__.MathTest'>>
<bound method MathTest.add of <class '__main__.MathTest'>>
5
6.3 抽象方法 @abc.abstractmethod
抽象方法是定义在基类中可能有或没有实现的方法
class Pizza(object):
@staticmethod
def test():
raise NotImplementedError
也可以使用 abc
模块来实现抽象方法。
import abc
class Pizza(object):
@abc.abstractmethod
def test():
pass