一文彻底搞懂python中的描述器反射(代码片段)

小杰666 小杰666     2022-12-23     652

关键词:

描述器

什么是描述器?

一个类中定义了如下一个或多个魔术方法,这个类的实例就是描述器:

__get__,__set__,__delete__

通常需要两个类来构建描述器:

如果类B的类属性x,指向另一个类A的实例。被指向的A的实例就是描述器对象。B.x是描述器,B也是描述器的属主(owner)。比如:

class A:
    def __get__(self, instance, owner):
        pass
    
    def __set__(self, instance, value):
        pass
    
    def __delete__(self, instance):
        pass

class B:
    x = A()  # x是描述器
    pass

类属性的值,通常是一些已有类型的对象,比如字符串、列表等。

当使用了描述器,类属性就指向一个描述器对象,描述器通过三个魔术方法,可以自定义属性的行为。

描述器的分类

非数据描述器:

只定义了__get__,就是非数据描述器(non-data descriptor)。

数据描述器:

定义了__get__,且定义了__set____set__ 与 __delete__,就是数据描述器(data descriptor)。

属性搜索顺序

当一个实例与它所属的类有相同的属性名时:

非数据描述器,实例的属性搜索顺序:

__getattribute__ ⟶ 默认搜索顺序 [1] ⟶ __getattr__。也就是说,此时__get__无效。

数据描述器,会拦截实例属性字典的访问:

不会访问实例属性字典__dict__。属性访问或修改会被描述器的__get__, __set__, __delete__方法处理。

注意:

如果有 __getattribute__ 方法,不管有没有描述器,实例属性搜索时,都优先调用此方法,可以拦截一切(包括 实例.__dict__ 的访问也拦截)。

__getattribute__ 和 __getattr__ 又是做什么的?下文讲。

[1] 默认搜索顺序:
默认搜索顺序就是没有描述器时的搜索顺序,遵循如下规则:
实例的属性字典(__dict__) ⟶ 类的属性字典 ⟶ 类的父类的属性字典 ⟶ … ⟶ 祖先类object的属性字典

属性搜索顺序与类的继承有关。如果是单继承,属性(或方法)搜索路径是确定的,一直向上找。如果是多继承,就涉及到MRO(方法解析顺序)。Python3的MRO采用C3算法,在类被创建出来的时候,就计算出一个MRO有序列表。关于C3算法,见官方文档

属性读写操作示例

B.x = 400,类属性赋值(赋值即重新定义),如果x是描述器,将被覆盖。
b.x = 500,非数据描述器时,将修改实例自己的属性(__dict__)。
b.x = 600,数据描述器时,将调用描述器的__set__方法。

B.x,若x是描述器,调用描述器的__get__方法。
b.x,若x是描述器,调用描述器的__get__方法。

直接操作实例的__dict__字典,可以绕开描述器对__get____set__等的调用。

举例:

class A:
    def __init__(self):
        print('A().init')
        self.x = 101       # 这是A自己的实例,与B无关

    def __get__(self, instance, owner):
        print('~~~~ A.get ~~~~')
        print(self)         # A的实例本身
        print(instance)     # B.x访问时,为None。b.x访问时,为B的实例对象
        print(owner)        # 类B
        print('~~~~ A.get ~~~~')
        return getattr(instance, 'z', 'no_z_found')  # 查找b.z时,又会回去调用B.__getattribute__

    def __set__(self, instance, value):
        print('~~~~~ A.set ~~~~')
        print(self)         # A的实例本身
        print(instance)     # 修改b.x时,才进入此方法,instance为B的实例对象
        print(value)        # 赋给 b.x 的值
        print('~~~~~ A.set ~~~~')
        instance.z = value  # 演示,把b.x的值保存到b.z,而不是b的属性字典__dict__

    def __delete__(self, instance):
        print('~~~~~ in delete')
        del instance.z

class B:
    # 创建描述器x
    x = A()
    
    # 定义如下方法,实例属性访问最先调用它,但在类A中定义无用
    def __getattribute__(self, item):
        print('___ in getattribute ___')
        
        # 查找b.x时,此处又会调用A.__get__(因为x是描述器),而不是调B.__getattribute__,不然会递归
        return object.__getattribute__(self, item)
    
    def __init__(self):
        print('B().init')
        self.x = 1000       # 非数据描述器时,实例修改自己的属性,self.x会访问实例自己的__dict__
                            # 数据描述器时,调用 A.__set__

