python屬性和方法的訪問

以下的討論都限於,新式類(個人認爲最好限於新式類)

1,python一切皆對象

除了object和type,兩個逆天的存在,不是說它們不是,而是它們更高級一點。
第一個區分的就是對象之間的關係,由__bases__和__class__兩個主線來關聯完成。

class AA(object):    
    x = 'x from AA'   
class BB(type):    
    x = 'x from BB' 
	y = 'y from BB'
class CC(AA):    
    __metaclass__ = BB
	z = 'z from CC'

c = CC()
print CC.__bases__ # 打印得到CC的師父    # (__mian__.AA,)
print CC.__class__ # 打印得到CC的爸爸    # __mian__.BB
print c.__class__ # 打印得到c的爸爸	 # __mian__.CC 

把實例化稱作爸爸,即__class__稱作爸爸,也就是c = CC()中,CC是c的爸爸,c由CC實例化得到

把繼承稱作師父,即__base__稱作師父,也就是 class CC(AA): pass 中,AA是CC的師父,CC繼承自AA

顯然對於一個對象來說,爸爸只能有一個,而且必須有一個(可以是默認的爸爸),而師父可以有多個,也可以沒有。


本文將着重討論c和CC的屬性訪問區別,因爲子c++/java裏面,我們一般只考慮c來屬性訪問,很少用CC來訪問屬性。

python一切皆對象的意思是:如果說對象有爸爸,那麼Python裏面所有東西都有爸爸;如果對象可以有自己的屬性,那麼python所有東西都可以有自己的屬性。萬物皆平等(除了object和type),CC相對於c來說,並沒有什麼特殊,都是對象而已,都有屬性訪問問題。

在BB和CC和c的關係中,我們稱BB是meta class object,稱CC是class object,稱c是instance Object。即BB是c的爺爺,CC是c的爸爸。

但在BB和CC的關係中,我們稱BB是class object,稱CC是instance Object。即BB是CC的爸爸。

但在type和BB的關係中,我們稱type是class object,稱BB是instance Object。即type是BB的爸爸。

爲什麼要區分這些呢,下面是兩個訪問屬性的例子,後續的文章也主要是爲了區分這兩種屬性訪問方式。

print CC.x # 請問是訪問BB中的x,還是AA中的x。即訪問爸爸的技能,還是師父的技能。
print c.y # 報錯,CC只能遺傳CC自己的技能(z)和師父AA的技能(x),不能遺傳父親BB的技能(x和y)

以前我們都是站在c的角度來看待AA的,現在我們要站在CC的角度來看待AA,看看AA這個師傅對CC會有什麼影響。 打個不太恰當的比方:

一個對象CC生下來,就擁有了父親BB的技能(指屬性x和y),

但是可以通過後天向師傅AA學習新的技能(指屬性x),來增加或覆蓋父親BB的技能,

也可以自己修煉技能(指屬性z)來增加或覆蓋父親BB的技能,

CC在生孩子的時候,只能遺傳給孩子c,自己CC的技能和師父AA的技能。

2,屬性的分類(作者自己的分類,僅供參考)

Python-provided屬性和用戶屬性:
Python-provided屬性大致就是指__class__、__bases__、__dict__這類屬性。
一個屬性是否是Python-provided屬性根據對象的不同而不同,比如__base__對於classObj是,但給instanceObj強行添加__base__的不算。
用戶屬性就是用戶自己添加的屬性,一般出現在各個對象自己的__dict__中。每個對象只可以增加刪除修改自己__dict__裏面的屬性,即InstanceObj中的屬性操作,不影響其ClassObj和BaseObj的__dict__中的屬性。

描述符(descriptor)、數據描述符(data descriptor)、非數據描述符(non-data descriptor)、普通屬性(others):
描述符也是對象,必須擁有__get__方法,__set__和__delete__方法可選,且方法的參數符合一定的規範。
數據描述符同時擁有__get__和__set__。非數據描述符只有__get__。非數據描述符一般就是指方法屬性。

3,屬性訪問中調用的方法:__getattribute__和__getattr__

