Python装饰器

Posted 素人渔芙2017

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python装饰器相关的知识,希望对你有一定的参考价值。

Python中的装饰器是你进入Python大门的一道坎,不管你跨不跨过去它都在那里。

为什么需要装饰器
我们假设你的程序实现了say_hello()和say_goodbye()两个函数。

def say_hello():
print "hello!"

def say_goodbye():
print "hello!" # bug here

if name == \'main\':
say_hello()
say_goodbye()
但是在实际调用中,我们发现程序出错了,上面的代码打印了两个hello。经过调试你发现是say_goodbye()出错了。老板要求调用每个方法前都要记录进入函数的名称,比如这样:

[DEBUG]: Enter say_hello()
Hello!
[DEBUG]: Enter say_goodbye()
Goodbye!
好,小A是个毕业生,他是这样实现的。

def say_hello():
print "[DEBUG]: enter say_hello()"
print "hello!"

def say_goodbye():
print "[DEBUG]: enter say_goodbye()"
print "hello!"

if name == \'main\':
say_hello()
say_goodbye()
很low吧? 嗯是的。小B工作有一段时间了,他告诉小A可以这样写。

def debug():
import inspect
caller_name = inspect.stack()[1][3]
print "[DEBUG]: enter {}()".format(caller_name)

def say_hello():
debug()
print "hello!"

def say_goodbye():
debug()
print "goodbye!"

if name == \'main\':
say_hello()
say_goodbye()
是不是好一点?那当然,但是每个业务函数里都要调用一下debug()函数,是不是很难受?万一老板说say相关的函数不用debug,do相关的才需要呢?

那么装饰器这时候应该登场了。

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能。