b = B()          # 先生成A的实例,即执行A.__init__,生成描述器对象,然后执行B.__init__

# 属性访问
print('\\n' + '-' * 30)
print(B.x)
print()
print(b.x)

# 属性修改
# 覆盖描述器
#B.x = 123
#print(B.x)
print('\\n' + '#' * 30)

# 非数据描述器时,b修改自己的属性,赋值即重新定义,覆盖描述器x
# 数据描述器时,b.x 调用 A.__set__
b.x = 456
print(b.x)

print('\\n' + '=' * 30)
print(b.__dict__)  # 甚至访问b的属性字典,也是调用B.__getattribute__
del b.x            # 删除属性,会调用 A.__delete__
print(b.x)
print(b.__dict__)

反射

上文提到的__getattribute__跟__get__有什么关系呢?实际上前者是反射相关的魔术方法。那什么是反射呢?

当我们需要用到对象的某个属性(或方法),但是由于某种原因无法确定这个属性是否存在,这时我们需要用一种特殊的机制,去访问和操作这个未知的属性,这种机制就称为反射(reflection)。反射就相当于一种自我检查机制。

反射机制不仅包括,要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息,改变程序状态或结构。总之一句话,反射指的是运行时获取类型定义信息,并且还能修改这些信息。

与反射相关的四个函数:getattr、setattr、delattr、hasattr。与这些函数相关的四个魔术方法:__getattr__, __setattr__, __delattr__, __getattribute__。见下面表格:

魔术方法含义
__getattr__此方法只影响实例。实例属性默认搜索顺序:实例自己(的__dict__,后同)、实例的类、类的父类、父类的父类、object祖先类。若从这个顺序中没有找到属性,会抛出AttributeError异常,但类中定义了__getattr__,实例将捕获异常,并调用此方法。此方法可用于实例没有找到属性时,拦截异常,做一些操作。
__setattr__此方法只影响实例。self.x = x, setattr(self, ‘x’, x) 等涉及到修改实例属性的操作时,如果定义了__setattr__,就会调用此方法。此方法可以拦截实例属性修改操作的默认行为。比如将实例的属性存储在新的字典中,而不是存储在默认字典__dict__。
__delattr__此方法只影响实例。del self.x, delattr(self, ‘x’) 等涉及删除实例属性的操作时,如果定义了__delattr__,将会调用此方法。
__getattribute__此方法只影响实例。实例的所有属性的访问,第一个就调用此方法。此方法能完全控制属性的默认访问顺序。可以在此方法中做一些处理,然后手动抛出AttributeError异常,这将继续调用__getattr__方法(如果有的话)。

__getattr__ VS __getattribute__

两者的执行时间点不同。

前者会在默认属性搜索顺序中未找到属性时,拦截异常,并执行。

后者会在第一时间执行,完全拦截默认属性搜索顺序。两者执行顺序如下:

__getattribute__实例属性的默认搜索顺序__getattr__

举例:

class A:
    def __init__(self, x, y):
        
        # 用kv赋值方式增加属性 不会调用__setattr__
        self.__dict__['a'] = 7
        self.__dict__['_d'] = 
        
        # 如下属性将会存储到新字典_d 会调用__setattr__
        self.x = x
        self.y = y

    def __getattr__(self, item):
        print('_in getattr:', item)
        
        # 属性未找到时 才调用__getattr__ 并从新字典返回属性
        return self._d[item]

    def __setattr__(self, key, value):
        print('_in setattr:', key, value)
        
        # 下面写法都会递归 它们都调用__setattr__
        # self.key = value
        # setattr(self, str(key), value)
        
        # 用新字典存储属性
        # _d 属性在实例的__dict__,因为是字典操作,所以等号左边的self._d就不会调用__getattr__
        self._d[key] = value

    def __delattr__(self, item):
        print('_in delattr:', item)
        del self._d[item]

a = A(4, 5)
print(a.__dict__)  # 打印属性字典,不会调用A.__getattr__,除非定义了_getattribute__
print(a.x)
del a.x
delattr(a, 'y')
a.t = 123          # 这也会调__setattr__
print(a.__dict__)
print('=' * 30)

class A:
    d = 
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __getattribute__(self, item):
        print(item, '~~~~~~~')
        
        # 推荐写法
        return object.__getattribute__(self, item)
        
a = A(3, 4)
print('#' * 30)
print(a.x, a.d)

