Python的描述器Descriptors

本文深入探讨Python的描述器概念,包括描述器的表现、属性访问顺序和在Python中的应用。描述器是一种特殊类型,通过实现`get`、`set`、`delete`方法来控制属性访问。数据描述器与非数据描述器的区别在于是否实现了`set`方法。描述器在类属性和实例属性的访问中起到关键作用,影响属性查找顺序。Python中的方法、staticmethod和classmethod都是描述器的实例,而property则是一个数据描述器,用于属性的保护和控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、描述器的表现

用到3个魔术方法: get() 、 set() 、 delete()
方法签名如下:
object.get(self, instance, owner)
object.set(self, instance, value)
object.delete(self, instance)
self 指代当前实例,调用者 ,instance 是owner的实例 ,owner 是属性的所属的类、也叫属主;

1、描述器的定义

Python中,一个类实现了 getsetdelete 三个方法中的任何一个方法,就是描述器,即描述器的类;要想把描述器用起来需把它赋给一个类属性,如果把它赋给实例没有效果、不会触发描述器机制、即不会触发__get__,这套机制就是这么设计的;
如果仅实现了 get ,就是非数据描述符 non-data descriptor;
同时实现了 getset 就是数据描述符 data descriptor;
如果一个类的类属性设置为描述器实例,那么它被称为owner属主

2、程序执行流程

类加载的时候,类变量需要先生成,而类B的x属性是类A的实例,所以类A先初始化,所以打印A.init;然后执行到打印B.x.a1;然后实例化并初始化B的实例b, 打印b.x.a1,会查找类属性b.x,指向A的实例,所以返回A实例的属性a1的值;
一旦通过访问类属性x,即访问A(),就会被__get__方法先拦截;要注意分析instance,是否触发描述器机制、即__get__方法,以及返回值;这算是一种黑魔法,因为通过属性描述器就能操作属主的类,功能强大;

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
​
print('-'*20)
print(B.x.a1)
print('='*20)
b = B()
print(b.x.a1)
​
​
# 运行结果
A.init
--------------------
a1
====================
B.init
a1

3、描述器

因为定义了 get 方法,类A就是一个描述器,对类B或者类B的实例的x属性读取,成为对类A的实例的访问, 就会调用 get 方法;

# 类A中实现 __get__ 方法
# 类调用描述器和实例调用描述器的区别
class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
​
print('-'*20)
print(B.x)   # 调用__get__方法,但instance为None,因为是类B调用x属性、并不是类B的实例
#print(B.x.a1) # 抛异常AttributeError: 'NoneType' object has no attribute 'a1'
​
print('='*20)
b = B()
print(b.x)  # 调用__get__方法,instance为b
#print(b.x.a1) # 抛异常AttributeError: 'NoneType' object has no attribute 'a1'
​
​
# 运行结果,抛异常AttributeError原因是__get__方法返回值为None
A.init
--------------------
A.__get__ <__main__.A object at 0x103e2aa20> None <class '__main__.B'>
None
====================
B.init
A.__get__ <__main__.A object at 0x103e2aa20> <__main__.B object at 0x103e2ab00> <class '__main__.B'>
None




# 类A中实现 __get__ 方法,解决__get__返回问题
class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
        return self   # 解决返回None的问题,返回一个A的实例
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
​
print('-'*20)
print(B.x)   # 调用__get__
print(B.x.a1)   # 调用__get__
​
print('='*20)
b = B()
print(b.x)   # 调用__get__
print(b.x.a1)   # 调用__get__
​
​
# 运行结果
A.init
--------------------
A.__get__ <__main__.A object at 0x10c439a20> None <class '__main__.B'>
<__main__.A object at 0x10c439a20>
A.__get__ <__main__.A object at 0x10c439a20> None <class '__main__.B'>
a1
​
====================
B.init
A.__get__ <__main__.A object at 0x10c439a20> <__main__.B object at 0x10c439b00> <class '__main__.B'>
<__main__.A object at 0x10c439a20>
A.__get__ <__main__.A object at 0x10c439a20> <__main__.B object at 0x10c439b00> <class '__main__.B'>
a1

以上的描述器均为类属性,但用类调用该类属性和实例调用该类属性表现不同,instance分别为None和具体实例;以下的描述器有所改动,改成实例属性;

