淺談Python中的描述器

描述器

對於Python的描述器的作用,我們可以先記住一句話:描述器是描述類的屬性的。

描述器的魔術方法

先思考下面程序的執行流程:

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


class B:
    x = A() # 定義(描述)類的屬性
    def __init__(self):
        print('B.init')

print(B.x.a1) # 會輸出什麼?

b = B()
print(b.x.a1) # 會輸出什麼?

上例中,x作爲B的類屬性,但x並非是一個簡單數據類型,而是A類的實例化對象。因此,訪問屬性x時,必然會實例化A。

因此,分別輸出爲:

print(B.x.a1) # 會輸出
A.init
a1

---------------

b = B()
# 輸出
A.init
B.init

print(b.x.a1) # 輸出
a1

上例的代碼,已經可以簡單認爲B類的屬性x,被A類的實例來"描述"了。但Python中的描述器,有更爲精確的定義,它要求必須實現某些魔術方法。

__get__方法

__get__(self, instance, owner)

如果對上述A類,做一些改造。比如在A類中實現__get__方法,看會發生什麼事情。

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

    def __get__(self, instance, owner): # A類增加了__get__方法
        print("Class A.__get__{} {} {}".format(self,instance,owner))

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


print(B.x) # 會輸出什麼?
# 輸出:
# A.init
# Class A.__get__<__main__.A object at 0x000001EDE1170A20> None <class '__main__.B'>
# None

b = B()
print(b.x) # 會輸出什麼?爲什麼?
# 輸出:
# A.init
# B.init
# Class A.__get__<__main__.A object at 0x000001DDDF3A0A20> <__main__.B object at 0x000001DDDF3A0EF0> <class '__main__.B'>
# None

print(b.x.a1)
# 異常AttributeError: 'NoneType' object has no attribute 'a1'
# b.x竟然是None?

解釋:因爲定義了__get__方法,A類就是一個描述器。

當對B類或者B類的實例的x屬性讀取時,變成對A類的實例的訪問時,就會調用此方法。此方法的返回值,會作爲A的實例化對象的返回值。

簡單而言,B類的x屬性,使用了一個A類去描述(定義)它。如果對x屬性訪問時,就會調用A類的__get__方法(描述方法),其返回值會返回給x屬性。

三個參數的解析:

參數 說明
self <__main__.A object at 0x000001DDDF3A0A20> 指當前實例,調用者 A(). 描述器實例自身
instance <__main__.B object at 0x000001DDDF3A0EF0> 指代owner的實例 B(),描述器擁有者的實例對象
owner <class '__main__.B'> 指屬性所屬的類 B,誰擁有這個描述器

正常情況下,__get__魔術方法需要返回self,也就是A的實例,才能正常返回a1屬性。

    def __get__(self,instance,owner):
        print("A.__get__{} {} {}".format(self,instance,owner))
        return self

此時,我們可以總結描述器的定義了。

描述器的定義

描述器定義:

Python中,一個類實現了__get__,__set__,__delete__ ,三個方法中任何一個方法,就是描述器。

如果僅實現了__get__,就是非數據描述器non-data descriptor;

同時實現了__get__,__set__就是數據描述器 data descriptor;

因此,上述的例子,屬於非數據描述器。

描述器一般都會實現__get__方法,因爲此方法的返回值就是作爲被描述的類的屬性的值。

因此,一個類的屬性,完全可以使用另一個類的實例來描述它。由於類的實例完全可以自由定義,對於某個類屬性,可以藉助類,來支撐起更爲強大的功能。

如果一個類的類屬性設置爲描述器,那麼它被稱爲owner屬主。比如上例中的B,它擁有了描述器A。

屬性的訪問順序

爲上例中的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()  # 一定會輸出A.init,因爲定義必須有值,而該值必須是A的實例化。
    def __init__(self):
        print('B init')
        self.x = 'b.x' # 增加實例屬性x

print(B.x) # 輸出:
# A.__get__<__main__.A object at 0x0000019BD760B400> None <class '__main__.B'>
# <__main__.A object at 0x0000019BD760B400>  x就是A的實例self


print(B.x.a1) # 輸出:
#A.__get__<__main__.A object at 0x000002929C0BB400> None <class '__main__.B'>
#a1

b = B()
print(b.x)
# A init
# B init
# b.x  訪問了實例屬性

print(b.x.a1) # AttributeError: 'str' object has no attribute 'a1'

b.x訪問到了實例的屬性,而不是描述器。

也就是說,對於非數據描述器,屬性的訪問順序是符合一般預期的。

總結:

非數據描述器,只對類屬性產生作用。當訪問類屬性時,將調用描述器的__get__方法

__set__方法

__set__(self,instance,value)

這裏的instance是什麼? value是什麼?

繼續修改代碼,爲類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()  # 一定會輸出A.init
    def __init__(self):
        print('B init')
        self.x = 'b.x' # 增加實例屬性x

#print(B.x)
#print(B.x.a1)
# 輸出
# A.init
# A.__get__<__main__.A object at 0x00000276E5920B38> None <class '__main__.B'>
# <__main__.A object at 0x00000276E5920B38>
# A.__get__<__main__.A object at 0x00000276E5920B38> None <class '__main__.B'>
# a1

b = B()
# 輸出:
# A.init
# B init
# A.__set__ <__main__.A object at 0x000002CE77C40B38> <__main__.B object at 0x000002CE77C40F60> b.x   b實例屬性的值傳遞到A上了。 看樣子,調用了__set__

print(b.x) # 還是訪問b實例屬性嗎?還是說變成了被描述器描述的屬性?
print(b.x.a1)
# 輸出:
# A.__get__<__main__.A object at 0x0000023250DE0B38> <__main__.B object at 0x0000023250DE0F60> <class '__main__.B'>
# <__main__.A object at 0x0000023250DE0B38>
# A.__get__<__main__.A object at 0x0000023250DE0B38> <__main__.B object at 0x0000023250DE0F60> <class '__main__.B'>
# a1   訪問b.x.a1,返回了描述器的數據

如果當b進行屬性賦值時(self.x = ‘b.x’),此時調用了set了。注意,此屬性必須是被描述器描述過的類屬性。

此時instance爲b,values爲b.x 。

一般都會使用實例來調用,而不是類來調用如B.x,因爲B.x就不會調用__set__方法。

當使用了__set__方法後,屬性的訪問順序發生了變化:

數據描述器 -->
實例的__dict__ -->
非數據描述器

數據描述器屬性訪問優先於實例的_dict_

實例的__dict__優先於非數據描述器。

__delete__方法在刪除相關屬性時觸發,具有同樣的效果,有了這個方法,就是數據描述器。

本質

觀察數據描述器和非數據描述器時,屬性字典的變化:

b.__dict__和 B.__dict__

屏蔽了__set__方法結果如下:(非數據描述器)

class A:
    n = 'A.x'

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

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

class B:
    x = A()

    def __init__(self):
        self.x = 'b.x'
        self.y = 'b.y'

print(B.__dict__)
# {'__module__': '__main__', 'x': <__main__.A object at 0x000001C9262370F0>, '__init__': <function B.__init__ at 0x000001C9262D7AE8>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}

print(B().__dict__)
# {'x': 'b.x', 'y': 'b.y'} # 實例字典符合一般預期

不屏蔽__set__方法結果如下:(數據描述器)

print(B.__dict__)
# {'__module__': '__main__', 'x': <__main__.A object at 0x000001D02F1570F0>, '__init__': <function B.__init__ at 0x000001D02F1F7B70>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}


print(B().__dict__)
# Call A __set__
# {'y': 'b.y'} # x屬性不見了

因此,是數據描述器時,把實例屬性從屬性字典中去除了,此時當然先訪問數據描述器了。

爲什麼b.x不存在b的字典中?按以前的理解,應該會添加一個key value。但由於它被描述了,行爲就不同了。

注意,因爲b.y不是描述器,則它的行爲還是如常表現,加入到字典中。

而B的dict也不變,因爲實例的賦值不會影響類屬性。

因此,如果類的屬性是一個描述器,此時實例屬性賦值的行爲就改變了。但是對於用戶而言,並不需要關心它是否爲描述器。因此,描述器的行爲就必須表現得和普通的屬性賦值一樣。

因此,需要做以下處理:

    def __set__(self,instance,value):
         print('A.__set__ {} {} {}'.format(self,instance,value))
         instance.__dict__['x'] = value

此時,描述器進行賦值時,對類屬性的讀或者寫會調用描述器的__get__或者__set__方法。

而instance爲b,此時在set方法中把‘x’的值手動添加一個kv到instance的dict中。如此再訪問b.x的時候,注意是訪問描述器,然後到__get__方法中。

因爲b.x一定會訪問描述器,而不是b.__dict__。此時攔截了訪問字典的路徑了。

如果b.x需要返回’b.x’,可以使用直接的方式:

    def __get__(self,instance,owner):
        print("A.__get__{} {} {}".format(self,instance,owner))
        return instance.__dict__['x']

總結

