什麼是描述符(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 notm: 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)獲取最新文章