27 - 面向对象高级-描述器

1 描述器

        一般来说,一个描述器是一个有绑定行为的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和 __delete__() 。

有这些方法的对象叫做描述器。

        默认对属性的访问控制是从对象的字典里面(__dict__)中获取、设置和删除它。举例来说, 比如 a.x 的查找顺序是, a.__dict__['x'] , 然后 type(a).__dict__['x'] , 然后找 type(a) 的父类(不包括元类(metaclass)). 如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type 或者 object 的类)。

2 描述器协议

描述器主要涉及三个方法:

  1. descr.__get__(self, obj, type=None) --> value
  2. descr.__set__(self, obj, value) --> None
  3. descr.__delete__(self, obj) --> None

一个对象具有其中任一个方法就会成为描述器,从而在被当作对象属性时重写默认的查找、设置和删除行为。

2.1 非数据描述器

在类中仅仅定义了__get__方法的描述器被称为非数据描述器(non-data descriptor)。

非数据描述器的优先级低于实例的__dict__。

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

    def __get__(self, instance, owner):
        pass
        # return self
class B:
    x = A()

    def __init__(self):
        print('B.init')

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


# Traceback (most recent call last):

# A.init

# --------------------

# B.init

#   File "E:/Python - base - code/ClassDesc.py", line 20, in <module>

#     print(b.x.a1)

# AttributeError: 'NoneType' object has no attribute 'a1'

分析:

  1. Class A实现了__get__方法,所以它是一个非数据描述器
  2. 由于Class B里面设置的x属性是Class A的实例,所以在定义阶段就会实例化,把实例化的对象赋给x属性,所以会先执行A的__init__方法。
  3. 访问实例b的x属性时,发现值是一个描述器,然后就会被描述器A的__get__方法捕获
  4. __get__方法默认然会None,所以None对象没有a1属性,所以报属性错误
  5. 在__get__方法中,return self就可以访问了

那么self是什么,__get__方法的参数都是什么意思:

  • self:对应A的实例(这里是属性x)
  • owner:对应的是x属性的拥有者,也就是B类
  • instance:它的值有两个
    • 当使用owner类直接调用时,它是None
    • 当使用owner类的实例调用是,是实例本身

2.1.1 实例分析

下面是小例子,分析代码结果

class Person:

    def __init__(self):
        self.country = 'Earth'

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

class ChinaPeople:
    country = Person()

    def __init__(self,name,country):
        self.name = name
        self.country = country

daxin = ChinaPeople('daxin','China')
print(daxin.country)

分析:

  1. 由于描述器Person是一个非数据描述器,优先级低于实例自己的__dict__
  2. 实例在初始化时对self.country属性赋值,会直接在自己的__dict__中,写入
  3. 在访问时根据MRO,优先访问实例自己的__dict__,所以结果是China

2.2 数据描述器

同时实现了__get__、__set__方法就称为数据描述器(data descriptor)

数据描述器的优先级高于实例的字典__dict__。

class A:

    def __init__(self):
        self.name = 'A'

    def __get__(self, instance, owner):
        print('From A __get__')
        return self.name

    def __set__(self, instance, value):
        print('From A __set__')

class B:
    name = A()
    def __init__(self):
        self.name = 'B'

b = B()
print(b.name)


# 结果:

# From A __set__

# From A __get__

# A

分析:

  1. 描述器A,实现一个数据描述器,优先级高于实例的自己的__dict__
  2. 在对b进行实例化的时候,设置了b的name属性,根据mro规则,找到父类B的name属性,然后发现其是一个数据描述器,然后被描述器A的__set__方法捕获。
  3. 当打印实例属性name时,由于数据描述器中,没有对传入的'B'进行赋值,所以这里'B'就丢了,最后访问属性name,会被描述器的__get__方法捕获,并返回描述器的name属性,所以打印是"A"

那么self是什么,__set__方法的参数都是什么意思:

  • self:对应A的实例(这里是属性name)
  • instance:对应的是实例本身,这里就是b
  • value:表示设置的值(这里就是'B')

2.2.1 实例

分析下面代码的运行原理

class A:

    def __init__(self):
        self.name = 'A'

    def __get__(self, instance, owner):
        print('From A __get__')
        # return self.name
        return instance.__dict__['name']

    def __set__(self, instance, value):
        print('From A __set__')
        instance.__dict__['name'] = value

class B:
    name = A()

    def __init__(self):
        self.name = 'B'

b = B()
print(b.name)

分析:

  1. 当b在初始化时,对name属性进行了设置,所以第一步先按照mro查找name属性。
  2. 在父类B中,查找到类属性name,它的结果是一个数据描述器,所以设置的请求被数据描述器的__set__方法捕获,在__set__方法中,为实例自己的__dict__注入了属性name以及它的值。
  3. 在打印name属性时,由于数据描述器的优先级高于实例的__dict__,所以操作被描述器的__get__方法捕获,在内部返回了实例自己__dict__的属性name,所以最后打印'B'