數據描述器,針對類屬性和實例屬性都產生作用。當訪問或者修改此屬性時,將調用相應的__get__或者__set__方法

我們在《淺談Python中的反射》這篇文章中知道,當定義了魔術方法__setattr__時,當對屬性進行設置時,會阻止設置實例屬性加入實例字典的行爲,而調用此方法。

而魔術方法__set__中又得知,當對屬性進行設置時,會調用此方法。那麼,這兩個方法的訪問優先級是如何的?

看下面的例子:

class A:
    n = 'A.x'
    def __init__(self,name):
        self.name = name

    def __get__(self, instance, owner):
        print('Call A __get__')
        return self

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



class B:
    x = A('x')

    def __init__(self):
        self.x = 'b.x'

    def __setattr__(self, key, value):
        print('Call B __setattr__')

b = B()
b.x = 'b.x1'
print(b.x)

# 輸出
# Call B __setattr__
# Call B __setattr__
# Call A __get__
# <__main__.A object at 0x0000020BC64E0A20>

以上結果可知,當設置實例屬性時,先調用實例本身的__setattr__方法,當讀取屬性時調用了描述器的__get__方法

因此,我們可以更爲準確得出實例屬性訪問順序:

實例調用__getattribute__ --> 
__setattr__ 攔截 -->
(數據描述器) -->
instance.__dict__ --> 
(非數據描述器) -->
instance.__class__.__dict__ --> 
繼承的祖先類(直到object).__dict__ --> 
找不到 --> 
調用__getattr__

Python中描述器的應用

描述器在Python中應用非常廣泛。

Python的方法,普通的實例方法,包括staticmethodclassmethod,都實現爲非數據描述器。因此,實例可以重新定義和覆蓋方法,表現爲正常的字典屬性的訪問和修改 。這允許單個實例獲取與同一類的其他實例不同的行爲。

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 # AttributeError: can't set attribute  無法覆蓋數據描述器的定義


a = A()
print(a.__dict__)  # {'foo': 100, 'bar': 200} 看不到z屬性
print(A.__dict__) # {'__module__': '__main__', 'foo': <classmethod object at 0x0000017674C6B7B8>, 'bar': <staticmethod object at 0x0000017674C6B7F0>, 'z': <property object at 0x0000017674BCD598>, 'getfoo': <function A.getfoo at 0x0000017674C58C80>, '__init__': <function A.__init__ at 0x0000017674C58D08>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

@staticmethod的描述器版本實現

class Staticmethod:

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

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

class Foo:

    @Staticmethod # show = Staticmethod(show) 相當於類屬性show,被類Staticmethod描述了。返回值爲__get__的返回值。
    def show():
        print('I am staticmethod')

f = Foo()
f.show()

@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 Bar:
    @ClassMethod # show = ClassMethod(show)
    def show(cls):
        print(cls.__name__)

Bar.show()

參數類型檢查功能

描述器方式實現

class Typed:
    def __init__(self,arg,typed):
        self.arg = arg
        self.typed = typed

    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.typed):
            raise Exception('type error')
        instance.__dict__[self.arg] = value


import inspect
def typeassert(cls):
    obj = inspect.signature(cls).parameters
    for name,value in obj.items():
        if value.annotation !=  value.empty:
            setattr(cls,name,Typed(name, value.annotation))
    return cls


@typeassert  # Person = typeassert(Person)
class Person:
    #name = Typed('name',str)
    #age = Typed('age',int)
    def __init__(self,name:str, age:int):
        self.name = name
        self.age = age



tom = Person('tom',100)
print(tom.__dict__)

類裝飾器方式實現

class Typed:
    def __init__(self,arg,typed):
        self.arg = arg
        self.typed = typed

    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.typed):
            raise Exception('type error')
        instance.__dict__[self.arg] = value


import inspect
# def typeassert(cls):
#     obj = inspect.signature(cls).parameters
#     for name,value in obj.items():
#         if value.annotation !=  value.empty:
#             setattr(cls,name,Typed(name, value.annotation))
#     return cls

class TypeAssert:
    def __init__(self,cls):
        obj = inspect.signature(cls).parameters
        for name,value in obj.items():
            if value.annotation != value.empty:
                setattr(cls,name,Typed(name,value.annotation))
        self.cls = cls

    def __call__(self, name, age):
        return self.cls(name,age)



@TypeAssert  # Person = TypeAssert(Person)
class Person:
    #name = Typed('name',str)
    #age = Typed('age',int)
    def __init__(self,name:str, age:int):
        self.name = name
        self.age = age


tom = Person('tom',100)
print(tom.__dict__)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章