魔术方法之描述器

描述器Descriptors

描述器的表现

用到3个方法:__get__()``__set__()``__delete__()

方法签名如下:

  • obj__get__(self,instance,owner)
  • obj__set__(self,instance,value)
  • obj__delete__(self,instance)
  • self指代当前实例,调用者
  • instance时owner的实例
  • owner是属性所属的类

请思考下面代码执行流程是什么?

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')
    
class B:
    x = A()
    def __init__(self):
        print('B.init')
        
print('-' * 30)
b = B()
print(b.x.a1,B.x.a1)
        
#执行结果如下
A.init
------------------------------
B.init
a1 a1

类加载的时候变量需要先生成,而类B的x属性是类A的实例,所以类A先初始化,所以打印A.init。实例化b后,该实例和类都可以成功访问到x指向的实例。

如果在类A中实现了__get__()方法,看看变化

class A:
    def __init__(self):
        self.a1 = 'a1'
        
    def __get__(self,instance,owner):
        print(self,instance,owner)
    
class B:
    x = A()
    def __init__(self):
        pass
        
b = B()
print(B.x)
print(b.x)
        
#执行结果如下
<__main__.A object at 0x0000013A521D8F98> None <class '__main__.B'>
None
<__main__.A object at 0x0000013A521D8F98> <__main__.B object at 0x0000013A521D8EF0> <class '__main__.B'>
None

因为定义了__get__()方法,类A就是一个描述器,使用类B或者类B的实例来对属性读取,就是对类A的实例的访问,就会调用该方法。如果需要通过该方法调用self实例的属性,则需要return self。当用类B访问时,instance的值为None,用类B的实例访问时,instance的值为类B的实例

那么类B的实例属性也可以这样吗?

class A:
    def __init__(self):
        self.a1 = 'a1'
        
    def __get__(self,instance,owner):
        print(self,instance,owner)
        return self
    
class B:
    x = A()
    def __init__(self):
        self.x = A()
        self.b1 = A()
        
b = B()
print(B.x.a1)
print(b.x.a1)
print(b.b1.a1)

#执行结果如下
<__main__.A object at 0x0000013A521D1908> None <class '__main__.B'>
a1
a1
a1

由此可见,只有B类属性是A类的实例才行。

描述器定义

python中,一个类实现了__get__()__set__()__delete__()三个方法中的任何一个方法,就是描述器。实现这三个中的某些方法,就支持了描述器协议。

  • 仅实现了__get__(),就是非数据描述器 non-data descriptor
  • 实现了__get__()__set__()或者__delete__()就是数据描述器 data descriptor

如果一个类的类属性设置为描述器实例,那么它就被称为owner属主(属主类)。
当该类的该属性被查找,设置,删除时,就会调用描述器相应的方法。

属性的访问顺序

先为类A增加__set__()方法。

class A:
    def __init__(self):
        self.a1 = 'a1'
        
    def __get__(self,instance,owner):
        #print(self,instance,owner)
        return self
    
    def __set__(self,instance,value):
        print('set----------',self,instance,value)
        
class B:
    x = A()
    def __init__(self):
        self.b1 = 5
        self.x = 5
        
b = B()
print(b.__dict__)

#执行结果如下
set---------- <__main__.A object at 0x0000013A521C3F98> <__main__.B object at 0x0000013A5223D0B8> 5
{'b1': 5}

为类A增加__set__()方法后,类B的实例创建属性时,因为实例属性x和类B属性x同名,结果被类A的set方法拦截下来,所以看到实例b的字典里只有b1属性。

由此得出实例属性查找优先级:
数据描述器 > 实例字典 > 非数据描述器

注意:
b.x = 500,该方法会调用描述器的set
B.x = 500,该方法重新定义了类的x属性,之前的描述器实例就被覆盖

__delete__()方法有同样的效果,有了这个方法,也是数据描述器。