2.3 描述器的调用及属性访问顺序

        当类中存在描述器时,那么对象属性的调用就会发生变化。根据上面的例子,我们知道,实例属性访问的优先级为:数据描述器 > 实例字典__dict__ > 非数据描述器

特别注意:这里的访问顺序指的是:实例属性对应一个描述器时的顺序。,如果直接对类属性进行赋值操作,会直接覆盖类的描述器。
结合前面学的魔术方法,分析整个过程。

  1. 实例daxix的属性name(daxin.name) 本质上执行的是daxin.__getattribute__()方法
  2. daxin.__getattribute__() 其实是type(daxin).__dict__['name'].__get__(daxin,type(daxin))

使用Pyhon描述这个过程就是

def __getattribute__(self, key):
    print('from B __getattribute__')
    v = super(B, self).__getattribute__(key)   # 这里用 self.__getattribute__就会递归了
    # v = object.__getattribute__(self, key)   # 使用super的方法,等同于直接调用object
    if hasattr(v, '__get__'):
        return v.__get__(self, type(self))  
    return v

完整的代码:

class A:

    def __init__(self):
        self.name = 'A'

    def __get__(self, instance, owner):
        print('From A __get__')
        # return self.name
        return instance.__dict__['name']

    def __set__(self, instance, value):
        print('From A __set__')
        instance.__dict__['name'] = value

class B:
    name = A()

    def __init__(self):
        self.name = 'B'

    def __getattribute__(self, key):
        print('from B __getattribute__')
        v = super(B, self).__getattribute__(key)   
        if hasattr(v, '__get__'):
            return v.__get__(self, type(self))
        return v

b = B()
print(b.name)

2.4 描述器总结

总结几点比较重要的:

  • 描述器的调用是因为__getattribute__()方法
  • 重写__getattribute__()方法会组织正常的描述器调用
  • __getattribute__()只对新式类的实例可用
  • object.__getattribute__()和type.__getattribute__()对__get__()的调用不一样
  • 数据描述器总是比实例字典优先
  • 非数据描述器可能被实例字典重写/覆盖(非数据描述器不如实例字典优先)

3 Python的描述器体现

        描述器在Python中应用非常广泛。我们定义的实例方法,包括类方法(classmethod)和静态方法(staticmethod)都属于非数据描述器。所以实例可以重新定义和覆盖方法。这样就可以使一个实例拥有与其他实例不同的行为(方法重写)。
        但property装饰器不然,它是一个数据描述器,所以实例不能覆盖属性。

class A:
    def __init__(self,name ):
        self._name = name

    @staticmethod 
    def hello():         # 非数据描述器
        print('world')

    @classmethod
    def world(cls):      # 非数据描述器
        print('world')

    @property
    def name(self):      # 数据描述器
        return self._name

    def welcome(self):   # 非数据描述器
        print('Welcome')

class B(A):            
    def __init__(self,name):
        super().__init__(name)


daxin = B('daxin')
daxin.hello = lambda : print('modify hello')  # 可以被覆盖
daxin.world = lambda : print('modify world')  # 可以被覆盖
daxin.welcome = lambda : print('modify welcome')  # 可以被覆盖
daxin.name = lambda self: self._name  # 无法被覆盖

daxin.hello()
daxin.world()
daxin.welcome()

3.1 staticmethod简单实现

下面是一个简单的StaticMethod的实现

class StaticMethod:
    def __init__(self, fn):
        self.fn = fn

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

class A:

    @StaticMethod  # hello = StaticMethod(hello)
    def hello():  
        print('hello world')

daxin = A()
daxin.hello()  # hello() = StaticMethod().fn()

静态方法不需要传参,那么只需要在__get__方法拦截后,仅仅返回方法本身即可。

3.2 ClassMethod简单实现

import  functools
class ClassMethod:

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

    def __get__(self, instance, owner):
        #return lambda : self.fn(owner)
        return  functools.partial(self.fn,owner)

class A:

    @ClassMethod
    def hello(cls):
        print('hello world {}'.format(cls.__name__))

daxin = A()
daxin.hello()  # hello() = functools.partial(self.fn,owner)

类方法由于默认会把类当作参数传递,所以需要把方法的第一个参数固定为类,所以使用偏函数来固定,是一个比较好的办法,又或者使用lambda,由于lambda函数只能接受一个参数,所以当类方法是多个参数时,无法接受。

3.3 对实例的数据进行校验

现有如下代码:

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

对上面类的属性name,age进行数据类型的校验。
思路:

  1. 写函数,直接在__init__中检查,如果不符合直接抛出异常(一般人都会)
  2. 装饰器(多数人会)
  3. 描述器(少数人会)
  4. 描述器版装饰器(基本没人会)

3.3.1 直接在__init__函数中检查

