Python0904-描述器

描述器的表现

用到3个魔术方法:get(),set(),delete()
- 方法签名如下
- object.get(self, instance, owner)
- object.set(self, instance, value)
- object.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('-' * 20)
print(B.x.a1)

print('=' * 20)
b = B()
print(b.x.a1)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A.init
--------------------
a1
====================
B.init
a1

可以看出执行的先后顺序吧?
类加载的时候,类变量需要先生成,而类B的x属性是类A的实例,所以类A先初始化,所以打印A.init
然后执行到打印B.x.a1.
然后实例化并初始化B的实例b
打印b.x.a1,会查找类属性b.x,指向A的实例,所以返回A实例的属性a1的值。

看懂执行流程了,再看下面的程序,对类A做一些改造。
如果在类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)
# print(B.x.a1)  # 抛异常 AttributeError: 'NoneType' object has no attribute 'a1'

print('=' * 20)
b = B()
print(b.x)
# print(b.x.a1)  # 抛异常 AttributeError: 'NoneType' object has no attribute 'a1'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A.init
--------------------
A.__get__ <__main__.A object at 0x000001F29574A748> None <class '__main__.B'>
None
====================
B.init
A.__get__ <__main__.A object at 0x000001F29574A748> <__main__.B object at 0x000001F29574A828> <class '__main__.B'>
None

因为定义了get方法,类A就是一个描述器,对类B或者类B的实例的x属性读取,成为对类A的实例的访问,就会调用get方法
如何解决上例中访问报错的问题, 问题应该来自get方法。
- self, instance, owner这三个参数,是什么意思?
- <main.A object at 0x000001F29574A748> None

None表示没有B类的实例,对应调用B.x
<__main__.B object at 0x000001F29574A828>表示是B的实例,对应调用B().x

使用返回值解决。返回self,就是A的实例,该实例有a1属性,返回正常。

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的问题


class B:
    x = A()
    def __init__(self):
        print('B.init')


print('-' * 20)
print(B.x)
print(B.x.a1)

print('=' * 20)
b = B()
print(b.x)
print(b.x.a1)

# print(b.b)  # 并没有触发__get__  AttributeError: 'B' object has no attribute 'b'
~~~~~~~~~~~~~~~~~~~~~~~~~~~
A.init
--------------------
A.__get__ <__main__.A object at 0x00000214FBFDA748> None <class '__main__.B'>
<__main__.A object at 0x00000214FBFDA748>
A.__get__ <__main__.A object at 0x00000214FBFDA748> None <class '__main__.B'>
a1
====================
B.init
A.__get__ <__main__.A object at 0x00000214FBFDA748> <__main__.B object at 0x00000214FBFDA828> <class '__main__.B'>
<__main__.A object at 0x00000214FBFDA748>
A.__get__ <__main__.A object at 0x00000214FBFDA748> <__main__.B object at 0x00000214FBFDA828> <class '__main__.B'>
a1

从运行结果可以看出,只有类属性是类的实例才行。

描述器定义

Python中,一个类实现了get,set, delete三个方法中的任何一个方法,就是描述器。
如果仅实现了get,就是非数据描述符non-date descriptor
同时实现了get,set就是数据描述符date descripotor
如果一个类的类属性设置为描述器,那么它被称为owner属性。

属性的访问顺序

为上例中的类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'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A.init
--------------------
A.__get__ <__main__.A object at 0x000002C283F3A7B8> None <class '__main__.B'>
<__main__.A object at 0x000002C283F3A7B8>
A.__get__ <__main__.A object at 0x000002C283F3A7B8> None <class '__main__.B'>
a1
====================
B.init
b.x

b.x访问到了实例的属性,而不是描述器。
继续修改代码,为类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.date = value


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)  # 返回a1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A.init
--------------------
A.__get__ <__main__.A object at 0x000001EC8FE1A7F0> None <class '__main__.B'>
<__main__.A object at 0x000001EC8FE1A7F0>
A.__get__ <__main__.A object at 0x000001EC8FE1A7F0> None <class '__main__.B'>
a1
====================
B.init
A.__set__ <__main__.A object at 0x000001EC8FE1A7F0> <__main__.B object at 0x000001EC8FE1A898> b.x
A.__get__ <__main__.A object at 0x000001EC8FE1A7F0> <__main__.B object at 0x000001EC8FE1A898> <class '__main__.B'>
<__main__.A object at 0x000001EC8FE1A7F0>
A.__get__ <__main__.A object at 0x000001EC8FE1A7F0> <__main__.B object at 0x000001EC8FE1A898> <class '__main__.B'>
a1