怎么写一个装饰器
在早些时候 (Python Version < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。

def debug(func):
def wrapper():
print "[DEBUG]: enter {}()".format(func.name)
return func()
return wrapper

def say_hello():
print "hello!"

say_hello = debug(say_hello) # 添加功能并保持原函数名不变
上面的debug函数其实已经是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。因为这样写实在不太优雅,在后面版本的Python中支持了@语法糖,下面代码等同于早期的写法。

def debug(func):
def wrapper():
print "[DEBUG]: enter {}()".format(func.name)
return func()
return wrapper

@debug
def say_hello():
print "hello!"
这是最简单的装饰器,但是有一个问题,如果被装饰的函数需要传入参数,那么这个装饰器就坏了。因为返回的函数并不能接受参数,你可以指定装饰器函数wrapper接受和原函数一样的参数,比如:

def debug(func):
def wrapper(something): # 指定一毛一样的参数
print "[DEBUG]: enter {}()".format(func.name)
return func(something)
return wrapper # 返回包装过函数

@debug
def say(something):
print "hello {}!".format(something)
这样你就解决了一个问题,但又多了N个问题。因为函数有千千万,你只管你自己的函数,别人的函数参数是什么样子,鬼知道?还好Python提供了可变参数*args和关键字参数**kwargs,有了这两个参数,装饰器就可以用于任意目标函数了。

def debug(func):
def wrapper(args, **kwargs): # 指定宇宙无敌参数
print "[DEBUG]: enter {}()".format(func.name)
print \'Prepare and say...\',
return func(
args, **kwargs)
return wrapper # 返回

@debug
def say(something):
print "hello {}!".format(something)
至此,你已完全掌握初级的装饰器写法。

高级一点的装饰器
带参数的装饰器和类装饰器属于进阶的内容。在理解这些装饰器之前,最好对函数的闭包和装饰器的接口约定有一定了解。(参见http://betacat.online/posts/python-closure/)

带参数的装饰器

假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么装饰器就会是这样的。

def logging(level):
def wrapper(func):
def inner_wrapper(args, **kwargs):
print "[{level}]: enter function {func}()".format(
level=level,
func=func.name)
return func(
args, **kwargs)
return inner_wrapper
return wrapper

@logging(level=\'INFO\')
def say(something):
print "say {}!".format(something)

如果没有使用@语法,等同于

say = logging(level=\'INFO\')(say)

@logging(level=\'DEBUG\')
def do(something):
print "do {}...".format(something)

if name == \'main\':
say(\'hello\')
do("my work")
是不是有一些晕?你可以这么理解,当带参数的装饰器被打在某个函数上时,比如@logging(level=\'DEBUG\'),它其实是一个函数,会马上被执行,只要这个它返回的结果是一个装饰器时,那就没问题。细细再体会一下。

\'\'\'
基于类实现的装饰器

装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

class Test():
def call(self):
print \'call me!\'

t = Test()
t() # call me
像__call__这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。

回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文)。那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果。
【不带参数的装饰器】
\'\'\'
class logging(object):
def init(self,func):
self.func = func

def __call__(self, *args, **kwargs):
    print (\'[DEBUG]:enter function {func}\'.format(func = self.func.__name__))
    return self.func(*args,**kwargs)

@logging
def say(something):
print (\'say %s\' % something)

say(\'abc\')
\'\'\'
【结果】
[DEBUG]:enter function say
say abc
\'\'\'

\'\'\'
【带参数的装饰器】
如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载__call__方法是就需要接受一个函数并返回一个函数。
\'\'\'
class logging(object):
def init(self,level=\'INFO\'):
self.level = level

def __call__(self, func): #接收函数
    def wrapper(*args,**kwargs):
        print (\'[%s]: enter function %s\' % (self.level,func.__name__))
        func(*args,**kwargs)
    return wrapper     #返回函数

@logging(level=\'DEBUG\')
def say(something):
print (\'say %s\' % something)
say(\'hello python\')
\'\'\'
【结果】
[DEBUG]: enter function say
say hello python
\'\'\'

【内置的装饰器】

内置的装饰器和普通装饰器原理是一样的,只不过返回的不是函数,而是类对象

@property

在了解这个装饰器前,你需要知道在不使用装饰器怎么写一个属性

class X(object):
def init(self,x):
self._x = x

def getx(self):
    return self._x

def setx(self, x):
    self._x = x

def delx(self):
    del self._x

p = property(getx,setx,delx,\'I am doc for property\')

xx = X(\'abc\')
print (xx.p)
\'\'\'
【结果】
abc
\'\'\'

def getx(self):
return self._x

def setx(self, x):
self._x = x

def delx(self):
del self._x
p = property(getx,setx,delx,\'I am doc for property\')
print (p)
print (property())
\'\'\'
【结果】
<property object at 0x102253098>
<property object at 0x1022530e8>
\'\'\'

\'\'\'

以上就是一个Python属性的标准写法,其实和Java挺像的,但是太罗嗦。有了@语法糖,能达到一样的效果但看起来更简单。
\'\'\'

@property
def getx(self):
return self._x
print (property())
\'\'\'
【结果】
<property object at 0x102253138>
\'\'\'

\'\'\'
【备注】属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setter和deleter是property()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,估计只是为了凑数,本身没有任何存在的意义。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。
\'\'\'

\'\'\'
【classmethod & staticmethod】
\'\'\'

\'\'\'
尽管classmethod和staticmethod非常的相似,但是两者在具体的使用上还是有着细微的差别:classmethod必须使用类对象作为第一个参数,而staticmethod则可以不传递任何参数。

让我们通过实际的例子来看看。

样板

让我们假设有处理日期信息的类:

class Date(object):
day = 0
month = 0
year = 0

def __init__(self, day=0, month=0, year=0):
    self.day = day
    self.month = month
    self.year = year

这个类很显然可以被用来存储某些日期信息(不考虑时区信息;让我们假设所有的日期都用UTC表示)

这里定义了__init__,典型的类实例初始化方法,它作为典型的instancemethod接受参数,其中第一个传递的必要参数是新建的实例本身。

类方法

有一些可以通过使用classmethod很好解决的任务。

假设我们有很多(\'dd-mm-yyyy\')格式字符串的日期信息,想要把它们创建成Date类实例。我们不得不在项目的不同地方做这些事情。

所以我们必须要做到:

分析得到的年月日字符串,把它们转化成三个整形变量或者拥有三个元素的元组的变量。
通过传递这些值实例化Date。
得到:

day, month, year = map(int, string_date.split(\'-\'))
date1 = Date(day, month, year)
C++拥有重载的特性可以达到这种目的,但是Python缺乏此类特性。所以,python使用classmethod的方式。让我们尝试一种另类的构造函数。
\'\'\'
class Date(object):
day = 0
month = 0
year = 0

def __init__(self,day=0,month=0,year=0):
    self.day = day
    self.month = month
    self.year = year

@classmethod
def from_str(cls,date_as_str):
    day,month,year = date_as_str.split(\'-\')
    date1 = cls(year,month,day)
    return date1

date2 = Date.from_str(\'2018-12-15\')
print (date2)
print (date2.year)
date4 = Date(1,1,2020)
print (date4.year)
\'\'\'
【结果】
<main.Date object at 0x102a50f28>
2018
2020
\'\'\'

class subDate(Date):
pass

date3 = subDate.from_str(\'2019-01-01\')
print (date3)
print (date3.year)
date5 = subDate(1,1,2021)
print (date5.year)
\'\'\'
【结果】
<main.subDate object at 0x102a560b8>
2019
2021
\'\'\'
\'\'\'
进一步分析一下以上代码的执行,以及它的优势:

1.在一个地方解析日期字符串并且重复使用它。
2.做到很好的封装(相对于把执行字符串解析作为一个单独的函数在任何地方执行,这里使用的方法更符合OOP的范式)
3.cls表示类对象,而不是类实例。这样很酷,因为如果我们继承Date类,那么所有的子类也都将拥有from_string这个方法。
\'\'\'

\'\'\'
静态方法

那么staticmethod又是什么呢?它和classmethod非常的相似,但是不强制要求传递参数(但是做的事与类方法或实例方法一样)。

让我们来看一个使用的例子。

我们有一个日期字符串需要以某种方式验证。这个任务与之前一样要定义在Date类内部,但是不要求实例化它。

静态方法在这种情况下就非常有用。看一下下面这个代码片段:
\'\'\'
class Date2(object):
@staticmethod
def is_date_valid(date_as_str):
year,month,day = map(int,date_as_str.split(\'-\'))
return year <= 3999 and month <=12 and day <= 31
is_date = Date2.is_date_valid(\'2019-1-1\')
print (is_date)
\'\'\'
【结果】
True
\'\'\'
\'\'\'
现在正如我们了解到的staticmethod的使用,我们不需要访问它所属的类,它本质上就是一个函数,调用方式和调用函数一样,不同的是它不关注对象和对象内部属性。
\'\'\'

\'\'\'
@staticmethod,@classmethod

有了@property装饰器的了解,这两个装饰器的原理是差不多的。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()构造函数。

class classmethod(object):
"""
classmethod(function) -> method
"""
def init(self, function): # for @classmethod decorator
pass
# ...
class staticmethod(object):
"""
staticmethod(function) -> method
"""
def init(self, function): # for @staticmethod decorator
pass
# ...
装饰器的@语法就等同调用了这两个类的构造函数。

class Foo(object):

@staticmethod
def bar():
    pass

# 等同于 bar = staticmethod(bar)

至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。

\'\'\'

\'\'\'
【装饰器里那些坑】

\'\'\'

def html_tags(tag_name):
print (\'begin outer function.\')
def wrapper_(func):
print ("begin of inner wrapper function.")
def wrapper(args, **kwargs):
content = func(
args, **kwargs)
print ("<{tag}>{content}</{tag}>".format(tag=tag_name, content=content))
print (\'end of inner wrapper function.\')
return wrapper
print (\'end of outer function\')
return wrapper_

@html_tags(\'b\')
def b_hello(name=\'b_hello\'):
return \'Hello {}!\'.format(name)

@html_tags(\'div\')
def div_hello(name=\'div_hello\'):
return \'Hello %s\' % name

b_hello()
b_hello(\'wxue\')
b_hello(\'wqi\')
div_hello()
div_hello(\'wxue\')
div_hello(\'wqi\')

\'\'\'
在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
Hello b_hello!
Hello wxue!
Hello wqi!

Hello div_hello
Hello wxue
Hello wqi
\'\'\'

\'\'\'
错误的函数签名和文档
装饰器装饰过的函数,看上去名字没变,其实已经变了
\'\'\'
def logging(func):
def wrapper(args,**kwargs):
print (\'[DEBUG]: enter %s\' % func.name)
return func(
args,**kwargs)
return wrapper
@logging
def say(something):
print (\'say %s\' % something)
print (say.name) #wrapper

\'\'\'
为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。

say = logging(say)
logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给say,say的__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如doc,source等等。

使用标准库里的functools.wraps,可以基本解决这个问题。
\'\'\'

from functools import wraps
def logging(func):
@wraps(func)
def wrapper(args,**kwargs):
print (\'[DEBUG]: enter %s\' % func.name)
return func(
args,**kwargs)
return wrapper

@logging
def say(something):
print (\'say %s\' % something)

print (say.name) #say

\'\'\'
看上去不错!主要问题解决了,但其实还不太完美。因为函数的签名和源码还是拿不到的。

import inspect
print inspect.getargspec(say) # failed
print inspect.getsource(say) # failed
如果要彻底解决这个问题可以借用第三方包,比如wrapt。后文有介绍。
\'\'\'

\'\'\'
不能装饰@classmethod 和@staticmethod
\'\'\'
from functools import wraps
def logging(func):
@wraps(func)
def wrapper(args,**kwargs):
print (\'[DEBUG] : enter %s\' % func.name)
return func(
args,**kwargs)
return wrapper
class Car(object):
def init(self, model):
self.model = model

@logging  # 装饰实例方法,OK
def run(self):
    print ("{} is running!".format(self.model))

@logging  # 装饰静态方法,Failed
@staticmethod
def check_model_for(obj):
    if isinstance(obj, Car):
        print ("The model of your car is {}".format(obj.model))
    else:
        print ("{} is not a car!".format(obj))

Car.check_model_for(\'abc\') #AttributeError: \'staticmethod\' object has no attribute \'name\'

\'\'\'
前面已经解释了@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,
那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。
要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,
因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的。
\'\'\'
from functools import wraps
def logging(func):
@wraps(func)
def wrapper(args,**kwargs):
print (\'[DEBUG]: enter %s\' % func.name)
return func(
args,**kwargs)
return wrapper
class Car(object):
def init(self,model):
self.model = model
@logging
def run(self):
print (\'%s is running..\' % self.model)

@staticmethod
@logging
def check_model_for(obj):
    if isinstance(obj,Car):
        print (\'The model of your car is %s\' % obj.model)
    else:
        print (\'%s is not a car!\' % obj)

Car.check_model_for(\'abc\')
\'\'\'
【结果】
[DEBUG]: enter check_model_for
abc is not a car!
\'\'\'

\'\'\'
[如何优化你的装饰器]
嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。
decorator.py
是一个非常简单的装饰器加强包,你可以很直观的先定义包装函数wrapper(),再使用decorator(func,wrapper)方法就可以完成一个装饰器
记得要先pip install decorator哦
\'\'\'

from decorator import decorate

def wrapper(func,args,**kwargs):
print (\'[DEBUG] : enter %s\' % func.name)
return func(
args,**kwargs)

def logging(func):
return decorate(func,wrapper)

@logging
def hello(something):
print (\'hello %s\' % something)

hello(\'decorator\')
\'\'\'
[DEBUG] : enter hello
hello decorator
\'\'\'

\'\'\'
你也可以使用她自带的@decorator装饰器来完成你的装饰器
\'\'\'
from decorator import decorator

@decorator
def logging(func,args,**kwargs):
print (\'[DEBUG]: enter %s\' % func.name)
return func(
args,**kwargs)

@logging
def hello(something):
print (\'hello %s\' % something)
hello(\'wqi\')
print (hello.name)
print (hello.doc)
import inspect
print (inspect.getsource(hello))
print (inspect.getsource(hello.wrapped))
\'\'\'
hello wqi
hello
None
@logging
def hello(something):
print (\'hello %s\' % something)

@logging
def hello(something):
print (\'hello %s\' % something)

【解析】
原文的解析是:
decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.wrapped)。
但是我的运行结果却如上,原因待查(猜测是最新版的模块代码有完善)
\'\'\'

\'\'\'
wrapt

wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。

import wrapt

without argument in decorator

@wrapt.decorator
def logging(wrapped, instance, args, kwargs): # instance is must
print "[DEBUG]: enter {}()".format(wrapped.name)
return wrapped(*args, **kwargs)

@logging
def say(something): pass
使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。

如果你需要使用wrapt写一个带参数的装饰器,可以这样写。

def logging(level):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
print "[{}]: enter {}()".format(level, wrapped.name)
return wrapped(*args, **kwargs)
return wrapper

@logging(level="INFO")
def do(work): pass
关于wrapt的使用,建议查阅官方文档,在此不在赘述。

http://wrapt.readthedocs.io/en/latest/quick-start.html
\'\'\'

\'\'\'
【小结】
Python的装饰器和Java的注解(Annotation)并不是同一回事,和C#中的特性(Attribute)也不一样,完全是两个概念。

装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都被命名为wrapper(),意义在于包装。函数只有在被调用时才会发挥其作用。比如@logging装饰器可以在函数执行时额外输出日志,@cache装饰过的函数可以缓存计算结果等等。

而注解和特性则是对目标函数或对象添加一些属性,相当于将其分类。这些属性可以通过反射拿到,在程序运行时对不同的特性函数或对象加以干预。比如带有Setup的函数就当成准备步骤执行,或者找到所有带有TestMethod的函数依次执行等等。

至此我所了解的装饰器已经讲完,但是还有一些内容没有提到,比如装饰类的装饰器。有机会再补充。谢谢观看。
\'\'\'

参考原文地址:http://www.cnblogs.com/cicaday/p/python-decorator.html 谢谢作者 很详细

以上是关于Python装饰器的主要内容,如果未能解决你的问题,请参考以下文章

[TimLinux] Python 装饰器

python装饰器

python装饰器关键代码

Python装饰器

python之装饰器

python 装饰器