屬性的讀操作先是調用__getattribute__方法,(如果過有)找不到就再調用__getattr__方法。
第一個問題是__getattribute__和__getattr__是什麼?
print a.b.c.d; 這段代碼有三次讀屬性操作。
a.b.c.d = e; 這段代碼有兩次讀屬性操作,一次寫屬性操作。
一般來說,一個讀的點操作,就會調用一次__getattribute__方法,當__getattribute__找不到屬性時,如果有,就調用並返回__getattr__方法的結果,否則拋出異常。
第二個問題是__getattribute__和__getattr__從哪裏來?
它們都從對象的類型中找,以及對象類型的基類中找。反正最後object和type中是一定會含有__getattribute__的。找到第一個則返回。即先在obj.__class__中找,再在obj.__class__.__bases__中找。

'a.__class__==AA、AA.__class__==object、c.__class__==CC、CC.__class__==DD、DD.__base__==BB'
class AA(object): 
    bar = 123
    def foo(self):
        print self.bar
    def __getattribute__(self, *args, **kwargs):  
        print("__getattribute__() in AA")
        return object.__getattribute__(self, *args, **kwargs)
    def __getattr__(self, name):  
        print("__getattr__() in AA ")  
class BB(type):
    def __getattribute__(self, *args, **kwargs):  
        print("__getattribute__() in BB")
        return type.__getattribute__(self, *args, **kwargs)
    def __getattr__(self, name):  
        print("__getattr__() in BB ")
class DD(BB):
    def __getattr__(self, name):  
        print("__getattr__() in DD ")
class CC(AA):
    __metaclass__ = DD
    def __getattribute__(self, *args, **kwargs):  
        print("__getattribute__() in CC")
        return object.__getattribute__(self, *args, **kwargs)
    def __getattr__(self, name):  
        print("__getattr__() in CC ") 
a = AA()
c = CC()
print c.attr  # __getattribute__() in CC 和 __getattr__() in CC
print CC.attr # __getattribute__() in BB 和 __getattr__() in DD
print a.attr # __getattribute__() in AA 和 __getattr__() in AA
print a.foo() # 調用兩次__getattribute__() in AA,一次是爲了找'foo',一次是執行 print self.bar
print AA.attr # 拋出異常,因爲AA對象的類型,也就是AA.__class__ == type,沒有__getattr__方法兜底
print c.__getattribute__('attr')
# 由CC中的__getattribute__負責找'__getattribute__'屬性,在CC中找到,並進行調用。
print CC.__getattribute__(c,'attr')
# 由BB中的__getattribute__負責找'__getattribute__'屬性,在CC中找到,並進行調用。

object和type中的__getattribute__方法不一樣,且會對參數做嚴格的檢查。c用的是object的__getattribute__,
CC用的是type的__getattribute__。所以它們找到並返回的'__getattribute__'屬性不一樣,
一個是綁定後的,一個是未綁定,所以調用的參數也不一樣。
默認的對__getattr__的調用不是在__getattribute__中,所以上面兩個代碼最終執行找到的'__getattribute__'方法,都會因爲沒有找到屬性而拋出異常。

4,__getattribute__詳解,讀屬性訪問優先級

描述符的性質、方法與綁定函數的轉換等機制都由__getattribute__方法來實現。
下面的步驟講的是__getattribute__查找的步驟(不考慮__slots__的情況,object和type中的__getattribute__方法有些細微的差別,畢竟一個是針對instanceObj,一個是針對classObj的訪問):
1):如果是特殊屬性,如Python-provided屬性,特殊代碼處理並返回。
2):依次在obj.__class__.__dict__和obj.__class__.__bases__.__dict__中查找,
        找到第一個匹配的屬性,停止查找。如果是數據描述符,則調用並返回它的__get__方法的結果。
        否則沒找到或找到的不是數據描述符,都進入3。
3):依次在obj.__dict__和obj.__bases__.__dict__中查找,
        找到第一個匹配的屬性,停止查找。如果是描述符(兩種描述符都可以),則調用並返回它的__get__方法的結果。
        如果不是描述符則直接返回該對象,如果沒有找到,則進入4。(糾正:object的__getattribute__方法中,找到描述符直接返回,並不會調用它的__get__方法)     