class A:
    def __init__(self):
        self.a1 = 'a1'
        
    def __get__(self,instance,owner):
        return self
    
    def __delete__(self,instance):
        print('del',self,instance)
        
        
class B:
    x = A()
    def __init__(self):
        pass
        
b = B()
del b.x
del B.x
B.__dict__
#执行结果如下
del <__main__.A object at 0x0000022E62491DD8> <__main__.B object at 0x0000022E62491EF0>
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.B.__init__(self)>,
              '__dict__': <attribute '__dict__' of 'B' objects>,
              '__weakref__': <attribute '__weakref__' of 'B' objects>,
              '__doc__': None})

上例得出,删除B.x后,类B的字典里就没有x属性了,所以描述器只有__get__方法对属主类调用起作用。

python中的描述器

描述器在python中应用非常广泛

python的方法(包括staticmethod和classmethod)都实现了非数据描述器。因此,实例可以重新定义和覆盖方法,这允许单个实例获取以同一类的其他实例不同的行为。

property()函数实现了一个数据描述器,因此,实例不能覆盖属性的行为。

class A:
    @classmethod
    def f1():#非数据描述器
        pass
    
    @staticmethod
    def f2():#非数据描述器
        pass
    
    def f3():#非数据描述器
        pass
    
    @property
    def f4():#-----数据描述器-----
        pass
    
    def __init__(self):#非数据描述器
        self.f1 = 1
        self.f2 = 2
        self.f3 = 3
        #self.f4 = 4#报属性错误AttributeError: can't set attribute
        
a = A()
print(A.__dict__)
print(a.__dict__)

##执行结果如下
{'__module__': '__main__', 'f1': <classmethod object at 0x0000013A5223D208>, 'f2': <staticmethod object at 0x0000013A5223D828>, 'f3': <function A.f3 at 0x0000013A521D6B70>, 'f4': <property object at 0x0000013A52236A48>, '__init__': <function A.__init__ at 0x0000013A521F4B70>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'f1': 1, 'f2': 2, 'f3': 3}

新增方法

3.6新增描述器方法__set_name__,它在属主类构建的时候就会调用。

class A:
    def __init__(self):
        self.a1 = 'a1'
        
    def __get__(self,instance,owner):
        return self
    
    def __set_name__(self,owner,name):
        print(self,owner,name)
        
        
class B:
    x = A()
    def __init__(self):
        pass
        
b = B()
##执行结果如下
<__main__.A object at 0x0000013A522322E8> <class '__main__.B'> x

提供该方法,就是可以知道属主类和属主类的类属性名

练习

1.实现StaticMethod装饰器

from functools import wraps
class StaticMethod:
    def __init__(self, fn ):
        self.fn = fn
        wraps(fn)(self)
        
    def __get__(self,instance,owner):
        return self.fn

class Person:
    @StaticMethod#该装饰器将foo提取出作为实参传入,并返回一个实例
    def foo(x, y ):#foo = StaticMethod(foo)  => StaticMethod instance
        print('foo',x + y)

p = Person()
Person.foo(1,2)
p.foo(3,4)

##执行结果如下
foo 3
foo 7

2.实现ClassMethod装饰器

from functools import partial
class ClassMethod:
    def __init__(self,fn):
        self.fn = fn
    
    def __get__(self,instance,owner):
        return partial(self.fn,owner)
    
class Person:
    @ClassMethod#该装饰器将foo提取出作为实参传入,并返回一个实例
    def foo(cls,x, y ):#foo = StaticMethod(foo)  => StaticMethod instance
        print('foo',cls.__name__,x + y)

p = Person()
p.foo(5,6)

##执行结果如下
foo Person 11

3.对下列实例的数据进行校验(使用装饰器和描述器)

class Person:
	def __init__(self,name:str,age:int):
		self.name = name
		self.age = age