# 进一步改动,类B的实例属性b也指向一个A的实例
class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
        return self   # 解决返回None的问题,返回一个A的实例
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
        self.x = A()  # 实例属性也指向一个A的实例
​
print('='*20)
b = B()
print(B.x)  # __get__
print()
print(b.x)  # 未调用__get__,因为自己字典有,不会触发__get__
print(b.x.a1)  # 未调用__get__,跟字典的搜索顺序不冲突
​
# 结果
A.init
====================
B.init
A.init
A.__get__ <__main__.A object at 0x103e5ebe0> None <class '__main__.B'>
<__main__.A object at 0x103e5ebe0>
​
<__main__.A object at 0x103e5ec50>
a1


# 进一步改动,类B的实例属性b也指向一个A的实例
class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
        return self   # 解决返回None的问题,返回一个A的实例
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
        self.b = A()  # 实例属性也指向一个A的实例
​
print('='*20)
b = B()
print(b.x)  # 触发__get__
print(b.x.a1)   # 触发__get__
​
print(b.b)  # 并没有触发__get__,只有类属性是A类的实例才行


# 运行结果
A.init
====================
B.init
A.init
A.__get__ <__main__.A object at 0x10948cb00> <__main__.B object at 0x10948cb38> <class '__main__.B'>
<__main__.A object at 0x10948cb00>
A.__get__ <__main__.A object at 0x10948cb00> <__main__.B object at 0x10948cb38> <class '__main__.B'>
a1
​
<__main__.A object at 0x10948cb70>

二、属性的访问顺序

当一个类的类属性是数据描述器,对该类的实例的同名属性操作相当于操作类属性;官方定义是围绕优先级:当一个类的类属性是数据描述器,对实例的该属性进行操作,实例属性字典的优先级会低于描述器优先级;
但打印字典就能看到本质:实例字典看不到该属性!

1、增加实例属性

属性查找顺序:实例的 dict 优先于 非数据描述器 ,数据描述器 优先于 实例的 dict
delete 方法有同样的效果,有了这个方法,也是数据描述器

# 为类B增加实例属性x,b.x访问到了实例的属性,而不是描述器
class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
        return self   
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
        self.x = 'b.x'  # 增加实例属性x
​
​
print('-'*20)
print(B.x)
print(B.x.a1)
​
print('='*20)
b = B()
print(b.x)
# print(b.x.a1) # AttributeError: 'str' object has no attribute 'a1'
​
print(B.__dict__)  # {'__module__': '__main__', 'x': <__main__.A object at 0x1079edc18>, '__init__': <function B.__init__ at 0x1079e58c8>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
print(b.__dict__)  # {}
​
​
# 运行结果
A.init
--------------------
A.__get__ <__main__.A object at 0x1060f6b38> None <class '__main__.B'>
<__main__.A object at 0x1060f6b38>
A.__get__ <__main__.A object at 0x1060f6b38> None <class '__main__.B'>
a1
====================
B.init
b.x

2、类A增加 set 方法

# 为类A增加__set__方法,访问到了描述器的数据
class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
        return self   
​
    def __set__(self, instance, value):
        print('A.__set__ {} {} {}'.format(self, instance, value))
        self.data = value
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
        self.x = 'b.x'  # 增加实例属性x
​
print('='*20)
b = B()
print(b.x)
print(b.x.a1) 
​
​
# 运行结果
A.init
====================
B.init
A.__set__ <__main__.A object at 0x1081adb38> <__main__.B object at 0x1081adb70> b.x
A.__get__ <__main__.A object at 0x1081adb38> <__main__.B object at 0x1081adb70> <class '__main__.B'>
<__main__.A object at 0x1081adb38>
A.__get__ <__main__.A object at 0x1081adb38> <__main__.B object at 0x1081adb70> <class '__main__.B'>
a1

数据描述器用处:对属性有保护作用,当用实例操作的属性是一个数据描述器时,无法更改,无法保存在实例字典中;以下b.x=500依然操作的是类属性,就会受描述器控制;

# 为类A增加__set__方法,访问到了描述器的数据
class A:
    def __init__(self, value='a1'):
        self.a1 = value
        print('A.init')
​
    def __get__(self, instance, owner):
        print("A.__get__ {} {} {}".format(self, instance, owner))
        return self   
