天天看點

這可能是将Python描述器(descriptor)介紹得最透徹的文章!(持續更新)一、引入描述器概念二、了解描述器必備三、參考資料

文章目錄

  • 一、引入描述器概念
    • 1. 使用`property`的可能陷阱
    • 2. 如何規避使用`property`的陷阱
    • 3. 描述器是實作特定方法的類
  • 二、了解描述器必備
    • 1. 描述器方法參數的含義
    • 2. 類和對象的`__dict__`屬性
    • 3. 設定/删除屬性的優先級
    • 4. 擷取屬性的優先級
  • 三、參考資料

一、引入描述器概念

1. 使用

property

的可能陷阱

在文章如何使用property掌控屬性通路?中,我們學習了如何使用

property

來實作在擷取或設定“執行個體屬性”的同時進行額外自定義操作,如:類型檢查或驗證等功能,但在僅了解這些的情況下使用

property

可能會産生如下述代碼一樣的問題:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        self._first_name = value

    # 以下為重複代碼,僅僅是換了一個名字而已
    @property
    def last_name(self):
        return self._last_name

    @last_name.setter
    def last_name(self, value):
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        self._last_name = value


def main():
    python_guru = Person('Raymond', 'Hettinger')
    print(python_guru.first_name)
    print(python_guru.last_name)
    print('-' * 50)
    python_creator = Person(1, 2)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

Raymond

Hettinger

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

Traceback (most recent call last):

TypeError: 姓名必須是字元串格式!

上述

# 1

# 2

# 3

# 4

處代碼都可以實作類型檢查等功能(因為使用

int

數值嘗試建立

Person

類的對象時會報錯),但代碼除了變量名稱不一樣外,其餘完全一緻,即代碼重複了,這樣的代碼不僅無美感可言,而且修改比較麻煩。這就是标題所說的陷阱所指。

2. 如何規避使用

property

的陷阱

為了避免上述問題,下面代碼使用本文主角描述器(在本文稍後會立馬給出描述器的定義,這裡僅給讀者對其做一個感性認識)來實作同樣功能:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):  # 1
        print('Name類中的__get__方法正在被調用...')
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]  # 3

    def __set__(self, instance, value): # 2
        print('Name類中的__set__方法正在被調用...')
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        instance.__dict__[self.storage_name] = value  # 4


class Person:
    first_name = Name('first_name')  # 5
    last_name = Name('last_name')  # 6

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Raymond', 'Hettinger')
    print('-' * 50)
    print(python_guru.first_name)
    print(python_guru.last_name)
    print('-' * 50)
    python_creator = Person(1, 2)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

Name類中的__set__方法正在被調用…

Name類中的__set__方法正在被調用…

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

Name類中的__get__方法正在被調用…

Raymond

Name類中的__get__方法正在被調用…

Hettinger

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

Name類中的__set__方法正在被調用…

Traceback (most recent call last):

TypeError: 姓名必須是字元串格式!

由上述代碼及其運作結果可知,該代碼也可以實作和使用

property

相同的功能。實際上,在上述代碼中,類

Name

其實就是一個描述器,而此時

Person

類的執行個體将設定和擷取執行個體屬性的功能代理給了描述器中的

__set__

__get__

方法,進而在這兩個方法中通過操作Person類執行個體的

__dict__

屬性來設定或擷取屬性的值。

3. 描述器是實作特定方法的類

定義:描述器是一個類,該類實作了

__get__

__set__

__delete__

三個方法中的至少一個方法1,描述器可以為對象通路多個屬性時提供一種相同的邏輯和操作。

僅從上述定義并不能看出描述器究竟有多麼強大的功能,但實際上描述器可以說是Python實作面向對象程式設計範式的重要基礎之一,因為Python中的函數、方法、屬性、類方法裝飾器

@classmethod

、靜态方法裝飾器

@staticmethod

等都基于描述器實作。對此,這個系列的文章将為大家一一道來。

二、了解描述器必備

在上述代碼中,你可能對于這幾個位置有疑問:

  • # 1

    # 2

    方法處的參數

    instance

    owner

    的作用;
  • # 3

    # 4

    __dict__

    屬性的功能;
  • # 5

    # 6

    處存在和執行個體對象同名類屬性的必要性。

為了更好地深入了解Python的描述器,下面先對這三點進行詳細分析:

1. 描述器方法參數的含義

在描述器實作的幾個方法中,各個方法的參數含義為:

  • instance

    :表示

    Person

    類的一個執行個體;
  • owner

    :表示執行個體化得到上述

    instance

    的類,即

    Person

    類。

驗證代碼如下:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
    	print('描述器Name的__get__方法正在被調用...')
        print('instance = ', instance, 'owner = ', owner)
        return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        print('描述器Name的__set__方法正在被調用...')
        print('instance = ', instance, 'value = ', value)
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        instance.__dict__[self.storage_name] = value


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

