首頁 後端開發 Python教學 介紹python描述子的意義

介紹python描述子的意義

Dec 21, 2020 pm 05:48 PM

你也許經常會聽到“描述符”這個概念,但是由於大多數的程式設計師很少會使用到他,所以可能你並不太清楚了解它的原理,python視頻教學專欄將詳細介紹

介紹python描述子的意義

推薦(免費):python影片教學

但是如果你想自己的事業來說更上一層的話,對於python的使用更加熟練的話,我認為你還是應該對描述符的這個概念有一個清晰的了解,這對於你以後的發展有著巨大的幫助,也有利於你將來更深層的python設計的理解。

儘管在開發的過程中,我們沒有直接的使用過描述符,但是它在底層的運用卻是十分頻繁的存在。例如下面的這些:

  • functionbound methodunbound method
  • ##裝置是器
  • propertystaticmethodclassmethod這些是不是都很熟悉?
    其實這些都與描述符有著千絲萬縷的聯繫,這樣吧,我們透過下面的文章來探討一下描述符背後的工作原理吧。

什麼是描述符?

在我們了解什麼是描述符前,我們可以先找一個例子來看

class A:
    x = 10print(A.x) # 10
登入後複製
這個例子很簡單,我們先在類別

A中定義一個類別屬性x,然後得到它的值。 除了這個直接定義類別屬性的方法外,我們還可以這樣去定義一個類別屬性:

class Ten:
    def __get__(self, obj, objtype=None):
        return 10class A:
    x = Ten()   # 属性换成了一个类print(A.x) # 10
登入後複製
我們可以發現,這回的類別屬性

x不是一個具體的值了,而是一個類別Ten,透過這個Ten定義了一個__get__方法,傳回具體的值。

因此可得出:在python中,

我們可以把一個類別的屬性,託管給一個類,而這樣的屬性就是一個描述符簡而言之,
描述子是一個綁定行為屬性

而這又有什麼意思呢?

回想,我們在開發時,一般情況下,會叫
行為什麼? 行為即一個方法。

所以我們也可以將

描述子理解為:物件的屬性並非一個具體的值,而是交給了一個方法去定義。

可以想像一下,如果我們用一個方法去定義一個屬性,這麼做有什麼好處?

有了方法,我們就可以在方法內實現自己的邏輯,最簡單的,我們可以根據不同的條件,在方法內給屬性賦予不同的值,就像下面這樣:

class Age:
    def __get__(self, obj, objtype=None):
        if obj.name == 'zhangsan':
            return 20
        elif obj.name == 'lisi':
            return 25
        else:
            return ValueError("unknow")class Person:

    age = Age()

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

p1 = Person('zhangsan')print(p1.age)   # 20p2 = Person('lisi')print(p2.age)   # 25p3 = Person('wangwu')print(p3.age)   # unknow
登入後複製
這個例子中,

age 類別屬性被另一個類別託管了,在這個類別的__get__# 中,它會根據Person 類別的屬性name,決定age 是什麼值。

透過這樣一個例子,我們可以看到,透過描述符的使用,我們可以輕易地改變一個類別屬性的定義方式。

描述子協定

了解了描述子的定義,現在我們把重點放到託管屬性的類別上。

其實,一個類別屬性想要託管給一個類,這個類別內部實作的方法不能是隨便定義的,它必須遵守「描述子協定」,也就是要實作以下幾個方法:

  • __get__(self, obj, type=None) -> value
  • __set__(self, obj, 值) -> None
  • __delete__(self, obj) -> None
#只要是實作了以上幾個方法的

其中一個,那麼這個類別屬性就可以稱為描述符。

另外,描述子又可以分成「資料描述子」與「非資料描述子」:

    只定義了
  • __get___,叫做非數據描述子
  • 除了定義
  • __get__ 之外,還定義了__set____delete__,叫做資料描述子
#它們兩者有什麼差別,我會在下面詳述。

現在我們來看一個包含

__get____set__ 方法的描述子範例:

# coding: utf8class Age:

    def __init__(self, value=20):
        self.value = value

    def __get__(self, obj, type=None):
        print('call __get__: obj: %s type: %s' % (obj, type))
        return self.value

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("age must be greater than 0")
        print(&#39;call __set__: obj: %s value: %s&#39; % (obj, value))
        self.value = valueclass Person:

    age = Age()

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

p1 = Person(&#39;zhangsan&#39;)print(p1.age)# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class &#39;__main__.Person&#39;># 20print(Person.age)# call __get__: obj: None type: <class &#39;__main__.Person&#39;># 20p1.age = 25# call __set__: obj: <__main__.Person object at 0x1055509e8> value: 25print(p1.age)# call __get__: obj: <__main__.Person object at 0x1055509e8> type: <class &#39;__main__.Person&#39;># 25p1.age = -1# ValueError: age must be greater than 0
登入後複製
在這個範例中,類別屬性

age 是一個描述符,它的值取決於Age 類別。

從輸出結果來看,當我們取得或修改

age 屬性時,呼叫了Age__get____set__ 方法:

  • 当调用 p1.age 时,__get__ 被调用,参数 objPerson 实例,typetype(Person)
  • 当调用 Person.age 时,__get__ 被调用,参数 objNonetypetype(Person)
  • 当调用 p1.age = 25时,__set__ 被调用,参数 objPerson 实例,value 是25
  • 当调用 p1.age = -1时,__set__ 没有通过校验,抛出 ValueError

其中,调用 __set__ 传入的参数,我们比较容易理解,但是对于 __get__ 方法,通过类或实例调用,传入的参数是不同的,这是为什么?

这就需要我们了解一下描述符的工作原理。

描述符的工作原理

要解释描述符的工作原理,首先我们需要先从属性的访问说起。

在开发时,不知道你有没有想过这样一个问题:通常我们写这样的代码 a.b,其背后到底发生了什么?

这里的 ab 可能存在以下情况:

  1. a 可能是一个类,也可能是一个实例,我们这里统称为对象
  2. b 可能是一个属性,也可能是一个方法,方法其实也可以看做是类的属性

其实,无论是以上哪种情况,在 Python 中,都有一个统一的调用逻辑:

  1. 先调用 __getattribute__ 尝试获得结果
  2. 如果没有结果,调用 __getattr__

用代码表示就是下面这样:

def getattr_hook(obj, name):
    try:
        return obj.__getattribute__(name)
    except AttributeError:
        if not hasattr(type(obj), '__getattr__'):
            raise    return type(obj).__getattr__(obj, name)
登入後複製

我们这里需要重点关注一下 __getattribute__,因为它是所有属性查找的入口,它内部实现的属性查找顺序是这样的:

  1. 要查找的属性,在类中是否是一个描述符
  2. 如果是描述符,再检查它是否是一个数据描述符
  3. 如果是数据描述符,则调用数据描述符的 __get__
  4. 如果不是数据描述符,则从 __dict__ 中查找
  5. 如果 __dict__ 中查找不到,再看它是否是一个非数据描述符
  6. 如果是非数据描述符,则调用非数据描述符的 __get__
  7. 如果也不是一个非数据描述符,则从类属性中查找
  8. 如果类中也没有这个属性,抛出 AttributeError 异常

写成代码就是下面这样:

# 获取一个对象的属性
def __getattribute__(obj, name):
    null = object()
    # 对象的类型 也就是实例的类
    objtype = type(obj)
    # 从这个类中获取指定属性
    cls_var = getattr(objtype, name, null)
    # 如果这个类实现了描述符协议
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            # 优先从数据描述符中获取属性            return descr_get(cls_var, obj, objtype)
    # 从实例中获取属性    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]
    # 从非数据描述符获取属性    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)
    # 从类中获取属性    if cls_var is not null:
        return cls_var
    # 抛出 AttributeError 会触发调用 __getattr__
    raise AttributeError(name)
登入後複製

如果不好理解,你最好写一个程序测试一下,观察各种情况下的属性的查找顺序。

到这里我们可以看到,在一个对象中查找一个属性,都是先从 __getattribute__ 开始的。

__getattribute__ 中,它会检查这个类属性是否是一个描述符,如果是一个描述符,那么就会调用它的 __get__ 方法。但具体的调用细节和传入的参数是下面这样的:

  • 如果 a 是一个实例,调用细节为:
type(a).__dict__['b'].__get__(a, type(a))复制代码
登入後複製
  • 如果 a 是一个,调用细节为:
a.__dict__['b'].__get__(None, a)复制代码
登入後複製

所以我们就能看到上面例子输出的结果。

数据描述符和非数据描述符

了解了描述符的工作原理,我们继续来看数据描述符和非数据描述符的区别。

从定义上来看,它们的区别是:

  • 只定义了 __get___,叫做非数据描述符
  • 除了定义 __get__ 之外,还定义了 __set____delete__,叫做数据描述符

此外,我们从上面描述符调用的顺序可以看到,在对象中查找属性时,数据描述符要优先于非数据描述符调用。

在之前的例子中,我们定义了 __get____set__,所以那些类属性都是数据描述符

我们再来看一个非数据描述符的例子:

class A:

    def __init__(self):
        self.foo = 'abc'

    def foo(self):
        return 'xyz'print(A().foo)  # 输出什么?
复制代码
登入後複製

这段代码,我们定义了一个相同名字的属性和方法 foo,如果现在执行 A().foo,你觉得会输出什么结果?

答案是 abc

为什么打印的是实例属性 foo 的值,而不是方法 foo 呢?

这就和非数据描述符有关系了。

我们执行 dir(A.foo),观察结果:

print(dir(A.foo))# [... '__get__', '__getattribute__', ...]复制代码
登入後複製

看到了吗?Afoo 方法其实实现了 __get__,我们在上面的分析已经得知:只定义 __get__ 方法的对象,它其实是一个非数据描述符,也就是说,我们在类中定义的方法,其实本身就是一个非数据描述符。

所以,在一个类中,如果存在相同名字的属性和方法,按照上面所讲的 __getattribute__ 中查找属性的顺序,这个属性就会优先从实例中获取,如果实例中不存在,才会从非数据描述符中获取,所以在这里优先查找的是实例属性 foo 的值。

到这里我们可以总结一下关于描述符的相关知识点:

  • 描述符必须是一个类属性
  • __getattribute__ 是查找一个属性(方法)的入口
  • __getattribute__ 定义了一个属性(方法)的查找顺序:数据描述符、实例属性、非数据描述符、类属性
  • 如果我们重写了 __getattribute__ 方法,会阻止描述符的调用
  • 所有方法其实都是一个非数据描述符,因为它定义了 __get__

描述符的使用场景

了解了描述符的工作原理,那描述符一般用在哪些业务场景中呢?

在这里我用描述符实现了一个属性校验器,你可以参考这个例子,在类似的场景中去使用它。

首先我们定义一个校验基类 Validator,在 __set__ 方法中先调用 validate 方法校验属性是否符合要求,然后再对属性进行赋值。

class Validator:

    def __init__(self):
        self.data = {}

    def __get__(self, obj, objtype=None):
        return self.data[obj]

    def __set__(self, obj, value):
        # 校验通过后再赋值
        self.validate(value)
        self.data[obj] = value

    def validate(self, value):
        pass    
复制代码
登入後複製

接下来,我们定义两个校验类,继承 Validator,然后实现自己的校验逻辑。

class Number(Validator):

    def __init__(self, minvalue=None, maxvalue=None):
        super(Number, self).__init__()
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    def validate(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value < self.minvalue:
            raise ValueError(
                f&#39;Expected {value!r} to be at least {self.minvalue!r}&#39;
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )class String(Validator):

    def __init__(self, minsize=None, maxsize=None):
        super(String, self).__init__()
        self.minsize = minsize
        self.maxsize = maxsize

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f&#39;Expected {value!r} to be no smaller than {self.minsize!r}&#39;
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )复制代码
登入後複製

最后,我们使用这个校验类:

class Person:

    # 定义属性的校验规则 内部用描述符实现
    name = String(minsize=3, maxsize=10)
    age = Number(minvalue=1, maxvalue=120)

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

# 属性符合规则
p1 = Person('zhangsan', 20)print(p1.name, p1.age)# 属性不符合规则
p2 = person('a', 20)# ValueError: Expected 'a' to be no smaller than 3p3 = Person('zhangsan', -1)# ValueError: Expected -1 to be at least 1复制代码
登入後複製

现在,当我们对 Person 实例进行初始化时,就可以校验这些属性是否符合预定义的规则了。

function与method

我们再来看一下,在开发时经常看到的 functionunbound methodbound method 它们之间到底有什么区别?

来看下面这段代码:

class A:

    def foo(self):
        return 'xyz'print(A.__dict__['foo']) # <function foo at 0x10a790d70>print(A.foo)     # <unbound method A.foo>print(A().foo)   # <bound method A.foo of <__main__.A object at 0x10a793050>>复制代码
登入後複製

从结果我们可以看出它们的区别:

  • function 准确来说就是一个函数,并且它实现了 __get__ 方法,因此每一个 function 都是一个非数据描述符,而在类中会把 function 放到 __dict__ 中存储
  • function 被实例调用时,它是一个 bound method
  • function 被类调用时, 它是一个 unbound method

function 是一个非数据描述符,我们之前已经讲到了。

bound methodunbound method 的区别就在于调用方的类型是什么,如果是一个实例,那么这个 function 就是一个 bound method,否则它是一个 unbound method

property/staticmethod/classmethod

我们再来看 propertystaticmethodclassmethod

这些装饰器的实现,默认是 C 来实现的。

其实,我们也可以直接利用 Python 描述符的特性来实现这些装饰器,

property 的 Python 版实现:

class property:

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.fget        if self.fget is None:
            raise AttributeError(), "unreadable attribute"
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError, "can't set attribute"
        return self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError, "can't delete attribute"
        return self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)复制代码
登入後複製

staticmethod 的 Python 版实现:

class staticmethod:

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

    def __get__(self, obj, objtype=None):
        return self.func
复制代码
登入後複製

classmethod 的 Python 版实现:

class classmethod:

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

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.func(klass, *args)
        return newfunc
复制代码
登入後複製

除此之外,你还可以实现其他功能强大的装饰器。

由此可见,通过描述符我们可以实现强大而灵活的属性管理功能,对于一些要求属性控制比较复杂的场景,我们可以选择用描述符来实现。

总结

这篇文章我们主要讲了 Python 描述符的工作原理。

首先,我们从一个简单的例子了解到,一个类属性是可以托管给另外一个类的,这个类如果实现了描述符协议方法,那么这个类属性就是一个描述符。此外,描述符又可以分为数据描述符和非数据描述符。

之后我们又分析了获取一个属性的过程,一切的入口都在 __getattribute__ 中,这个方法定义了寻找属性的顺序,其中实例属性优先于数据描述符调用,数据描述符要优先于非数据描述符调用。

另外我們又了解到,方法其實就是一個非資料描述符,如果我們在類別中定義了相同名字的實例屬性和方法,按照__getattribute__ 中的屬性查找順序,實例屬性優先訪問。

最後我們分析了functionmethod 的區別,以及使用Python 描述子也可以實作propertystaticmethodclassmethod 裝飾器。

Python 描述子提供了強大的屬性存取控制功能,我們可以在需要對屬性進行複雜控制的場景中去使用它。

本作品採用《CC 協議》,轉載必須註明作者與本文連結

以上是介紹python描述子的意義的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

如何解決Linux終端中查看Python版本時遇到的權限問題? 如何解決Linux終端中查看Python版本時遇到的權限問題? Apr 01, 2025 pm 05:09 PM

Linux終端中查看Python版本時遇到權限問題的解決方法當你在Linux終端中嘗試查看Python的版本時,輸入python...

如何在使用 Fiddler Everywhere 進行中間人讀取時避免被瀏覽器檢測到? 如何在使用 Fiddler Everywhere 進行中間人讀取時避免被瀏覽器檢測到? Apr 02, 2025 am 07:15 AM

使用FiddlerEverywhere進行中間人讀取時如何避免被檢測到當你使用FiddlerEverywhere...

在Python中如何高效地將一個DataFrame的整列複製到另一個結構不同的DataFrame中? 在Python中如何高效地將一個DataFrame的整列複製到另一個結構不同的DataFrame中? Apr 01, 2025 pm 11:15 PM

在使用Python的pandas庫時,如何在兩個結構不同的DataFrame之間進行整列複製是一個常見的問題。假設我們有兩個Dat...

Uvicorn是如何在沒有serve_forever()的情況下持續監聽HTTP請求的? Uvicorn是如何在沒有serve_forever()的情況下持續監聽HTTP請求的? Apr 01, 2025 pm 10:51 PM

Uvicorn是如何持續監聽HTTP請求的? Uvicorn是一個基於ASGI的輕量級Web服務器,其核心功能之一便是監聽HTTP請求並進�...

如何在10小時內通過項目和問題驅動的方式教計算機小白編程基礎? 如何在10小時內通過項目和問題驅動的方式教計算機小白編程基礎? Apr 02, 2025 am 07:18 AM

如何在10小時內教計算機小白編程基礎?如果你只有10個小時來教計算機小白一些編程知識,你會選擇教些什麼�...

在Linux終端中使用python --version命令時如何解決權限問題? 在Linux終端中使用python --version命令時如何解決權限問題? Apr 02, 2025 am 06:36 AM

Linux終端中使用python...

如何繞過Investing.com的反爬蟲機制獲取新聞數據? 如何繞過Investing.com的反爬蟲機制獲取新聞數據? Apr 02, 2025 am 07:03 AM

攻克Investing.com的反爬蟲策略許多人嘗試爬取Investing.com(https://cn.investing.com/news/latest-news)的新聞數據時,常常�...

See all articles