返回变成了a1,访问到了描述器的数据。
属性查找顺序
实例的dict优先于非数据描述器
数据描述器优先于实例的dict
delete方法有同样的效果,有了这个方法,就是数据描述器
尝试着增加下面的代码,看看字典的变化
b.x = 500
B.x = 600
b.x = 500 ,这是调用数据描述器的set方法,或调用非数据描述器的实例覆盖
B.x = 600 ,赋值即定义,这是覆盖类属性

本质(进阶)

Python真的会做的这么复杂吗,再来一套属性查找顺序规则?看看非数据描述器和数据描述器,类B及其dict的变化。
屏蔽和不屏蔽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.date = value


class B:
    x = A()
    def __init__(self):
        print('B.init')
        self.x = 'b.x'  # 增加实例属性x
        self.y = 'b.y'


print('-' * 20)
print(B.x)
print(B.x.a1)

print('=' * 20)
b = B()
print(b.x)
# print(b.x.a1)

print(b.y)
print('字典')
print(b.__dict__)
print(B.__dict__)
# 屏蔽__set__方法结果如下
{'y': 'b.y', 'x': 'b.x'}
{'__init__': <function B.__init__ at 0x000001E88FC71B70>, '__module__': '__main__', '__doc__': None, 'x': <__main__.A object at 0x000001E88FC7A748>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>}

# 不屏蔽__set__方法结果如下
{'y': 'b.y'}
{'__dict__': <attribute '__dict__' of 'B' objects>, '__module__': '__main__', 'x': <__main__.A object at 0x000001E14AF6A780>, '__doc__': None, '__init__': <function B.__init__ at 0x000001E14AF61BF8>, '__weakref__': <attribute '__weakref__' of 'B' objects>}

原来不是什么数据描述器优先级高,而是把实例的属性从dict中给去除了,造成了该属性如果是数据描述器有限访问的假象
说到底,属性访问顺序从来就没有变过。

Python中的描述器

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

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

class A:
    @classmethod
    def foo(cls):  # 非数据描述器
        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__)
print(A.__dict__)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{'bar': 200, 'foo': 100}
{'getfoo': <function A.getfoo at 0x000001A5989F1BF8>, 'bar': <staticmethod object at 0x000001A5989FA898>, '__init__': <function A.__init__ at 0x000001A5989F1C80>, 'z': <property object at 0x000001A5989F84A8>, '__doc__': None, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'A' objects>, 'foo': <classmethod object at 0x000001A5989FA860>, '__dict__': <attribute '__dict__' of 'A' objects>}

foo, bar 都可以在实例中覆盖,但是z不可以

练习

实现StaticMethod装饰器,完成staticmethod装饰器的功能

# 类staticmethod装饰器
class StaticMethod:  # 怕冲突改名
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        return self.fn


class A:
    @StaticMethod
    def stmtd():
        print('static method')


A.stmtd()
A().stmtd()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static method
static method

实现ClassMethon装饰器,完成staticmethod装饰器的功能

from functools import partial

# 类classmethod装饰器
class ClassMethod:  # 怕冲突改名
    def __init__(self, fn):
        self._fn = fn

    def __get__(self, instance, owner):
        return self._fn(owner)


class A:
    @ClassMethod
    # clsmtd = ClassMethod(clsmtd)
    # 调用A.clsmtd()或者A().clsmtd()
    def clsmtd(cls):
        print(cls.__name__)


print(A.__dict__)
A.clsmtd
A.clsmtd()

A.clsmtd()的意思就是None(),一定报错。怎么修改?
A.clsmtd()其实就应该是A.clstmd(cls)(), 应该怎么处理?
A.clsmtd = A.clsmtd(cls)
应该用partial函数

from functools import partial

# 类classmethod装饰器
class ClassMethod:  # 怕冲突改名
    def __init__(self, fn):
        self._fn = fn

    def __get__(self, instance, cls):
        return partial(self._fn, cls)


class A:
    @ClassMethod
    def clsmtd(cls):
        # clsmtd = ClassMethod(clsmtd)
        # 调用A.clsmtd()或者A().clsmtd()
        print(cls.__name__)


print(A.__dict__)
A.clsmtd
A.clsmtd()
~~~~~~~~~~~~~~~~~~~~~~~
{'__doc__': None, '__dict__': <attribute '__dict__' of 'A' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'A' objects>, 'clsmtd': <__main__.ClassMethod object at 0x000001992470A748>}
A

对实例的数据进行校验

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

对上面的类的实例的属性name,age进行数据校验

思路
  1. 写函数,在init中先检查,如果不合格,直接抛异常
  2. 装饰器,使用imspect模块完成
  3. 描述器
