🇨🇳
99 的博客
  • 久韭
  • Python
    • 你真的“创建”了类吗?
    • MRO 三定律
    • 描述器学习指南
    • 描述器实现 property
    • 如何做一个好中介
    • PEP 阅读清单
    • Python 双刃剑
    • 类型标注
    • “类”的渐进式剖析
  • Network
    • IPv6
    • 大道至简
    • DNS 根域
    • RFC 阅读清单
    • 基础公共服务
      • DNS
      • NTP
    • MAC 地址厂商信息
    • 点对点的中介
  • Windows
    • Windows 使用小技巧
  • Linux
    • SSH 技高一筹
    • nftables 速成
    • Linux 30 周年
    • 新时代 Linux 命令
    • 树莓派实时性优化
    • Bash DEBUG 一行搞定
  • 佳作收藏
    • 美文
      • 短篇
        • 爱
        • 弟弟
        • 暗途
        • 噩梦
        • 想象
        • 底线
        • 疯人
        • 雪兔
        • 夺妻
        • 闹钟
        • 虐猫
        • 做起来
        • 陈小手
        • 道德极限
        • 符号控制
        • 与人为友
        • 消失的人
        • 时间旅行
        • 情债肉偿
        • 铁血恋爱
        • 太阳黑子
        • 蚂蚁人生
        • 风骚和魅力
        • 我只要一种
        • 不朽的失眠
        • 秋天的怀念
        • 午夜的汽笛
        • 多美的故事
        • 相信不相信
        • 清晨的变故
        • 真实的高贵
        • 神秘领奖人
        • 幸福的夜晚
        • 幸福的生日
        • 我有事找你
        • 我是个窃贼
        • 最担心的是你
        • 客厅里的爆炸
        • 假如是你的话
        • 愿你慢慢长大
        • 民意与伪民意
        • 婚姻中没有天堂
        • 一种深久的不安
        • 非走不可的弯路
        • 单调产生的快乐
        • 我只是讨厌屈服
        • 跳槽只为“软福利”
        • 你的阅读造就了你
        • 聪明人和傻子和奴才
        • 你永远有做不完的事
        • 精神病院里的年轻人
        • 一位短跑运动员的孤独
        • 那些你所不知道的大事
        • 穿过大半个中国去睡你
        • 我觉得自己会永远生猛下去
      • 长篇
        • 早餐
        • 波心
        • 老王
        • 翻浆
        • 小猫
        • 良娼
        • 自信
        • 黑羊
        • 猫人
        • 斜眼
        • 识人
        • 身价
        • 肯肯舞
        • 狗和猫
        • 热包子
        • 黄裙子
        • 老江湖
        • 逃脱术
        • 侯银匠
        • 太原诅咒
        • 死亡花朵
        • 罗马惊艳
        • 刺青时代
        • 好嘴杨巴
        • 扼杀胎儿
        • 纪念照片
        • 紫色人形
        • 天外飞石
        • 戒烟公司
        • 蔡二少爷
        • 女人的星球
        • 一千张糖纸
        • 爱情与投资
        • 赌徒的遗书
        • 飞越流水线
        • 跟踪狂入门
        • 一桩自杀案
        • 机舱里的钟声
        • 桌子还是桌子
        • 一小时的故事
        • 不存在的女友
        • 谁在编造历史
        • 父亲坐在黑暗中
        • 一个小小的建议
        • 彬彬有礼的强盗
        • 18 本画册的爱恋
        • 河流最蓝的地方
        • 好人总会有人疼
        • 偷听谈话的妙趣
        • 公主整夜不能睡
        • 他们那时多有趣
        • 布莱克·沃兹沃斯
        • 坐在路边鼓掌的人
        • 最伟大的科幻小说
        • 你丈夫是干什么的
        • 谁也看不见的阳台
        • 上午打瞌睡的女孩
    • 段子
      • 程序员段子
      • 二次元段子
      • 大老师片段
      • 金句
      • 喝痰
      • 数学家
      • 牛子
      • 男 ♂ 语
      • 甲方乙方
      • 新编故事四则
      • 没人知道大东为何在此舞蹈
  • 日常随记
    • 坑
    • 豆知识
    • 逻辑推断
    • 观影后记
    • LaMDA 有意识吗?
    • VS Code Vim 速记
    • QQ 空间表情符号
    • ZEN of DEBUG
    • 数据收集的途径
    • 呼叫转移设置方式
    • 空中浩劫 ACI 经典
    • 联机海难特色简述
    • 智能家居入门指南
    • 财政学笔记
    • 行为与实验经济学笔记
    • 电商管理学笔记
