神級程序員苦口婆心帶你理解Python對象的屬性!(實用)

編程語言 Python 程序員 技術 潭州python 潭州python 2017-09-10

如果你還在入門糾結,如果你還在苦惱怎麼入門python!小編有個建議,可以加小編弄的一個Python交流基地,大家可以進入交流基地:58937142,裡面新手入門資料,可以說從零到項目實戰,都是可以免費獲取的,還有程序員大牛為各位免費解答問題,熱心腸的小夥伴也是蠻多的。不失為是一個交流的的好地方,小編在這裡邀請大家加入我的大家庭。歡迎你的到來。一起交流學習!共同進步!小編等你!

神級程序員苦口婆心帶你理解Python對象的屬性!(實用)

對於Python對象的屬性是如何讀取的,我一直存在一些疑問。對對象的屬性賦值,什麼時候直接指向一個新的對象,什麼時候會直接拋出AttributeError錯誤,什麼時候會通過Descriptor?Python的 descriptor 是怎麼工作的?如果對a.x進行賦值,那麼a.x不是應該直接指向一個新的對象嗎?但是如果x是一個descriptor實例,為什麼不會直接指向新對象而是執行__get__方法?經過一番研究和實驗嘗試,我大體明白了這個過程。

__getattr__ __getattribute__和__setattr__

對於對象的屬性,默認的行為是對對象的字典(即 __dict__ )進行get set delete操作。比如說,對a.x查找x屬性,默認的搜索順序是a.__dict__[‘x’],然後是type(a).__dict__[‘x’],然後懟type(a)的父類(metaclass除外)繼續查找。如果查找不到,就會執行特殊的方法。

# -*- coding: utf-8 -*- class Foo(object): class_foo = 'world' def __init__(self): self.foo = 'hello' def __getattr__(self, name): print("Foo get attr run..." + self + name) class Bar(Foo): def __getattr__(self, name): print("Bar get attr run..." + name)bar = Bar()print bar.foo # helloprint bar.class_foo # world

__getattr__ 只有在當對象的屬性找不到的時候被調用。

 class LazyDB(object): def __init__(self): self.exists = 5 def __getattr__(self, name): value = ‘Value for %s’ % name setattr(self, name, value) return value
 data = LazyDB() print(‘Before:’, data.__dict__) print(‘foo: ’, data.foo) print(‘After: ‘, data.__dict__) >>> Before: {‘exists’: 5} foo: Value for foo After: {‘exists’: 5, ‘foo’: ‘Value for foo’}

__getattribute __ 每次都會調用這個方法拿到對象的屬性,即使對象存在。

class ValidatingDB(object): def __init__(self): self.exists = 5 def __getattribute__(self, name): print(‘Called __getattribute__(%s)’ % name) try: return super().__getattribute__(name) except AttributeError: value = ‘Value for %s’ % name setattr(self, name, value) return value data = ValidatingDB() print(‘exists:’, data.exists) print(‘foo: ’, data.foo) print(‘foo: ’, data.foo) >>> Called __getattribute__(exists) exists: 5 Called __getattribute__(foo) foo: Value for foo Called __getattribute__(foo) foo: Value for foo

__setattr__ 每次在對象設置屬性的時候都會調用。

判斷對象的屬性是否存在用的是內置函數 hasattrhasattr 是C語言實現的,看了一下源代碼,發現自己看不懂。不過搜索順序和本節開頭我說的一樣。以後再去研究下源代碼吧。

總結一下,取得一個對象的屬性,默認的行為是:

  1. 查找對象的__dict__

  2. 如果沒有,就查找對象的class的__dict__,即 type(a).__dict__['x']

  3. 如果沒有,就查找父類class的__dict__

  4. 如果沒有,就執行 __getattr__ (如果定義了的話)

  5. 否則就拋出 AttributeError

對一個對象賦值,默認的行為是:

  1. 如果定義了 __set__ 方法,會通過 __setattr__ 賦值

  2. 否則會更新對象的 __dict__

但是,如果對象的屬性是一個Descriptor的話,會改變這種默認行為。

Python的Descriptor

對象的屬性可以通過方法來定義特殊的行為。下面的代碼, Homework.grade 可以像普通屬性一樣使用。

class Homework(object): def __init__(self): self._grade = 0 @property def grade(self): return self._grade @grade.setter def grade(self, value): if not (0 <= value <= 100): raise ValueError(‘Grade must be between 0 and 100’) self._grade = value

但是,如果有很多這樣的屬性,就要定義很多setter和getter方法。於是,就有了可以通用的Descriptor。

class Grade(object): def __get__(*args, **kwargs): #... def __set__(*args, **kwargs): #...  class Exam(object): # Class attributes math_grade = Grade() writing_grade = Grade() science_grade = Grade()

Descriptor是Python的內置實現,一旦對象的某個屬性是一個Descriptor實例,那麼這個對象的讀取和賦值將會使用Descriptor定義的相關方法。如果對象的 __dict__ 和Descriptor同時有相同名字的,那麼Descriptor的行為會優先。