使用硬方法
class Person:
    def __init__(self,name:str,age:int):
        tar = ((name,str),(age,int))
        for n,t in tar:
            if not isinstance(n,t):
                print(f'{n} TypeError')
                #raise TypeError(f'{n}')
        self.name = name
        self.age = age
        
p = Person(18,'tom')
p.__dict__
print('-' * 30)
p1 = Person('tom',18)
p1.__dict__
##执行结果如下
18 TypeError
tom TypeError
------------------------------
{'name': 'tom', 'age': 18}

这样把代码写死了,不好!

使用描述器
class AttrCheck:
    def __init__(self,name,typ):
        self.name = name
        self.typ = typ
    
    def __get__(self,instance,owner):
        if not instance:
            raise TypeError(f'access denied: {owner.__name__}')
        return instance.__dict__[self.name]
        
    def __set__(self,instance,value):
        print('target:{}--actual:{} value:{}'.format(self.typ,type(value),value))
        if isinstance(value,self.typ):
            instance.__dict__[self.name] = value

class Person:
    name = AttrCheck('name',str)
    age = AttrCheck('age',int)
    def __init__(self,name:str,age:int):
        self.name = name
        self.age = age

p = Person(18,'tom')
print('-' * 30)
p1 = Person('tom',18)
print('-' * 30)
print(p1.__dict__)
p1.name
#Person.name#get方法中实现了通过属主类访问异常所以,该代码报错
##执行结果如下
target:<class 'str'>--actual:<class 'int'> value:18
target:<class 'int'>--actual:<class 'str'> value:tom
------------------------------
target:<class 'str'>--actual:<class 'str'> value:tom
target:<class 'int'>--actual:<class 'int'> value:18
------------------------------
{'name': 'tom', 'age': 18}
'tom'
使用装饰器函数加描述器
import inspect

class AttrCheck:
    def __init__(self,name,typ):
        self.name = name
        self.typ = typ
    
    def __get__(self,instance,owner):
        if not instance:
            raise TypeError(f'access denied: {owner.__name__}')
        return instance.__dict__[self.name]
        
    def __set__(self,instance,value):
        print('target:{}--actual:{} value:{}'.format(self.typ,type(value),value))
        if isinstance(value,self.typ):
            instance.__dict__[self.name] = value

def attrchect(cls):
    sig = inspect.signature(cls)
    parm = sig.parameters
    for v in parm.values():
        if not v.annotation is inspect._empty:
            print(v.name,v.annotation)
            setattr(cls,v.name,AttrCheck(v.name,v.annotation))
    return cls

@attrchect        
class Person:
    def __init__(self,name:str,age:int):
        self.name = name
        self.age = age
Person.__dict__
p = Person(18,'tom')
print(p.__dict__)
print('-' * 30)
p1 = Person('tom',18)
print(p1.__dict__)
p1.name,p1.age
##执行结果如下
name <class 'str'>
age <class 'int'>
target:<class 'str'>--actual:<class 'int'> value:18
target:<class 'int'>--actual:<class 'str'> value:tom
{}
------------------------------
target:<class 'str'>--actual:<class 'str'> value:tom
target:<class 'int'>--actual:<class 'int'> value:18
{'name': 'tom', 'age': 18}
('tom', 18)

通过装饰器函数给类Person动态添加了两个属性(属性名根据init函数签名获得),这样就避免了写死代码

nt(p1.dict)
p1.name,p1.age
##执行结果如下
name <class ‘str’>
age <class ‘int’>
target:<class ‘str’>–actual:<class ‘int’> value:18
target:<class ‘int’>–actual:<class ‘str’> value:tom
{}

target:<class ‘str’>–actual:<class ‘str’> value:tom
target:<class ‘int’>–actual:<class ‘int’> value:18
{‘name’: ‘tom’, ‘age’: 18}
(‘tom’, 18)


通过装饰器函数给类Person动态添加了两个属性(属性名根据init函数签名获得),这样就避免了写死代码



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值