Python decorators

Python代码里经常看到一个符号@摆在函数的上面一行,这叫decorator,但它到底是什么,表示什么意思,有什么好处呢?decorator是用来修饰函数的,等于将原函数(比如foo)经过了一定加工,达成的效果是foo = our_decorator(foo)这个样子,可以简写成@our_decoratorfoo上面。

举个简单的例子:首先我们定义一个函数

def foo(x):
      print("Hi, foo has been called with " + str(x))

调用此函数

foo("Hi")
# Hi, foo has been called with Hi

加上decorator:

def our_decorator(func):
      def function_wrapper(x):
            print("Before calling " + func.__name__)
            func(x)
            print("After calling " + func.__name__)
      return function_wrapper

foo = our_decorator(foo)

我们将foo作为一个function object传输给了our_decorator,our_decorator被调用并返回了function wrapper这个函数。这等于说foo经过了our_decorator的修饰,多了一些功能。但是这时候foo还没有被调用。接下来调用被修饰过的foo函数:

foo(42)
"""
Before calling foo
Hi, foo has been called with 42
After calling foo
"""

这就是decorator做的事情,我们可以用一个@符号来取代foo = our_decorator(foo)这一行,更加简单和pythonic:

@our_decorator
def foo(x):
    print("Hi, foo has been called with " + str(x))

其它的装饰函数定义和调用foo函数的方法不变。

要理解decorator,主要是要理解callback function的含义,即给现有函数传入一个函数,返回的也是一个函数,而传入函数将作为一个值暂居在被返回的函数里,等到现有函数被调用的时候,实际上调用的是被返回的那个函数,其中也包括了我们传入的函数。

稍微复杂一点,如果我们想给decorator也加parameter的话:

def greeting(expr):
    def greeting_decorator(func):
        def function_wrapper(x):
            print(expr + ", " + func.__name__ + " returns:")
            func(x)
        return function_wrapper
    return greeting_decorator

@greeting("καλημε�α")
def foo(x):
    print(42)

foo("Hi")

在装饰函数的时候,尽管我们最后调用的还是原来那个函数的名字foo,但是它的元信息已经发生了变化,比如:

def greeting(func):
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper

@greeting
def f(x):
    """ just some silly function """
    return x + 4

f(10)
print("function name: " + f.__name__)
print("docstring: " + f.__doc__)
print("module name: " + f.__module__)

'''
Hi, f returns:
function name: function_wrapper
docstring:  function_wrapper of greeting 
module name: greeting_decorator
'''

我们看到,原函数被传入装饰函数时,还具有原来的信息,但是当我们调用被装饰过的函数时,函数名字、docstring、被调用的模块都发生了变化。如果我们想保留foo原来的元函数信息的话,可以在装饰函数中进行元函数信息的保留,即将返回函数的元信息设置成原函数的元信息:

def greeting(func):
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    function_wrapper.__name__ = func.__name__
    function_wrapper.__doc__ = func.__doc__
    function_wrapper.__module__ = func.__module__
    return function_wrapper

f(10)
print("function name: " + f.__name__)
print("docstring: " + f.__doc__)
print("module name: " + f.__module__)

'''
Hi, f returns:
function name: f
docstring:  just some silly function 
module name: __main__
'''

哎哟,这么多行累死了是不是?我们可以给 function_wrapper这个函数也加一个装饰函数,达到一样的效果:

from functools import wraps

def greeting(func):
    @wraps(func)   # 使function_wrapper具有跟func一样的元信息
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper

举一个现实生活中的例子。用户登录页面的时候,我们常要先检查其是否有访问权限,因为这种操作非常多,可以将之refactor成一个装饰函数:

from flask import Flask, request, abort
from functools import wraps

app = Flask(__name__)


def validate_json(*expected_args):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            json_object = request.get_json()
            for expected_arg in expected_args:
                if expected_arg not in json_object:
                    abort(400)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@app.route('/grade', methods=['POST'])
@validate_json('student_id')
def update_grade():
    json_data = request.get_json()
    print(json_data)
    # update database
    return "success!"

我们也可以用一连串的decorator,比如以下用了两个:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makebold  # hello = makebold(makeitalic(hello)), returns <b><i>hello()<i><b>
@makeitalic  # hello = makeitalic(hello), returns <i>hello()</i>
def hello():
    return "hello world"

print hello() ## returns "<b><i>hello world</i></b>"

最后,任何callable object都可以成为装饰主体,因此除了函数,我们还可以用class。如:

class decorator2:
    
    def __init__(self, f):
        self.f = f
        
    def __call__(self):
        print("Decorating", self.f.__name__)
        self.f()

@decorator2
def foo():
    print("inside foo()")

foo()

当进行装饰的时候,__init__被调用并保存了f的信息,当foo被调用的时候,__call__函数被调用。

在现实生活中常用的decorator有:

@classmethod (俺尚不是很理解它的应用价值)

在一个类的函数上使用@classmethod,函数返回该类的构造函数。这样的情况下,我们调用了一个类中的函数,同时也创造了一个对象。比如:

from datetime import date

# random Person
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def fromBirthYear(cls, name, birthYear):
        return cls(name, date.today().year - birthYear)

    def display(self):
        print(self.name + "'s age is: " + str(self.age))

person = Person('Adam', 19)
person.display()

person1 = Person.fromBirthYear('John',  1985)
person1.display()

'''
Adam's age is: 19
John's age is: 31
'''

@classmethod比@staticmethod多的一个好处是,它能正确反映继承关系。如:

from datetime import date

# random Person
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @staticmethod
    def fromFathersAge(name, fatherAge, fatherPersonAgeDiff):
        return Person(name, date.today().year - fatherAge + fatherPersonAgeDiff)

    @classmethod
    def fromBirthYear(cls, name, birthYear):
        return cls(name, date.today().year - birthYear)

    def display(self):
        print(self.name + "'s age is: " + str(self.age))

class Man(Person):
    sex = 'Male'

man = Man.fromBirthYear('John', 1985)
print(isinstance(man, Man))      # True

man1 = Man.fromFathersAge('John', 1965, 20)
print(isinstance(man1, Man))      # False

@property

在类里,我们常用一个getter和setter来对类中的变量进行操作,这是为了data encapsulation的考虑,也可以在set和get的时候进行bound check。@property就提供了这样一个方法。

class Person(object):  
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name

    @full_name.setter
    def full_name(self, value):
        first_name, last_name = value.split(' ')
        self.first_name = first_name
        self.last_name = last_name

    @full_name.deleter
    def full_name(self):
        del self.first_name
        del self.last_name

我们可以看到,full_name函数经过@property装饰,有了getter的功能,而经过@full_name.setter的装饰,有了setter的功能。虽然在类中,函数不可以有相同的名字,但他们经过了装饰,所以这是可行的。要注意的是,getter的装饰函数叫@property,setter的装饰函数是@func.setter。property函数是这样定义的:property(fget=None, fset=None, fdel=None, doc=None)。装饰之后,我们得到:

full_name.fget is full_name_getter    # True  
full_name.fset is full_name_setter    # True  

之后我们在这个类的对象上进行set或者get的操作的话,就会调用相应的函数。比如person.name = 'Bob',那么就会调用full_name_getter函数。但要注意的是,只有当类从object继承的时候,才会有这样的效果。所以在定义类的时候,一定要从object继承哦。

Reference

  1. Python tutorial
  2. How to make a chain of function decorators?
  3. Primer on Python Decorators
  4. Python classmethod()
  5. Python Properties
  6. Clear example
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容