​
    def __set__(self, instance, value):
        print('A.__set__ {} {} {}'.format(self, instance, value))
        self.data = value
​
class B:
    x = A()
    def __init__(self):
        print('B.init')
        self.x = A('abc')   # 增加实例属性x
​
print('='*20)
print(B.x)
print(B.x.a1)
​
b = B()  # 右边触发A.init,self.x触发 A.__set__,value为一个新的A实例、
​
print(b.x)
print(b.x.a1) 
​
print(B.__dict__)
print(b.__dict__)
​
# B.x = 500
# print(B.x)   # 500 赋值即定义,描述器已经没有,故不会触发
​
b.x = 500   # 触发__set__
print(b.x)   # 不会显示500,会触发__get__,所以实例操作的是一个数据描述器时,无法更改
​
​
# 运行结果
A.init
====================
A.__get__ <__main__.A object at 0x108effc18> None <class '__main__.B'>
<__main__.A object at 0x108effc18>
A.__get__ <__main__.A object at 0x108effc18> None <class '__main__.B'>
a1
​
B.init
A.init
A.__set__ <__main__.A object at 0x108effc18> <__main__.B object at 0x108effc50> <__main__.A object at 0x108effc88>
​
A.__get__ <__main__.A object at 0x108effc18> <__main__.B object at 0x108effc50> <class '__main__.B'>
<__main__.A object at 0x108effc18>
A.__get__ <__main__.A object at 0x108effc18> <__main__.B object at 0x108effc50> <class '__main__.B'>
a1
​
{'__module__': '__main__', 'x': <__main__.A object at 0x108effc18>, '__init__': <function B.__init__ at 0x108ef88c8>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
{}
​
A.__set__ <__main__.A object at 0x10b8c7a58> <__main__.B object at 0x10b8c7c18> 500
A.__get__ <__main__.A object at 0x10b8c7a58> <__main__.B object at 0x10b8c7c18> <class '__main__.B'>
<__main__.A object at 0x10b8c7a58>

本质:并不是数据描述器优先极高,而是把实例属性从__dict__中去掉了,造成了该属性如果是数据描述器就优先访问类属性的假象,即属性访问顺序从没变过,只不过在写实例__dict__前做了拦截,第一个拦截的是__getattribute__,优先级最高,其次是数据描述器;

三、Python中的描述器

描述器在Python中应用非常广泛;Python的方法(包括staticmethod()和classmethod())都实现为非数据描述器;因此,实例可以重新定义和覆盖方法,这允许单个实例获取与同一类的其他实例不同的行为;

property()函数实现为一个数据描述器;因此,实例不能覆盖属性的行为;
Python中所有的方法、包括魔术方法都是非数据描述器,实例可以覆盖;除了属性装饰器property本质上是一个类,它是一个数据描述器;一切皆对象,对象的方法都有描述器;

# foo、bar都可以在实例中覆盖,但是z不可以
class A:
    @classmethod
    def foo(cls): # 非数据描述器,可以从'foo': <classmethod object at 0x105bcdb38>看出foo是一个类对象,里面实现了__get__方法
        pass
​
    @staticmethod # 非数据描述器 
    def bar():
        pass
​
    @property # 数据描述器 
    def z(self):
        return 5
​
    def getfoo(self): # 非数据描述器
        return self.foo
​
    def __init__(self): # 非数据描述器 
        self.foo = 100
        self.bar = 200
        #self.z = 300
​
a = A()
print(a.__dict__)  # {'foo': 100, 'bar': 200}
print(A.__dict__)  # {'__module__': '__main__', 'foo': <classmethod object at 0x105bcdb38>, 'bar': <staticmethod object at 0x105bcdb70>, 'z': <property object at 0x1056a4d68>, 'getfoo': <function A.getfoo at 0x105bc68c8>, '__init__': <function A.__init__ at 0x105bc6950>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

1、staticmethod装饰器

# 类staticmethod装饰器
class StaticMethod: # 怕冲突改名 
    def __init__(self, fn):
        print(fn)
        self._fn = fn
​
    def __get__(self, instance, owner):
        return self._fn
​
class A:
    @StaticMethod
    def stmtd():  # stmtd = StaticMethod(stmtd) 类属性stmtd是一个描述器
        print('static method')
​
f = A.stmtd
print(1,f)
f()  # static method
A().stmtd()  # static method

2、classmethod装饰器

# 类classmethod装饰器
from functools import partial
import inspect
​
class ClassMethod: # 怕冲突改名
    def __init__(self, fn):
        self._fn = fn
​
    def __get__(self, instance, cls):
        ret = partial(self._fn, cls)
        print('newfn signature', inspect.signature(ret))
        return ret  # 返回是一个函数
​
class A:
    @ClassMethod   # 调用是A.clsmtd() 或者 A().clsmtd() 
    def clsmtd(cls):  # clsmtd = ClassMethod(clsmtd)
        print(cls.__name__)
​
print(A.__dict__)  # {'__module__': '__main__', 'clsmtd': <__main__.ClassMethod object at 0x10e022940>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
​
A.clsmtd()
A().clsmtd()
​
# newfn signature ()
# A
# newfn signature ()
# A

3、对实例数据进行校验

# 对类的实例的属性name、age进行数据校验 
class Person:
    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age

思路:
写函数,在 init 中先检查,如果不合格,直接抛异常;方法耦合度太高
装饰器,使用inspect模块完成
描述器,数据描述器

# 函数装饰器
import inspect
​
class TypeCheck:
    def __init__(self, name, type):
        self.name = name
        self.type = type
​
    def __get__(self, instance, owner):
        if instance is not None:
            return instance.__dict__[self.name]
        return self
​
    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError(value)
        instance.__dict__[self.name] = value
​
def typeassert(cls):
    params = inspect.signature(cls).parameters
    print(params)  # OrderedDict([('name', <Parameter "name: str">), ('age', <Parameter "age: int">)])
    for name, param in params.items():
        if param.annotation != param.empty: # 注入类属性 
            setattr(cls, name, TypeCheck(name, param.annotation))
    return cls
​
@typeassert
class Person:
    # name = TypeCheck('name', str) # 由装饰器注入类属性
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age
​
p = Person('tom', 20)
p1 = Person('jerry', '20')



# 类装饰器
import inspect
​
class TypeCheck:
    def __init__(self, name, type):
        self.name = name
        self.type = type
​
    def __get__(self, instance, owner):
        if instance is not None:
            return instance.__dict__[self.name]
        return self
​
    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError(value)
        instance.__dict__[self.name] = value
​
class TypeAssert:
    def __init__(self, cls):
        self.cls = cls
        params = inspect.signature(cls).parameters
        print(params)  # OrderedDict([('name', <Parameter "name: str">), ('age', <Parameter "age: int">)])
        for name, param in params.items():
            if param.annotation != param.empty: # 注入类属性 
                setattr(cls, name, TypeCheck(name, param.annotation))
​
    def __call__(self, name, age):
        return self.cls(name, age) # 重新构建一个新的Person对象
​
@TypeAssert
class Person:
    # name = TypeCheck('name', str) # 由装饰器注入 
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age
​
    def __str__(self):
        return "{} is {}".format(self.name, self.age)
​
p1 = Person('tom', 18)
print(id(p1))  # 4448889936
p2 = Person('tom', 20)
print(id(p2))  # 4443970248
p3 = Person('tom', '20')



# 类装饰器
import inspect
​
class Typed:
    def __init__(self, type):
        self.type = type
​
    def __get__(self, instance, cls):
        pass
​
    def __set__(self, instance, value):
        print('Typed set', self, instance, value)
        if not instance(value, self.type):
            raise ValueError(value)
​
class TypeAssert:
    def __init__(self, cls):
        self.cls = cls
​
    def __call__(self, name, age):
        params = inspect.signature(self.cls).parameters
        print(params)  # OrderedDict([('name', <Parameter "name: str">), ('age', <Parameter "age: int">)])
        for name, param in params.items():
            print(name, param.annotation)  # name <class 'str'>   age <class 'int'>
            if param.annotation != param.empty:
                setattr(self.cls, name, Typed(param.annotation))
​
@TypeAssert
class Person:   # Person = TypeAssert(Person)
    # name = Typed(str)
    # age = Typed(int)  用setattr(self.cls, name, Typed(param.annotation))动态创建
​
    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age
​
p1 = Person('tom', 18)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值