class Person:
    def __init__(self, name:str, age:int):
        params = ((name, str), (age, str))
        if not self.checkdate(params):
            raise TypeError()
        self.name = name
        self.age = age

    def checkdate(self, params):
        for p, t in params:
            if not isinstance(p, t):
                return False
            return True


p = Person('tom', '20')

这种方法耦合度太高。
装饰器的方法,前面写过类似的,这里不再赘述。

描述器方法

需要使用数据描述器,写入实例属性的时候做检查

class Typed:
    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):  # TODO 报错
            raise TypeError(value)
        instance.__dict__[self.name] = value


class Person:
    name = Typed('name', str)  # 不优雅
    age = Typed('age', int)  # 不优雅

    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age


p = Person('tom', '20')
~~~~~~~~~~~~~~~~~~~~~~
    raise TypeError(value)
TypeError: 20

代码看似不错,但是有硬解码,能否直接获取形参类型,使用inspect模块

使用inspect模块

先做个试验
params = inspect.signature(Person).parameters
看看返回什么结果
完整代码如下

class Typed:
    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


import inspect
def typeassert(cls):
    params = inspect.signature(cls).parameters
    print(params)
    for name, param in params.items():
        print(param.name, param.annotation)
        if param.annotation != param.empty:  # 注入类属性
            setattr(cls, name, Typed(name, param.annotation))
    return cls


@typeassert
class Person:
    # name = Typed('name', str)  # 装饰器注入
    # age = Typed('age', int)

    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age

    def __repr__(self):
        return "{} is {}".format(self.name, self.age)


# p = Person('tom', '20')  # TypeError: 20
p = Person('tom', 20)
print(p)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OrderedDict([('name', <Parameter "name:str">), ('age', <Parameter "age:int">)])
name <class 'str'>
age <class 'int'>
tom is 20
函数装饰器改为类装饰器
class Typed:
    def __init__(self, type):
        self.type = type

    def __get__(self, instance, owner):
        pass

    def __set__(self, instance, value):
        print('T.set', self, instance, value)
        if not isinstance(value, self.type):
            raise ValueError(value)


import inspect
class TypeAssert:
    def __init__(self, cls):
        self.cls = cls  # 记录着被包装的Person类
        params = inspect.signature(self.cls).parameters
        print(params)
        for name, param in params.items():
            print(name, param.annotation)
            if param.annotation != param.empty:  # 注入类属性
                setattr(self.cls, name, Typed(param.annotation))
        print(self.cls.__dict__)


    def __call__(self, name, age):
        p = self.cls(name, age)  # 重新构建一个新的Person对象
        return p


@TypeAssert  # Person = TypeAssert(Person)
class Person:
    # name = Typed(str)
    # age = Typed(int)

    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age


p1 = Person('tom', 18)
print(id(p1))
p2 = Person('tom', 20)
print(id(p2))
p3 = Person('tom', '20')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OrderedDict([('name', <Parameter "name:str">), ('age', <Parameter "age:int">)])
Traceback (most recent call last):
name <class 'str'>
  File "C:/python10/code/Python09/0901.py", line 46, in <module>
age <class 'int'>
{'__doc__': None, '__dict__': <attribute '__dict__' of 'Person' objects>, '__init__': <function Person.__init__ at 0x00000240FF32F158>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, 'age': <__main__.Typed object at 0x00000240FF1436D8>, '__module__': '__main__', 'name': <__main__.Typed object at 0x00000240FF12AB00>}
T.set <__main__.Typed object at 0x00000240FF12AB00> <__main__.Person object at 0x00000240FF143F28> tom
T.set <__main__.Typed object at 0x00000240FF1436D8> <__main__.Person object at 0x00000240FF143F28> 18
2478180679464
T.set <__main__.Typed object at 0x00000240FF12AB00> <__main__.Person object at 0x00000240FF1436A0> tom
T.set <__main__.Typed object at 0x00000240FF1436D8> <__main__.Person object at 0x00000240FF1436A0> 20
2478180677280
T.set <__main__.Typed object at 0x00000240FF12AB00> <__main__.Person object at 0x00000240FF34AB38> tom
T.set <__main__.Typed object at 0x00000240FF1436D8> <__main__.Person object at 0x00000240FF34AB38> 20
    p3 = Person('tom', '20')
  File "C:/python10/code/Python09/0901.py", line 28, in __call__
    p = self.cls(name, age)  # 重新构建一个新的Person对象
  File "C:/python10/code/Python09/0901.py", line 39, in __init__
    self.age = age
  File "C:/python10/code/Python09/0901.py", line 11, in __set__
    raise ValueError(value)
ValueError: 20
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值