1. 程式人生 > >什麼是描述符(descriptor)

什麼是描述符(descriptor)

只要是定義了__get__()__set()____delete()__中任意一個方法的物件都叫描述符。那描述符協議是什麼呢?這個協議指的就是這三個方法。

descr.__get__(self, obj, type=None) --> value

descr.__set__(self, obj, value) --> None

descr.__delete__(self, obj) --> None

那麼描述符有什麼牛逼的? 通常來說Python物件的屬性控制預設是這樣的:從物件的字典(__dict__)中獲取(get),設定(set),刪除(delete),比如:對於例項a

a.x的查詢順序為a.__dict__['x'],然後是type(a).__dict__['x'].如果還是沒找到就往上級(父類)中查詢。描述符就好比是破壞小子,他會改變這種預設的控制行為。究竟是怎麼改變的呢?

想必會你已經猜到了,如果屬性x是一個描述符,那麼訪問a.x時不再從字典__dict__中讀取,而是呼叫描述符的__get__()方法,對於設定和刪除也是同樣的原理。

既然知道他有化腐朽為神奇的這種特點,聰明的你一定能想到的能用在什麼場景下,我用郵件地址的驗證這個簡單的例子來演示他是如何運作的。

class Person(object):
    def __init__(self
, email): self.email = email

現在如果有不安分的小子總想著搞破壞,傳遞一個無效的email過來,如果你不使用描述符你是沒轍的,你別告訴我說你可以在init方法裡面做驗證嘛?老兄,python是一門動態語言,也沒有像我大java一樣擁有私有變數。用一個例子來粉碎你的猜想。

import re
class Person(object):
    def __init__(self, email):
        m = re.match('\[email protected]\w+\.\w+', email)
        if not
m: raise Exception('email not valid') self.email = email

上面這個初始化方法看似完美有缺,如果客戶端能安分的按規則行房,錯了,是行事。就不會出什麼大問題。傳入的無效值也能優雅的以異常的形式警告。

>>> p = test.Person('[email protected]')
>>> p.email
'[email protected]'
>>> p2 = test.Person('dfsdfsdf')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 38, in __init__
    raise Exception('email not valid')
Exception: email not valid
>>>

但是,搗蛋小子來了,他要這樣給p物件賦值email:

>>> p.email = 'sdfsdfsdf'
>>> p.email
'sdfsdfsdf'
>>> p.__dict__
{'email': 'sdfsdfsdf'}
>>>

這時的p.email預設從__dict__讀取值。你看給p傳個火星來的email地址也能接受。這下只有上帝能救你於水火之中,其實上帝就是那個描述符啦。那怎麼把email變成一個描述符啊?當然方式有好幾種:

基於類建立描述符

import re

class Email(object):

    def __init__(self):
        self._name = ''

    def __get__(self, obj, type=None):
        return self._name

    def __set__(self, obj, value):
        m = re.match('\[email protected]\w+\.\w+', value)
        if not m:
            raise Exception('email not valid')
        self._name = value

    def __delete__(self, obj):
        del self._name

class Person(object):
    email = Email()

這下你給他賦值一個火星文看看:

>>> p = Person()
>>> p.email = 'ではないああを行う'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 46, in __set__
    raise Exception('email not valid')
Exception: email not valid
>>>

>>> p.email = '[email protected]'
>>> p.email
'[email protected]'

現在總算是能抵擋住大和民族的呀咩嗲了,再來看看__dict__中有哪些東西:

>>> Person.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Person' objects>, '__module__': 'test', '__weakref__': <attribute '__weakref__' of 'Person' objects>, 'email': <test.Email object at 0x8842fcc>, '__doc__': None})
>>> p.__dict__
{}

嗯,縱使email赫然在列dict中,擁有了描述符後,直譯器對其視而不見,轉而去呼叫描述符中對應的方法。即使是下面的操作方式也是徒勞而已:

>>> p.__dict__['email'] = 'xxxxxx'
>>> p.email
'[email protected]'
>>>

使用property()函式建立描述符

class Person(object):

    def __init__(self):
        self._email = None

    def get_email(self):
        return self._email

    def set_email(self, value):
         m = re.match('\[email protected]\w+\.\w+', value)
         if not m:
             raise Exception('email not valid')
         self._email = value

    def del_email(self):
        del self._email

    email = property(get_email, set_email, del_email, 'this is email property')


>>> p = Person()
>>> p.email
>>> p.email = 'dsfsfsd'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 71, in set_email
    raise Exception('email not valid')
Exception: email not valid
>>> p.email = '[email protected]'
>>> p.email
'[email protected]'
>>>

property()函式返回的是一個描述符物件,它可接收四個引數:property(fget=None, fset=None, fdel=None, doc=None)

  • fget:屬性獲取方法
  • fset:屬性設定方法
  • fdel:屬性刪除方法
  • doc: docstring

採用property實現描述符與使用類實現描述符的作用是一樣的,只是實現方式不一樣。property的一種純python的實現方式如下:

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        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")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        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__)

留心的你發現property裡面還有getter,setter,deleter方法,那他們是做什麼用的呢?來看看第三種建立描述符的方法。

使用@property裝飾器

class Person(object):

    def __init__(self):
        self._email = None

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
         m = re.match('\[email protected]\w+\.\w+', value)
         if not m:
             raise Exception('email not valid')
         self._email = value

    @email.deleter
    def email(self):
        del self._email

>>>
>>> Person.email
<property object at 0x02214930>
>>> p.email = 'lzjun'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 93, in email
    raise Exception('email not valid')
Exception: email not valid
>>> p.email = '[email protected]'
>>> p.email
'[email protected]'
>>>

發現沒有,其實裝飾器property只是property函式的一種語法糖而已,setter和deleter作用在函式上面作為裝飾器使用。

哪些場景用到了描述符

其實python的例項方法就是一個描述符,來看下面程式碼塊:

>>> class Foo(object):
...     def my_function(self):
...        pass
...
>>> Foo.my_function
<unbound method Foo.my_function>
>>> Foo.__dict__['my_function']
<function my_function at 0x02217830>
>>> Foo.__dict__['my_function'].__get__(None, Foo)
<unbound method Foo.my_function>
>>> Foo().my_function
<bound method Foo.my_function of <__main__.Foo object at 0x0221FFD0>>
>>> Foo.__dict__['my_function'].__get__(Foo(), Foo)
<bound method Foo.my_function of <__main__.Foo object at 0x02226350>>

my_function函式實現了__get__方法。描述符也被大量用在各種框架中,比如:django的paginator.py模組,django的model其實也使用了描述符。


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