由 GitBook 提供支持
在本页
  • 引言
  • 装饰器基本内容
  • 多层装饰器的含义
  • 带参装饰器的含义
  • 描述器基本内容
  • 官方模拟的源码
  • 代码分析
  • 改写测试用代码
  • 全部输出
  • 初始化部分
  • 对象 a1 创建及属性操作部分
  • 对象 a2 创建及属性操作部分
  • @property 中的小陷阱
  • 扩展内容
  • 参考
在GitHub上编辑
  1. Python

描述器实现 property

描述器实战应用

上一页描述器学习指南下一页如何做一个好中介

最后更新于1年前

引言

在 Python 官方文档中的 一篇中,给出了常用装饰器 property 的 Python 代码模拟实现,且这个实现是基于描述器原理的。在本文中,作者将利用这段代码,向你展示 描述器的实际运行原理。

装饰器基本内容

首先我们回忆一下装饰器的有关内容。

装饰器本质是个返回函数的函数,表现为数学概念中的复合函数(g∘f)(x)⇒g(f(x))\big(g \circ f\big)(x) \Rightarrow g\big(f(x)\big)(g∘f)(x)⇒g(f(x))

下面我们用伪代码再补充一些容易产生疑问的情况。

多层装饰器的含义

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

↑↑↑ 等价于 ↓↓↓

func = dec2(dec1(func))

带参装饰器的含义

@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
    pass

↑↑↑ 等价于 ↓↓↓

func = decomaker(argA, argB, ...)(func)

类装饰器和函数装饰器是一致的,只相当于将 func 代表的函数换做 cls 代表的类罢了,故这里不再多做涉及。

如果对于装饰器还有问题的话建议阅读一下官方 PEP,可以直接使用 Google 翻译成中文,可读性还是可以接受的。

描述器基本内容

这里推荐我针对描述器撰写的上一篇博文

官方模拟的源码

我们首先看一下用描述器模拟实现装饰器 property 的源代码。

(这里大概看一下就好,我们后文再详细分析)

class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

代码分析

改写测试用代码

其实作者比较喜欢的方式是利用 VS Code 在代码段顶部就加上断点,然后逐句执行查看代码的具体运行位置和变量值。 但这种方式难以通过文字向大家展示,所以我换了个变通的方式,在每个关键环节都加一个 print() 输出相关内容,这样我们看代码的命令行反馈就可以了。

更改后的测试代码如下:

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    print('body of Property')

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        print(f'Property.__init__() {self=}')
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        print(f'Property.__get__() {self=} {obj=} {objtype=}')
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        print(f'Property.__set__() {self=} {obj=}')
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        print(f'Property.__delete__() {self=} {obj=}')
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        print(f'Property.getter() {self=}')
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        print(f'Property.setter() {self=}')
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        print(f'Property.deleter() {self=}')
        return type(self)(self.fget, self.fset, fdel, self.__doc__)


class A(object):

    print('body of A')

    def __init__(self):
        print(f'A.__init__() {self=}')
        self._x=0
        pass 

    @Property       # 事实上这里就是 getter 哦
    def x(self):
        print(f'A.x.fget() {self=}')
        return self._x

    @x.setter
    def x(self, value):
        print(f'A.x.fset() {self=}')
        self._x=value
    @x.deleter
    def x(self):
        print(f'A.x.fdelete() {self=}')
        del self._x

    # setter 和 deleter 的函数命名并不会有具体含义,所以按照官方的示例,直接与 getter 同名即可

print('\n----- before a1 = A() -----')
a1 = A()
print('\n----- before   a1.x   -----')
a1.x
print('\n-----before  a1.x = 1 -----')
a1.x = 1
print('\n-----before del a1.x  -----')
del a1.x

print()

print('\n----- before a2 = A() -----')
a2 = A()
print('\n----- before   a2.x   -----')
a2.x
print('\n-----before  a2.x = 1 -----')
a2.x = 1
print('\n-----before del a2.x  -----')
del a2.x

全部输出

运行以上代码得到的输出是:

body of Property
body of A
Property.__init__() self=<__main__.Property object at 0x000002A7BBA310A0>
Property.setter() self=<__main__.Property object at 0x000002A7BBA310A0>
Property.__init__() self=<__main__.Property object at 0x000002A7BBA31100>
Property.deleter() self=<__main__.Property object at 0x000002A7BBA31100>
Property.__init__() self=<__main__.Property object at 0x000002A7BBA310A0>

----- before a1 = A() -----
A.__init__() self=<__main__.A object at 0x000002A7BBA31100>

----- before   a1.x   -----
Property.__get__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31100> objtype=<class '__main__.A'>
A.x.fget() self=<__main__.A object at 0x000002A7BBA31100>