4):依次在obj.__class__.__dict__和obj.__class__.__bases__.__dict__中查找,
        找到第一個匹配的屬性,停止查找。如果是描述符(兩種描述符都可以),則調用並返回它的__get__方法的結果。
        如果不是描述符則直接返回該對象,如果沒有找到,則進入5。

5):拋出異常AttributeError。

class DD(object):   
    'data descriptor'  
    def __init__(self,name):  
        self._name = name  
    def __get__(self, obj, cls=None): # ,', obj:', obj,', cls:', cls  
        print 'get in DD, name:', self._name  
    def __set__(self, obj, val):   
        print 'set in DD, name:', self._name  
class Non_DD(object):   
    'non-data descriptor'  
    def __init__(self,name):  
        self._name = name  
    def __get__(self, obj, cls=None):   
        print 'get in Non_DD, name:', self._name  
class AA(object):  
    x = DD('aa1')  
    y = Non_DD('aa2')  
    z = DD('aa3')  
    m = DD('aa4')  
    n = DD('aa5')  
class BB(type):  
    x = DD('bb1')  
    y = DD('bb2')  
    z = Non_DD('bb3')  
    n = Non_DD('bb4')  
    k = DD('bb5')  
    t = Non_DD('bb6')  
    e = 'bb7'  
class CC(AA):  
    __metaclass__ = BB  
    x = DD('cc1')  
    y = 'cc2'  
    z = Non_DD('cc3')  
    m = 'cc4'  
    e = Non_DD('cc7')  
  
c = CC() 
print '--------c的屬性訪問調用object中的__getattribute__方法--------'      
print c.x # 步驟 2  
print c.y # 步驟4  
print c.z # 步驟 4  
print c.m # 步驟 4  
print c.n # 步驟 2  
c.__dict__['n'] = "c.__dict__['n']"  
print c.n # 步驟 2  
c.__dict__['m'] = "c.__dict__['m']"  
print c.m # 步驟 3  
c.foo = Non_DD('foo in c obj')
print c.foo # 步驟 3
print '--------CC的屬性訪問調用type中的__getattribute__方法--------'  
print CC.x # 步驟 2  
print CC.y # 步驟 2  
print CC.z # 步驟 3  
print CC.m # 步驟 3  
print CC.n # 步驟 3  
CC.n = 'CC.n'  
print CC.n # 步驟 3  
print CC.t # 步驟 4  
print CC.e # 步驟 3
CC.foo = Non_DD('foo in CC obj')
print CC.foo # 步驟 3
只有第2步中找到的數據描述符,纔是真正的數據描述符,才能執行完整的數據描述符機制。

其它步驟中描述符和普通屬性的優先級一樣,只是會額外調用描述符的get方法。

5,寫屬性和__setattr__

python寫屬性都直接調用__setattr__,沒有__setattribute__方法。
__setattr__中步驟詳解(不考慮__slots__的情況):
1):依次在obj.__class__.__dict__和obj.__class__.__bases__.__dict__中查找,
        找到第一個匹配的屬性,停止查找。如果是數據描述符,則調用並返回它的__set__方法的結果。
        否則沒找到或找到的不是數據描述符,都進入2。
2):在obj.__dict__中插入該屬性,並賦值。效果類似於obj.__dict__['varname'] = value
這裏的特殊屬性(Python-provided屬性)不在特殊。

6,普通方法、類方法和靜態方法的實現和裝飾器

前面講過,非數據描述符基本上可以說是指方法。那麼非數據描述符的訪問處理方式,就是調用並返回它的__get__方法的結果。我們下面看看如何使用裝飾器配合非數據描述符的訪問機制,來實現區分類方法、靜態方法和普通方法。
class myclassmethod(object):
    def __init__(self,method):
        self._classmethod=method
    def __get__(self,obj=None,cls=None):
        print 'myclassmethod: **** ',obj,cls,' **** '
        if cls == None:
            cls = type(obj)
        def _deco(*args,**kwargs):
            return self._classmethod(cls,*args,**kwargs)
        return _deco