class Person:
    def __init__(self, name:str, age:int):
        # 每次都判断,然后赋值
        # if self._typecheck(name,str):
        #     self.name = name
        # if self._typecheck(age, int):
        #     self.age = age

        # 或者直接构建需要的数据类型,一次性判断,最后赋值
        params = [(name,str),(age,int)]
        for param in params:
            if not self._typecheck(*param):
                raise TypeError(param[0])
        self.name = name
        self.age = age

    def _typecheck(self,value,typ):
        if not isinstance(value, typ):
            raise TypeError(value)
        return True

daxin = Person('daxin',20)
print(daxin.name)
print(daxin.age)

看起来也太丑了,不能复用不说,在初始化阶段还做了大量的逻辑判断,也不容易让别人明白你真正的意图是啥。

3.3.2 装饰器版本

import inspect

def TypeCheck(cls:object):
    def wrapper(*args,**kwargs):
        sig = inspect.signature(cls)  # 获取签名对象
        param = sig.parameters.values()  # 抽取签名信息(有序)
        data = zip(args,param)  # 构建值与类型的元组
        for value,typ in data:
            if typ.annotation != inspect._empty:   # 当定义了参数注解时,开始参数判断
                if not isinstance(value,typ.annotation):
                    raise TypeError(value)   # 判断不通过,爆出异常
        return cls(*args,**kwargs)
    return wrapper

@TypeCheck  # Person = TypeCheck(Person)('daxin',20)  ==> wrapper('daxin',20)
class Person:
    def __init__(self,name:str, age:int):
        self.name = name 
        self.age = age

daxin = Person('daxin','20')
print(daxin.name)
print(daxin.age)

看起来很好的解决了参数类型的检查,并且也可以针对不同类继续进行参数检查,所以说:装饰器真香

3.3.3 描述器版本

class TypeCheck:
    def __init__(self, name, typ):
        self.name = name
        self.typ = typ

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value,self.typ):
            raise TypeError(value)
        instance.__dict__[self.name] = value

class Person:

    name = TypeCheck('name',str)      # 硬编码
    age = TypeCheck('age',int)        # 硬编码

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

daxin = Person('daxin','20')
print(daxin.name)
print(daxin.age)

3.3.4 装饰器+描述器版本之函数装饰器

import inspect


class TypeCheck:

    def __init__(self, name, typ):
        self.name = name
        self.typ = typ

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value,self.typ):
            raise TypeError(value)
        instance.__dict__[self.name] = value


# 动态注入name,age描述器属性
def AttriCheck(cls:object):
    def wrapper(*args,**kwargs):
        sig = inspect.signature(cls)
        params = sig.parameters
        for k,v in params.items():
            print(v.annotation)
            if v.annotation != inspect._empty:
                if not hasattr(cls,k):
                    setattr(cls,k,TypeCheck(k,v.annotation))
        return cls(*args,**kwargs)
    return wrapper

@AttriCheck   # Person = AttriCheck(Person)
class Person:

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

a = Person('daxin', 20)
print(a.name)
print(a.age)

使用装饰器结合描述器时,类必须包含对应同名描述器,才可以利用描述器进行参数检查,所以,利用反射,将参数注入类中,然后通过描述器进行检查

3.3.5 装饰器+描述器版本之类装饰器

能否把上面的装饰器函数,改为类?

import inspect

class TypeCheck:

    def __init__(self, name, typ):
        self.name = name
        self.typ = typ

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value,self.typ):
            raise TypeError(value)
        instance.__dict__[self.name] = value

class AttriCheck:
    def __init__(self,cls):
        self.cls = cls

    def __call__(self, *args, **kwargs):
        sig = inspect.signature(self.cls)
        params = sig.parameters
        for name,typ in params.items():
            if typ.annotation != inspect._empty:
                if not hasattr(self.cls, name):
                    setattr(self.cls,name,TypeCheck(name,typ.annotation))
        return self.cls(*args,**kwargs)


@AttriCheck   # Person = AttriCheck(Person)
class Person:

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

a = Person('daxin', '20')
print(a.name)
print(a.age)

4 疑问

看下面例子:

class B:
    def __init__(self, data):
        self.data = data

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

    def __set__(self, instance, value):
        self.data = value


class C:
    name = B('daxin')
    age = B(20)

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

daxin = C('tom',18)
dachenzi = C('Jack',29)
print(daxin.name) 

结果是'Jack',为什么呢?

  1. name和age属于类属性,只会在定义的时候实例化一次!不同实例的name和age属性是公用的!
  2. 在描述器中,把实例设置的值,绑定到了描述器本身的属性上去了。
  3. 不同实例的name和age属性都指向了相同的描述器,并且每次修改的都是同一个属性。
  4. 这种坑是要避免的,尽量把属性绑定在实例自己身上
posted @ 2019-03-10 15:38  SpeicalLife  阅读(572)  评论(0编辑  收藏  举报