-----before  a1.x = 1 -----
Property.__set__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31100>
A.x.fset() self=<__main__.A object at 0x000002A7BBA31100>

-----before del a1.x  -----
Property.__delete__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31100>
A.x.fdelete() self=<__main__.A object at 0x000002A7BBA31100>


----- before a2 = A() -----
A.__init__() self=<__main__.A object at 0x000002A7BBA31160>

----- before   a2.x   -----
Property.__get__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31160> objtype=<class '__main__.A'>
A.x.fget() self=<__main__.A object at 0x000002A7BBA31160>

-----before  a2.x = 1 -----
Property.__set__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31160>
A.x.fset() self=<__main__.A object at 0x000002A7BBA31160>

-----before del a2.x  -----
Property.__delete__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31160>
A.x.fdelete() self=<__main__.A object at 0x000002A7BBA31160>

由于这其中涉及的内容较多,我们拆开来做分析。

初始化部分

body of Property        # Python 解释器在发现类创建操作后,对类进行初始化
body of A               # 类初始化首先执行的就是类体中的代码,而类体中的 @Property def x(self): .... 相当于被转化为了 x=Property(x)  注意后面这个 x 是原本的函数
                        # 如果不理解的话建议回头看一下最上面的 装饰器基本内容
Property.__init__() self=<__main__.Property object at 0x000002A7BBA310A0>        # 继而 Property 对象初始化
Property.setter() self=<__main__.Property object at 0x000002A7BBA310A0>          # 这里对应的是 @x.setter
Property.__init__() self=<__main__.Property object at 0x000002A7BBA31100>        # 注意这里是在原本 Property 的对象基础上,又创造了一个新的Property对象(具体参见Property的setter方法)
Property.deleter() self=<__main__.Property object at 0x000002A7BBA31100>         # 对应 @x.deleter
Property.__init__() self=<__main__.Property object at 0x000002A7BBA310A0>        # 和前面的 setter 一样,这里也是返回了新的 Property 对象
                                                                                 # 自这之后, A 类中和 x 挂勾的 Property 就确定下来了

对象 a1 创建及属性操作部分

----- before a1 = A() -----
A.__init__() self=<__main__.A object at 0x000002A7BBA31100>        # a1 实例对象创建的初始化

----- before   a1.x   -----
Property.__get__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31100> objtype=<class '__main__.A'>
  # 上面这行是实例对象 a1 想要获取属性 x 的值,Property 作为一个描述器,它的 __get__ 方法被调用
A.x.fget() self=<__main__.A object at 0x000002A7BBA31100>        # 在 Property 的 __get__ 方法中,转调用了 通过 @Property 设置的方法

## setter 和 deleter 的调用方式和 getter 一样,没有本质差别

-----before  a1.x = 1 -----
Property.__set__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31100>
A.x.fset() self=<__main__.A object at 0x000002A7BBA31100>

-----before del a1.x  -----
Property.__delete__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31100>
A.x.fdelete() self=<__main__.A object at 0x000002A7BBA31100>

对象 a2 创建及属性操作部分

----- before a2 = A() -----
A.__init__() self=<__main__.A object at 0x000002A7BBA31160>        # a2 实例对象初始化,a2 和 a1 不是相同的对象

                            # 以下的调用整体上与前面 a1 的调用方式是一样的
                            # 但是请注意:尽管 a2 和 a1 不是相同的对象,但 Property 对象还是同一个
                            # 这说明类对应的描述器对象其实是唯一的
----- before   a2.x   -----
Property.__get__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31160> objtype=<class '__main__.A'>
A.x.fget() self=<__main__.A object at 0x000002A7BBA31160>

-----before  a2.x = 1 -----
Property.__set__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31160>
A.x.fset() self=<__main__.A object at 0x000002A7BBA31160>

-----before del a2.x  -----
Property.__delete__() self=<__main__.Property object at 0x000002A7BBA310A0> obj=<__main__.A object at 0x000002A7BBA31160>
A.x.fdelete() self=<__main__.A object at 0x000002A7BBA31160>

@property 中的小陷阱

@property 装饰后生成的描述器实际上成为了数据描述器!

扩展内容

其实在同一篇中官方也给出了 staticmethod 和 classmethod 的类似模拟实现,有兴趣的读者也不妨一览:

class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

参考

描述器使用指南
PEP 318 -- Decorators for Functions and Methods
PEP 3129 – Class Decorators
描述器学习指南
描述器使用指南
property 装饰器的描述器实现
PEP 318 -- Decorators for Functions and Methods
PEP 3129 – Class Decorators