class mystaticmethod(object):
    def __init__(self,method):
        self._staticmethod=method
    def __get__(self,obj=None,cls=None):
        print 'mystaticmethod: **** ',obj,cls,' **** '
        def _deco(*args,**kwargs):
            return self._staticmethod(*args,**kwargs)  
        return _deco
class method2bounded_function(object): 
    def __init__(self,method):
        self._normalmethod=method
    def __get__(self,obj=None,cls=None):
        print 'normal function: **** ',obj,cls,' **** '
        def _deco(*args,**kwargs):
            if obj!=None:
                return self._normalmethod(obj,*args,**kwargs) 
            elif args and isinstance(args[0],cls): # 對應的是從classObj中直接調用方法
                return self._normalmethod(*args,**kwargs) 
            else:
                print 'raise TypeError: unbound method'
        return _deco
        
class AA(object):
    @method2bounded_function
    def foo1(obj):# 不用self爲了和下面的cls做對比
        print obj
        print 'normal method'
    @classmethod
    def foo2(cls):
        print cls
        print 'the built-in classmethod decorator'
    @myclassmethod
    def foo3(cls):
        print cls
        print 'my classmethod decorator'
    @staticmethod
    def bar2():
        print 'the built-in staticmethod decorator'
    @mystaticmethod
    def bar3():
        print 'my staticmethod decorator'

a = AA() 
print repr(a)
a.foo1() # 調用object中的__getattribute__,傳給get的參數:obj和type(obj)
a.foo2()
a.foo3()
a.bar2()
a.bar3()
print repr(AA)
AA.foo1() # 調用type中的__getattribute__,傳給get的參數:None和obj
AA.foo1(a)
AA.foo1(123)# get方法,會對參數進行檢查
AA.foo2()
AA.foo3()
AA.bar2()
AA.bar3()
裝飾器知識點補充:
@classmethod
def foo2(cls):
    pass
相當於、等價於:foo2 = classmethod(foo2)
前面忘記講__getattribute__方法是如何調用非數據描述符的__get__方法。
對於object中的__getattribute__方法,調用Non_DD.__get__(obj,type(obj)),
對於type中的__getattribute__方法,調用Non_DD.__get__(None,obj),它們傳給get方法的參數不同。

對於靜態方法來說只需要__get__方法將原始參數傳給原方法即可,

對於類方法來說,__get__方法需要保證將cls和原始參數傳給原方法即可,

對於普通方法來說,__get__方法需要保證將obj和原始參數傳給原方法。

object和type中的__getattribute__方法的區別可能有很多,本文暫時只討論這一個。

可以看到每次返回的都是封裝後的方法,所以如果測試方法的id,則每次都不一樣,如下所示:

print id(a.foo1) # 代碼要一行一行的執行
print id(a.foo1)
print id(a.foo1)

7,MRO(method resolution order)和C3算法

前面說的查找obj.__bases__.__dict__中基類的排序、線性化問題是由C3算法解決的。

首先對於序列 [ C1, C2, C3…, CN ]  來說:

頭 head = C1,尾 tail = C2C3…CN。

則C3算法:

L[C(B1B2…BN)] = C + merge(L(B1),L(B2),…,L(BN), B1B2…BN),

其中C是本次考慮的類對象,(B1B2…BN)類C的基類列表,按照定義時的順序。

merge函數:

從merge中從左往右,尋找不存在於任何tail中的head,取出來,然後從頭再開始尋找,直至爲空。否則,C3算法將會拒絕創建類C並拋出一個錯誤。

MRO需要滿足局部優先級和單調性兩個條件:
局部優先級::class Z(F,E): pass ,那麼  F 的方法應該優先於  E 的方法。
單調性::在C中基類的線性化中,假如C1優先於C2,那麼在C的任何子類中基類的線性化中,C1優先於C2。

class __metaclass__(type):
    "All classes are metamagically modified to be nicely printed"
    __repr__ = lambda cls: cls.__name__

class ex_1(object):
    "Serious order disagreement" #From Guido
    O = object
    class F(O): pass
    class E(F): pass
    try:
        class Z(F,E): pass #creates Z(A,B) in Python 2.2
    except TypeError,e:
        print 'error: ',TypeError,e