# -*- coding: utf-8 -*- class Descriptor(object): def __init__(self, name='x'): self.value = 0 self.name = name def __get__(self, obj, type=None): print "get call" return self.value def __set__(self, obj, value): print "set call" self.value = value class Foo(object): x = Descriptor()foo = Foo()print foo.xfoo.x = 200print foo.xprint foo.__dict__foo.__dict__['x'] = 500print foo.__dict__print foo.x # --------------# output# get call# 0# set call# get call# 200# {}# {'x': 500}# get call# 200

實現了 __get__()__set__() 方法的叫做data descriptor,只定義了 __get__() 的叫做non-data descriptor( 文檔說通常用於method ,這裡我還不太明白)。上文提到,data descriptor優先級高於對象的 __dict__ 但是non-data descriptor的優先級低於data descriptor。上面的代碼刪掉__set__() 將會是另一番表現。

# -*- coding: utf-8 -*- class Descriptor(object): def __init__(self, name='x'): self.value = 0 self.name = name def __get__(self, obj, type=None): print "get call" return self.value class Foo(object): x = Descriptor()foo = Foo()print foo.xfoo.x = 200print foo.xprint foo.__dict__foo.__dict__['x'] = 500print foo.__dict__print foo.x # --------------# output# get call# 0# 200# {'x': 200}# {'x': 500}# 500

如果需要一個“只讀”的屬性,只需要將 __set__() 拋出一個 AttributeError 即可。只定義 __set__() 也可以稱作一個data descriptor。

調用關係

對象和類的調用有所不同。

對象的調用在 object.__getattribute__(),b.x 轉換成 type(b).__dict__['x'].__get__(b, type(b)) ,然後引用的順序和上文提到的那樣,首先是data descriptor,然後是對象的屬性,然後是non-data descriptor。

對於類的調用,由 type.__getattribute__()B.x 轉換成 B.__dict__['x'].get(None, B)。Python實現如下:

def __getattribute__(self, key): "Emulate type_getattro() in Objects/typeobject.c" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v

需要注意的一點是, Descriptor默認是由 __getattribute__() 調用的,如果覆蓋 __getattribute__() 將會使Descriptor失效。

Function,ClassMethod和StaticMethod

看起來這和本文內容沒有什麼關係,但其實Python中對象和函數的綁定,其原理就是Descriptor。

在Python中,方法(method)和函數(function)並沒有實質的區別,只不過method的第一個參數是對象(或者類)。Class的 __dict__ 中把method當做function一樣存儲,第一個參數預留出來作為 self 。為了支持方法調用,function默認有一個 __get__() 實現。也就是說, 所有的function都是non-data descriptor,返回bound method(對象調用)或unbound method(類調用)。用純Python實現,如下。

class Function(object): . . . def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" return types.MethodType(self, obj, objtype)
>>> class D(object):... def f(self, x):... return x...>>> d = D()>>> D.__dict__['f'] # Stored internally as a function<function f at 0x00C45070>>>> D.f # Get from a class becomes an unbound method<unbound method D.f>>>> d.f # Get from an instance becomes a bound method<bound method D.f of <__main__.D object at 0x00B18C90>>

bound和unbound method雖然表現為兩種不同的類型,但是在 C源代碼裡 ,是同一種實現。如果第一個參數 im_self 是NULL,就是unbound method,如果 im_self 有值,那麼就是bound method。

總結:Non-data descriptor提供了將函數綁定成方法的作用。Non-data descriptor將 obj.f(*args) 轉化成 f(obj, *args),klass.f(*args) 轉化成 f(*args) 。如下表。

TransformationCalled from an ObjectCalled from a Class
functionf(obj, *args)f(*args)
staticmethodf(*args)f(*args)
classmethodf(type(obj), *args)f(klass, *args)

可以看到,staticmethod並沒有什麼轉化,和function幾乎沒有什麼差別。因為staticmethod的推薦用法就是 將邏輯相關,但是數據不相關的functions打包組織起來。 通過函數調用、對象調用、方法調用都沒有什麼區別。staticmethod的純python實現如下。

class StaticMethod(object): "Emulate PyStaticMethod_Type() in Objects/funcobject.c"  def __init__(self, f): self.f = f def __get__(self, obj, objtype=None): return self.f

classmethod用於那些適合通過類調用的函數,例如工廠函數等。與類自身的數據有關係,但是和實際的對象沒有關係。例如,Dict類將可迭代的對象生成字典,默認值為None。

class Dict(object): . . . def fromkeys(klass, iterable, value=None): "Emulate dict_fromkeys() in Objects/dictobject.c" d = klass() for key in iterable: d[key] = value return d fromkeys = classmethod(fromkeys) >>> Dict.fromkeys('abracadabra'){'a': None, 'r': None, 'b': None, 'c': None, 'd': None}

classmethod的純Python實現如下。

class ClassMethod(object): "Emulate PyClassMethod_Type() in Objects/funcobject.c"  def __init__(self, f): self.f = f def __get__(self, obj, klass=None): if klass is None: klass = type(obj) def newfunc(*args): return self.f(klass, *args) return newfunc

最後的話

一開始只是對對象的屬性有些疑問,查來查去發現還是官方文檔最靠譜。然後認識了Descriptor,最後發現這並不是少見的trick,而是Python中的最常見對象——function時時刻刻都在用它。從官方文檔中能學到不少東西呢。另外看似平常、普通的東西背後,可能蘊含了非常智慧和簡潔的設計。

相關推薦

推薦中...