描述器Name的__set__方法正在被調用…

instance = <__main__.Person object at 0x7f6d01576630> value = Guido

描述器Name的__set__方法正在被調用…

instance = <__main__.Person object at 0x7f6d01576630> value = Rossum

描述器Name的__get__方法正在被調用…

instance = <__main__.Person object at 0x7f6d01576630> owner = <class ‘__main__.Person’>

Guido

2. 類和對象的

__dict__

屬性

在Python中,不管是類還是用類建立的執行個體都是對象(關于類也是一種對象,以及類這種對象是由什麼執行個體化而來,請見文章為什麼說元類是你最常使用卻也最陌生的Python語言特性之一?),這兩類對象都有一個名為

__dict__

的屬性,該屬性都是一個字典,其中:

  • 類對象的

    __dict__

    屬性儲存了該類的命名空間,其中以鍵值對形式包含:類屬性、執行個體方法、幫助文檔資訊等;
  • 用類所建立執行個體對象的

    __dict__

    屬性以鍵值對形式包含執行個體屬性。

關于上述說明,可由下列代碼來運作确認:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        instance.__dict__[self.storage_name] = value


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    print('Person.__dict__ = ', Person.__dict__)
    python_guru = Person('Raymond', 'Hettinger')
    print('python_guru.__dict__ = ', python_guru.__dict__)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

Person.__dict__ = {…, ‘__doc__’: ‘描述一個人的類’, ‘first_name’: <__main__.Name object at 0x7f0ae2eb99e8>, ‘last_name’: <__main__.Name object at 0x7f0ae11a0550>, ‘__init__’: <function Person.__init__ at 0x7f0ae119e9d8>, ‘__dict__’: <attribute ‘__dict__’ of ‘Person’ objects>, …}

python_guru.__dict__ = {‘first_name’: ‘Raymond’, ‘last_name’: ‘Hettinger’}

3. 設定/删除屬性的優先級

在Python中,當使用

obj.attr

(請記住,執行個體方法的參數

self

也引用了執行個體對象,這對了解本節有較大幫助)的文法設定/删除某指定名稱的屬性時,根據以下幾點:

  • 類中是否有通過描述器建立的對象作為類屬性;
  • 描述器實作了

    __get__

    __set__

    __delete__

    中的哪些方法(其中:僅定義

    __get__

    方法的類叫做非資料型描述器,僅定義

    __set__

    __delete__

    方法的類叫做資料型描述器);
  • 執行個體對象是否包含指定名稱屬性。

程式的屬性查找順序不同,具體來說可用下圖來表示:

這可能是将Python描述器(descriptor)介紹得最透徹的文章!(持續更新)一、引入描述器概念二、了解描述器必備三、參考資料
結論1:在使用

obj.attr

的文法設定/删除指定名稱的屬性時,資料型描述器具有最高優先級,其次為執行個體屬性。

下面對上述結論做依次驗證:

驗證1.1:當滿足下列情形(即對應上圖中的

# 1

-->

# 2

-->

# 3

-->

# 4

),則描述器對應方法被優先調用:
  • 建立對象的類中有指定名稱的類屬性;
  • 該類屬性為資料類描述器;
  • 設定屬性時資料類描述器實作了

    __set__

    方法;
  • 删除屬性時資料描述其實作了

    __delete__

    方法。