如果上面提及的魔术方法同时存在,会怎么样呢?详见 Python中的属性搜索顺序

描述器的应用

用描述器实现ClassMethod、StaticMethod(非数据描述器的应用):

# 非数据描述器 实现StaticMethod
class StaticMethod(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        print('_in StaticMethod')
        print(self, instance, owner)
        return self.fn


# 非数据描述器 实现ClassMethod
class ClassMethod(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        print('_in ClassMethod')
        print(self, instance, owner)
        return partial(self.fn, owner)  # 固定fn的owner参数,就是固定bar函数的所属类A2


class A2(object):
    AGE = 20

    def __init__(self, name, age):
        self.name = name
        self.__age = age

    # 装饰器语法
    @StaticMethod  # foo=StaticMethod(foo) 构建非数据描述器对象foo
    def foo(x):
        print('_in foo')
        return x
    
    # 装饰器语法
    @ClassMethod  # bar=ClassMethod(bar) 构建非数据描述器对象bar
    def bar(cls, x):
        print('_in bar')
        return cls.AGE, x
    
a = A2('Tom', 19)

# 这里分两步,a.foo读取属性foo,会调用__get__返回A2里定义的foo函数,这个函数其实是描述器的属性fn,
# 然后调函数fn(3),以此来实现静态方法foo
print(a.foo(3))
print('=' * 30)

# 这里分两步,a.bar读取属性bar,会调用__get__返回A2里定义的bar函数,这个函数其实是描述器的属性fn,
# 然后调函数fn(4),cls参数已固定,实现了类方法
print(a.bar(4))

用描述器实现Property(数据描述器的应用):

class Property(object):
    def __init__(self, fget, fset=None, fdel=None):
        self.fget = fget  # fget是A的方法age
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        # fget是self实例的属性,不是Property的方法,所以不会把self自动传递给fget,所以需要instance参数
        return self.fget(instance)

    def __set__(self, instance, value):
        self.fset(instance, value)

    def __delete__(self, instance):
        self.fdel(instance)

    def setter(self, fset):
        self.fset = fset
        return self  # 必须返回Property实例才能构建描述器

    def deleter(self, fdel):
        self.fdel = fdel
        return self


class A(object):
    def __init__(self):
        self.__age = 13

    # age=Property(age)=描述器对象
    # @Property必需在@age.setter与@age.deleter的前面
    # 因为@Property创建了描述器对象add,下面才能使用add对象
    @Property
    def age(self):
        return self.__age

    # age=age.setter(age)=描述器对象
    # 因为age是描述器对象,指向Property的实例,该实例有属性setter
    @age.setter
    def age(self, value):
        self.__age = value

    @age.deleter
    def age(self):
        del self.__age


a = A()
print(a.age)  # 把方法调用变成了属性访问
a.age = 17    # 调__set__
print(a.age)
del a.age     # 调__delete__

一文彻底搞懂python中的装饰器偏函数(代码片段)

装饰器要讲清楚装饰器,首先要知道一些前置概念。下文涉及到这些概念的地方,会展开讲述。什么是装饰器?装饰器是一种AOP(面向切面编程)的设计模式。面向对象编程往往需要通过继承或组合依赖等方... 查看详情

一文彻底搞懂python中的装饰器偏函数(代码片段)

装饰器要讲清楚装饰器,首先要知道一些前置概念。下文涉及到这些概念的地方,会展开讲述。什么是装饰器?装饰器是一种AOP(面向切面编程)的设计模式。面向对象编程往往需要通过继承或组合依赖等方... 查看详情

一文彻底搞懂docker中的namespace(代码片段)

什么是namespacenamespace是对全局系统资源的一种封装隔离。这样可以让不同namespace的进程拥有独立的全局系统资源。这样改变一个namespace的系统资源只会影响当前namespace中的进程,对其它namespace中的资源没有影响。以前Linux也... 查看详情

python入门到精通一文让你彻底搞懂python的函数(代码片段)

🚀作者:“大数据小禅”🚀粉丝福利:加入小禅的大数据社群🚀欢迎小伙伴们点赞👍、收藏⭐、留言💬目录Python中的函数及其调用对于函数的理解:python中的自定义函数自定义空函数Python特性之... 查看详情

一文彻底搞懂slam技术(代码片段)

什么是SLAM?SLAM (simultaneouslocalizationandmapping),也称为CML(ConcurrentMappingandLocalization),即时定位与地图构建,或并发建图与定位。问题可以描述为:将一个机器人放入未知环境中的未知位置,是否有办法让机器人一边逐步描... 查看详情

一文彻底搞懂slam技术(代码片段)

什么是SLAM?SLAM (simultaneouslocalizationandmapping),也称为CML(ConcurrentMappingandLocalization),即时定位与地图构建,或并发建图与定位。问题可以描述为:将一个机器人放入未知环境中的未知位置,是否有办法让机器人一边逐步描... 查看详情

一文彻底搞懂前端沙箱(代码片段)

什么是“沙箱”沙箱(Sandbox)[1]也称作:“沙箱/沙盒/沙盘”。沙箱是一种安全机制,为运行中的程序提供隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。沙箱能够安全的执行不受信... 查看详情

一文带你彻底搞懂docker中的cgroup(代码片段)

前言进程在系统中使用CPU、内存、磁盘等计算资源或者存储资源还是比较随心所欲的,我们希望对进程资源利用进行限制,对进程资源的使用进行追踪。这就让cgroup的出现成为了可能,它用来统一将进程进行分组࿰... 查看详情

java中的线程池如何实现,一文彻底搞懂(代码片段)

前言为什么要用线程池一键获取线程相关资料,还可获取最新java面试真题库在HotSpotVM的线程模型中,Java线程被一对一映射为内核线程。Java在使用线程执行程序时,需要调用操作系统内核的API,创建一个内核线程&... 查看详情

一文彻底搞懂leveldb架构(代码片段)

leveldbleveldb是一个写性能十分优秀的存储引擎,是典型的LSM-tree的实现。LSM的核心思想是为了换取最大的写性能而放弃掉部分读性能。那么,为什么leveldb写性能高?简单来说它就是尽量减少随机写的次数。leveldb首先将... 查看详情

一文彻底搞懂leveldb架构(代码片段)

leveldbleveldb是一个写性能十分优秀的存储引擎,是典型的LSM-tree的实现。LSM的核心思想是为了换取最大的写性能而放弃掉部分读性能。那么,为什么leveldb写性能高?简单来说它就是尽量减少随机写的次数。leveldb首先将... 查看详情

一文彻底搞懂leveldb架构(代码片段)

leveldbleveldb是一个写性能十分优秀的存储引擎,是典型的LSM-tree的实现。LSM的核心思想是为了换取最大的写性能而放弃掉部分读性能。那么,为什么leveldb写性能高?简单来说它就是尽量减少随机写的次数。leveldb首先将... 查看详情

一文彻底读懂python装饰器(代码片段)

装饰器主要用途是:不修改函数源码的前提下,添加额外的功能。如果你有Java开发经验,你会发现,Python中的装饰器其实就类似于Java的注解。好的,废话不多说,进入正题。我们假想如下一个场景:... 查看详情

一文彻底读懂python装饰器(代码片段)

装饰器主要用途是:不修改函数源码的前提下,添加额外的功能。如果你有Java开发经验,你会发现,Python中的装饰器其实就类似于Java的注解。好的,废话不多说,进入正题。我们假想如下一个场景:... 查看详情

一文彻底搞懂docker中的namespace(代码片段)

什么是namespacenamespace是对全局系统资源的一种封装隔离。这样可以让不同namespace的进程拥有独立的全局系统资源。这样改变一个namespace的系统资源只会影响当前namespace中的进程,对其它namespace中的资源没有影响。以前Linux也... 查看详情

一文彻底搞懂zookeeper(代码片段)

本文是基于CentOS7.9系统环境,进行Zookeeper的学习和使用1.Zookeeper简介1.1什么是ZookeeperZookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。本质上,就是文件系统+通知机制1.2Zookeeper工作机制Zookeepe... 查看详情

一文彻底搞懂zookeeper(代码片段)

本文是基于CentOS7.9系统环境,进行Zookeeper的学习和使用1.Zookeeper简介1.1什么是ZookeeperZookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。本质上,就是文件系统+通知机制1.2Zookeeper工作机制Zookeepe... 查看详情

flink总结之一文彻底搞懂时间和窗口(代码片段)

Flink总结之一文彻底搞懂时间和窗口文章目录Flink总结之一文彻底搞懂时间和窗口一、Flink中时间概念1.事件时间(EventTime)2.处理时间(ProcessingTime)3.摄入时间(IngestionTime)二、水位线(Watermark)1.... 查看详情