class ex_2(object):
    "Serious order disagreement" #From Guido
    O = object
    class X(O): pass
    class Y(O): pass
    class A(X,Y): pass
    class B(Y,X): pass
    try:
        class Z(A,B): pass #creates Z(A,B) in Python 2.2
    except TypeError,e:
        print 'error: ',TypeError,e
class ex_5(object):
    "My second example"
#     O = object
    class O: pass
    class F(O): pass
    class E(O): pass
    class D(O): pass
    class C(D,F): pass
    class B(E,D): pass
    class A(B,C): pass
class ex_6(object):
    "My first example"
#     O = object
    class O: pass
    class B(O): pass
    class A(B): pass
    class D(B): pass
    class N(O): pass
    class M(N): pass
    class Z(A,D,M): pass
class ex_9(object):
#     O = object
    class O: pass
    class A(O): pass
    class B(O): pass
    class C(O): pass
    class D(O): pass
    class E(O): pass
    class K1(A,B,C): pass
    class K2(D,B,E): pass
    class K3(D,A): pass
    class Z(K1,K2,K3): pass

def merge(seqs):
    print '\n\nCPL[%s]=%s' % (seqs[0][0],seqs),
    res = []; i=0
    while 1:
        nonemptyseqs=[seq for seq in seqs if seq]
        if not nonemptyseqs: return res
        i+=1; print '\n',i,'round: candidates...',
        for seq in nonemptyseqs: # find merge candidates among seq heads
            cand = seq[0]; print ' ',cand,
            nothead=[s for s in nonemptyseqs if cand in s[1:]]
            if nothead: cand=None #reject candidate
            else: break
        if not cand: raise "Inconsistent hierarchy"
        res.append(cand)
        for seq in nonemptyseqs: # remove cand
            if seq[0] == cand: del seq[0]

def mro(C):
    "Compute the class precedence list (mro) according to C3"
    return merge([[C]]+map(mro,C.__bases__)+[list(C.__bases__)])
def print_mro(C):
    print '\nMRO[%s]=%s' % (C,mro(C))

print_mro(ex_5.A)
print_mro(ex_6.Z)
print_mro(ex_9.Z)
ex_5.A舉例:
L[O] = O
L[F] = L[F(O)] = FO
L[E] = L[E(O)] = EO
L[D] = L[D(O)] = DO
L[C] = L[C(DF)] = C+L[D]+L[F] = C+DO+FO = CDFO
L[B] = L[B(ED)] = B+L[E]+L[D] = B+EO+DO = BEDO
L[A] = L[A(BC)] = A+L[B]+L[C] = A+BEDO+CDFO 
     = A[BEDO+CDFO] = AB[EDO+CDFO] = ABE[DO+CDFO]
     = ABEC[DO+DFO] = ABECD[O+FO] = ABECDF[O+O]
     = ABECDFO
ex_1 和  ex_2  都會被拒絕創建,然後拋出異常。
ex_5的是   A, B, E, C, D, F, O
ex_6的是   Z, A, D, B, M, N, O  
記住每次取,是從頭開始取,而不是接着取,所以,M的類別會到最後 ,再被取走
切記:MRO的C3方法,不等價於對有向圖進行拓撲排序。

8,super

在類的定義中訪問基類的屬性,可以直接通過指明基類進行訪問,也可以通過super類訪問,兩種方法。
指明基類調用相對於super調用的缺點:1)這些寫一是死板,如果基類需要改動,初始化代碼也需要改動。2)在多繼承中父類的代碼可能被執行多次。
但使用super訪問,初始化不僅完成了所有的基類的調用,而且保證了每一個基類的初始化函數只調用一次。調用基類的順序是按照MRO中的順序來的,super(B, self).func的
調用並不一定是調用當前類的父類的func函數,這點需要注意。
super調用有個缺點就是當基類方法的簽名不一致時,就比較麻煩了。
混用super類和指明基類對象是一個危險行爲,這可能導致應該調用的基類方法沒有調用或者一個基類方法被調用多次。

9,__dict__、dir()、__slots__

不是所有的對象都有__dict__屬性。例如,你在一個類中添加了__slots__屬性,
那麼這個類的實例將不會擁有__dict__屬性,但是dir()仍然可以找到並列出它的實例所有有效屬性,所以__dict__不是唯一獲取對象屬性的方式。

