Python中的装饰器(decorator)与描述器(descriptor)

python

装饰器(decorator)和描述器(descriptor)是Python中两个重要的概念,理解它们是深入理解Python的关键,因为这是很多特性的基础,包括:函数、方法、属性、类方法、静态方法和父类的引用等等(Python如何实现property、classmethod和staticmethod)。

本文重点在于探讨decorator和descriptor特性的细节。


装饰器(decorator)

装饰器可以认为是面向切面编程模式(AOP aspect-oriented programming)的一种实现,可以对原有功能做扩展或者彻底的改变,而不改变外部的调用方式。

语法分析

下面来看一个例子,fPrint函数实现了一个打印的功能,现在需要在不改变fPrint的前提下,为打印内容增加一个前缀,以方便阅读

def header(func):
    def wrapper(msg):
        print('log: ', end='')
        func(msg)

    return wrapper

@header
def fPrint(msg):
    print(msg)

# 调用打印
fPrint('msg')

# 打印内容
>>>> log: msg

从结果上来看fPrint('msg')的结果:先调用了header然后调用了fPrint。我们注意到,fPrint的函数定义前面,多了@header,装饰器语法实际上是将fPrint的实现做了替换,所以我们调用的不再是原始的那个fPrint。装饰器语法实际上等价于:

...

def fPrint(msg):
    print(msg)
    
fPrint = header(fPrint)

# 调用打印
fPrint('msg')

# 打印内容
>>>> log: msg

带有自定义入参的装饰器

通常装饰器函数是以被装饰函数作为入参,我们要手动传入参数该怎么办?记住一点装饰器语法实际是函数地址的替换,从最简单的装饰器(fPrint = header(fPrint))入手,推测一下自定义入参等价形式:fPrint = header(param)(fPrint),根据等价形式实现装饰器:

def header(ipt):
    def decorator(func):
        def wrapper(msg):
            print('{} log: '.format(ipt), end='')
            func(msg)
        return wrapper

    return decorator

@header('header')
def fPrint(msg):
    print(msg)

# 调用打印
fPrint('msg')

# 打印内容
>>>> header log: msg

关于语法的一些理解:当@header后面省略参数时,则以后面的函数定义(fPrint)为参数来完成函数替换。有参数时,执行后的结果为一个函数,再以后面的函数定义(fPrint)为参数完成函数替换,即fPrint = header('header')(fPrint)

装饰器函数写起来不是很好理解,尤其是对于含有自定义参数的装饰器,这里建议,先写出要实现的装饰器的等价形式,依此来写装饰器函数

装饰器类的定义

装饰器可以是一个函数,也可以是一个class, fPrint作为__init__函数的参数传入,fPrint = header(fPrint),这时fPrint已经不再是函数而是header的一个实例,至于有什么用等下篇再谈。

class header(object):
    def __init__(self, func):
        self.f = func

@header
def fPrint(msg):
    print(msg)

描述器(descriptor)

描述器是任何一个实现了描述器协议(__get__(), __set__(), 或者 __delete__())的对象,当描述器作为class属性时,对这个属性的访问便会触发相应的方法。通常使用a.b的语法来获取、赋值、删除一个class属性时,会在A.__dict__中查找属性b,如果b是一个描述器,相应的描述器方法就会被调用。

描述器的实现

以下方法仅适用于,一个描述器作为class属性时,这一点需要特别注意,如果作为实例属性,对应方法是不会被触发的
在以下的例子中,"属性"指的是class属性,即type(a).__dict__中的某个元素。

object.__get__(self, instance, owner=None)
当通过类或者实例获取属性时__get__方法时会被调用,可选的owner参数是持有者类(以a.b为例,ownerA),当通过实例获取属性时,instance为对应实例(以a.b为例,instancaa),通过持有者类获取时(A.b),instanceNone,这个方法应该返回一个计算好的属性值或者抛出AttributeError异常

object.__set__(self, instance, value)
通过持有者类的实例对一个属性设置新的值时会调用__set__方法,注意的是,添加set或者delete方法会使得描述器的类型变为数据型描述器,查看“描述器的调用”获取更多细节。

object.__delete__(self, instance)
通过持有者的实例来del一个属性时会调用__delete__方法

object.__set_name__(self, owner, name)
描述器被创建并添加到持有者类时被调用,这个调用不会自动进行,需要显式调用,以告知描述器已被持有者持有

class A:
    pass

descr = custom_descriptor()
A.attr = descr
descr.__set_name__(A, 'attr')

描述器方法的调用

首先,关注一下普通属性的调用优先级:
一个属性可以是class属性,也可以是实例属性,回顾一下属性调用的优先级:
a.x调用优先级为:a.__dict__['x']type(a).__dict__['x'],所以当一个class属性不存在同名的实例属性时,通过class和实例都可以调用。

描述器是基于属性的调用来发挥作用的,对于一个描述器(它是一个class 属性)在不存在实例属性x的情况下,a.x等价于A.x等价于type(a).__dict__['x']

  1. 以下是触发描述器__get__方法的情景:

    • 直接调用:
      x.__get__(a)

    • 通过实例调用:
      a.x

    • 通过类调用:
      A.x

  2. 以下是触发描述器__set__方法的情景:

class A(object):
    pass

class Descriptor(object):
    def __set__(self, instance, value):
        pass

A.b = Descriptor()
a = A()

# 用class调用不会触发__set__
# A.b = 'b'

# 实例调用才会触发__set__
a.b = 'b'
  1. 以下是触发描述器__delete__方法的情景:
class A(object):
    pass

class Descriptor(object):
    def __set__(self, instance, value):
        pass

    def __delete__(self, instance):
        pass

a = A()
A.b = Descriptor()
del a.b

数据型和非数据型描述器

  • 数据型描述器:
    定义了__set____get__方法,这种描述器可以监听读和写

  • 非数据型描述器:
    只定义了__get__方法,只能监听属性的读取操作。

二者的区别在于,以a.b为例:
对于一个非数据型描述器b,如果给a设置了一个实例属性b,根据属性读取的优先级,a.b读取的将是a的实例属性b,描述器的__get__方法将不会被触发。而对于数据型描述器,即使存在一个同名的实例属性,a.b访问的仍然是描述器,数据型描述器不会被实例属性覆盖。

class DataDescriptor(object):
    def __set__(self, instance, value):
        print('data descriptor set')

    def __get__(self, instance, owner=None):
        return 'data descriptor get'

class NonDataDescriptor(object):

    def __get__(self, instance, owner=None):
        return 'non-data descriptor get'

class A(object):
    dataDescriptor = DataDescriptor()
    nonDataDescriptor = NonDataDescriptor()
    pass

a = A()

print('a.dataDescriptor: {}'.format(a.dataDescriptor))
print('a.nonDataDescriptor: {}'.format(a.nonDataDescriptor))

a.dataDescriptor = 11

# 非数据型描述器会被实例属性覆盖
a.nonDataDescriptor = 22 

print('a.dataDescriptor: {}'.format(a.dataDescriptor))
print('a.nonDataDescriptor: {}'.format(a.nonDataDescriptor))

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