驗證代碼如下圖所示:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        print('描述器Name的__set__方法正在被調用...')
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        instance.__dict__[self.storage_name] = value

    def __delete__(self, instance):
        print('描述器Name的__delete__方法正在被調用...')
        del instance.__dict__[self.storage_name]


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    middle_name = Name('middle_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    print('Person.__dict__ = ', Person.__dict__)
    python_guru = Person('Guido', 'Rossum')
    python_guru.middle_name = 'van'
    print('python_guru.__dict__ = ', python_guru.__dict__)
    del python_guru.first_name
    del python_guru.middle_name
    print('python_guru.__dict__ = ', python_guru.__dict__)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

Person.__dict__ = {…, ‘first_name’: <__main__.Name object at 0x7f1b6d0bd5f8>, ‘middle_name’: <__main__.Name object at 0x7f1b6d0bd630>, ‘last_name’: <__main__.Name object at 0x7f1b6d0bd668>, …}

描述器Name的__set__方法正在被調用…

描述器Name的__set__方法正在被調用…

描述器Name的__set__方法正在被調用…

python_guru.__dict__ = {‘first_name’: ‘Guido’, ‘last_name’: ‘Rossum’, ‘middle_name’: ‘van’}

描述器Name的__delete__方法正在被調用…

描述器Name的__delete__方法正在被調用…

python_guru.__dict__ = {‘last_name’: ‘Rossum’}

由上述代碼還可以知道:

當使用

obj.attr

的文法設定屬性時,并不要求執行個體對象中有和類屬性同名的執行個體屬性。
驗證1.2:在使用

obj.attr

的文法設定/删除指定名稱的屬性時,當滿足下列情形(即對應上圖中的

# 1

-->

# 2

-->

# 3

-->

# 5

流程)時程式會優先嘗試調用對應的描述器方法,調用失敗則抛出

AttributeError

異常:
  • 建立對象的類中有使用描述器建立的指定名稱類屬性;
  • 設定屬性時描述器僅實作了

    __delete__

    方法;
  • 删除屬性時描述器僅實作了

    __set__

    方法。
class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __delete__(self, instance):
        print('描述器Name的__delete__方法正在被調用...')
        del instance.__dict__[self.storage_name]


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')

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

def main():
    python_guru = Person('Guido')


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

AttributeError: __set__
驗證1.3:在使用

obj.attr

的文法設定/删除指定名稱的屬性時,即使

Person

類中定義了由描述器建立的對象作為類屬性,隻要描述器僅實作了

__get__

方法(即僅為資料型描述器),程式都會直接對執行個體對象的

__dict__

屬性進行相應操作。

4. 擷取屬性的優先級

當使用

obj.attr

的文法擷取屬性時,屬性查找的優先級要比設定/删除屬性時要複雜得多,因為解釋器将根據下列不同情況進行屬性查找:

  • 類中是否有由描述器建立的對象作為類屬性;
  • 描述器是資料描述器還是非資料描述器。

具體流程可以用下圖來表示:

這可能是将Python描述器(descriptor)介紹得最透徹的文章!(持續更新)一、引入描述器概念二、了解描述器必備三、參考資料
結論2:在使用

obj.attr

擷取屬性時,屬性查找的優先級為:資料類描述器的優先級最高,其次為執行個體屬性,再次為非資料型描述器,然後是普通類屬性。

下面對上述結論做依次驗證:

驗證2.1:當類中有由資料型描述器建立的類屬性,且描述器該描述器同時為非資料型描述器(即實作了

__get__

方法),則優先調用描述器中的

__get__

擷取屬性(對應流程

# 1

-->

# 2

-->

# 3

-->

# 4

);如果描述器僅實作

__delete__

方法而未實作

__set__

方法,則

__get__

方法也不會被調用。
class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
        print('描述器Name的__get__方法正在被調用...')
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        print('描述器Name的__set__方法正在被調用...')
        if not isinstance(value, str):
            raise TypeError('姓名必須是字元串格式!')
        instance.__dict__[self.storage_name] = value


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)
    print(type(python_guru).__dict__['first_name'].__get__(python_guru, type(python_guru)))


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

描述器Name的__set__方法正在被調用…

描述器Name的__set__方法正在被調用…

描述器Name的__get__方法正在被調用…

Guido

描述器Name的__get__方法正在被調用…

Guido

即此時通過

obj.attr

的方式擷取屬性相當于

type(obj).__dict__['attr'].__get__(obj, type(obj))

驗證2.2:當類中沒有由資料型描述器建立的類屬性,而執行個體屬性中有待擷取屬性,則傳回執行個體屬性(對應流程

# 1

-->

# 2

-->

# 6

-->

# 7

–>

# 8

)。
class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

Guido
驗證2.3:當執行個體中不具有待擷取屬性,而有同名類屬性,但類屬性是一個非資料型描述器建立的對象,此時調用

__get__

方法(對應流程

# 1

-->

# 2

-->

# 6

-->

# 7

–>

# 8

–>

# 9

-->

# 10

–>

# 11

)。(注意此時雖然調用了

__get__

方法,但是程式會報錯!)
class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
    	print('描述器Name的__get__方法正在被調用...')
		if instance is None:
			return self
		else:
			return instance.__dict__[self.storage_name]


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

描述器Name的__get__方法正在被調用…

Traceback (most recent call last):

KeyError: ‘first_name’

驗證2.4:當執行個體對象中沒有待擷取屬性,且類屬性是由任意未實作任何描述器方法的類建立,則直接傳回類屬性((對應流程

# 1

-->

# 2

-->

# 6

-->

# 7

–>

# 8

–>

# 9

-->

# 10

–>

# 12

))。
class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name


class Person:
    """描述一個人的類"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

<__main__.Name object at 0x7ff9e759e748>

三、參考資料

  • [1] Descriptor HowTo Guide
  1. 這就是描述器協定,即隻要一個類實作了

    __get__

    __set__

    __delete__

    三個方法其中之一,則其就是一個描述器,而不管該類是否還實作了其他任何方法,這也是鴨子類型的一種展現,關于協定和鴨子類型這兩個名詞,更深入了解請見文章淺談Python中的注解和類型提示。 ↩︎