27 - 面向對象高級-描述器

1 描述器

        通常來講,一個描述器是一個有綁定行爲的對象屬性(object attribute),它的訪問控制被描述器協議方法重寫。這些方法是 __get__(), __set__(), 和 __delete__() 。app

有這些方法的對象叫作描述器。函數

        默認對屬性的訪問控制是從對象的字典裏面(__dict__)中獲取、設置和刪除它。舉例來講, 好比 a.x 的查找順序是, a.__dict__['x'] , 而後 type(a).__dict__['x'] , 而後找 type(a) 的父類(不包括元類(metaclass)). 若是查找到的值是一個描述器, Python就會調用描述器的方法來重寫默認的控制行爲。這個重寫發生在這個查找環節的哪裏取決於定義了哪一個描述器方法。注意, 只有在新式類中時描述器纔會起做用。(新式類是繼承自 type 或者 object 的類)。編碼

2 描述器協議

描述器主要涉及三個方法:code

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

一個對象具備其中任一個方法就會成爲描述器,從而在被看成對象屬性時重寫默認的查找、設置和刪除行爲。orm

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__方法的參數都是什麼意思:ip

  • 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__中,寫入{"country":"China}
  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. 這種坑是要避免的,儘可能把屬性綁定在實例本身身上
相關文章
相關標籤/搜索