dir()所做的不是查找一個對象的__dict__屬性(這個屬性有時甚至都不存在),它使用的是對象的繼承關係來反饋一個對象的完整的有效屬性。

class AA(object):
    __slots__ = ('name', 'age')
    name = 123
    gender = 1259
class BB(AA):
    __slots__ = ('height', 'weight')
    height  = 123
    gender = 1259
class CC(AA):
    height  = 123
    gender = 1259
AA中__slots__,name,gender都是隻讀的,age可寫
BB中__slots__,name,gender,height都是隻讀的,age和weight可寫
CC中讀寫都可以,不過是在自己的__dict__中(不完全正確)。
所有的slot是對instanceObj限制的,不是對classObj限制的。
classObj中定義了__slots__,則instanceObj只能訪問__slots__中的屬性和已有的類屬性,且類屬性只讀。如果__slots__和類屬性衝突,則以類屬性爲準。
通過AA.newvar = 123 添加的屬性都是類屬性,即存在於類AA的__dict__中,a = AA(); a.newvar = 123;添加的是實例屬性,即存在於實例a的__dict__中。

c = CC()
c.age = 25

但是任何對象的__dict__中竟然都找不到'age':25的鍵值對!?

10,其他

前面一直說__getattribute__調用描述符的__get__方法。這裏需要非常注意的是__get__方法來自哪裏?

經過測試可知來自對象的類型類,也就是obj.__class__所指的對象中。如下代碼所示:

'----測試get方法的來源---'
def __get__(*args,**kwargs):
    print 'get in foo',args,kwargs   
def foo(*args,**kwargs):
    print 'foo',args,kwargs
class Non_DD(object): 
    'non-data descriptor'
    def __init__(self,name):
        self._name = name
    def __get__(self, obj, cls=None): 
        print 'get in Non_DD, name:', self._name    
class A(object):
    def test(self):
        def __get__(*args,**kwargs):
            print 'get in test',args,kwargs
        print 'test'
foo.__get__ = __get__
A.foo = foo
A.bar = Non_DD('bar in A')
a = A()
A.test(a)
A.foo(a) # 可以看到A對A中的foo和test的訪問,都沒有調用實例級別的__get__方法。
print A.bar # 可以看到A對A中的bar的訪問,調用對象bar的類型級別的__get__方法,即Non_DD中的__get__方法。
a.test()
a.foo() # 可以看到a對A中的foo和test的訪問,都沒有調用實例級別的__get__方法。
print a.bar # 可以看到a對A中的bar的訪問,調用對象bar的類型級別的__get__方法,即Non_DD中的__get__方法。
a.foo = foo
a.foo() # 可以看到a對a中的foo的訪問,連foo類型級別的__get__方法都沒有調用,即沒有對foo進行封裝。
a.bar = Non_DD('bar in a')
print a.bar # 可以看到a對a中的bar的訪問,連bar類型級別的__get__方法都沒有調用,即Non_DD中的__get__方法。

'----測試函數的get屬性---'
def foo(*args,**kwargs):
    def __get__(*args,**kwargs):
        print 'get in foo',args,kwargs  
    print 'foo',args,kwargs
    
print foo.__dict__ # 並沒有__get__方法
foo.__get__ = __get__
print foo.__dict__ # 有__get__方法
print A.test.__dict__ # 並沒有__get__方法
A.test.__get__ = __get__
print A.test.__dict__ # 並沒有__get__方法,因爲每次都返回的是新的封裝後的test對象

'----測試函數的object和type中的__getattribute__---'
A.bar = Non_DD('bar in A')
print A.bar
print type.__getattribute__(A,'bar') # 調用Non_DD的__get__方法
print object.__getattribute__(A,'bar') # 沒有調用Non_DD的__get__方法,直接返回Non_DD對象
print object.__getattribute__(A,'bar').__get__(a)


參考鏈接:

http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html

http://www.cnblogs.com/lovemo1314/archive/2011/05/03/2035005.html


(如果有什麼說的不對的地方,歡迎大家多多批評指正。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章