Python中__get__, __getattr__, __getattribute__的區別及延遲初始化


本節知識點

  1、__get__, __getattr__, __getattribute__的區別

  2、__getattr__巧妙應用

  3、延遲初始化(lazy property)

1、__get__, __getattr__, __getattribute__的區別

obj.__getattribute__(self, name)

在實例訪問屬性的時候無條件被調用。如果class中定義了__getattr__(),__getattr__()也不會被調用,除非顯示的調用或者沒有訪問到屬性引發AttributeError異常

obj.__getattr__(self, name)

當一般位置找不到屬性時,會調用__getattr__()返回一個值,如果不存在__getattr__()方法則會引發AttributeError異常。

obj.__get__(self, instance, owner)

如果類定義了它,則這個類可以被稱為descriptor(描述符),owner是所有者的類,instance是訪問descriptor的實例,如果不是通過實例訪問,而是通過類訪問的畫,instance則為None。

descriptor的實例自己訪問自己是不會觸發__get__,而會觸發__call__,只有descriptor作為其它類的屬性才有意義。

類里面是默認不會提供__get__()方法的

 

class C:
    a = 'abc'

    def __getattribute__(self, *args, **kwargs):
        print("__getattribute__() is called")
        # print(1, object.__getattribute__(self, *args, **kwargs))
        return object.__getattribute__(self, *args, **kwargs)

    def __getattr__(self, name):
        print("__getattr__() is called")
        return name + " from getattr"

    def __get__(self, instance, owner):
        print("__get__() is called", instance, owner)  # instance 是訪問desciptor的實例
        return self

    def foo(self, x):
        print(x)

    def __call__(self, *args, **kwargs):
        print('__call__() is called', args, kwargs)


class C2:
    d = C()


if __name__ == '__main__':
    c = C()
    c2 = C2()
    print(c.a)  # 1、__getattribute__() is called 2、abc  先調用__getattribute__()方法,然后獲取屬性
    print(c.zzzzzzzz)  # 1、__getattribute__() is called 2、__getattr__() is called 3、zzzzzzzz from getattr
    print(c2.d)  # d是C類的實例,而C因為存在__get__()方法,而變成描述符,訪問文件描述符的實例的時候,默認應該是不走__getattribute__方法,所以也就更不可能調用到__getattr__()方法
    # 1、__get__() is called 2、C2 object 3、C2 4、d指向的實例C object
    print('//////////////////////////////////')
    print(c2.d.a)  # 同上面一樣會先獲取d,走__get__()方法,然后獲取a屬性的時候又會走__getattribute__
    # 1、__get__() is called 2、C2 object 3、C2 4、__getattribute__ 5、abc
    print('..................................')
    print(c2.d.b)  # 繼續上面的邏輯,是描述符,到獲取b屬性,沒有找到走__getattr__()方法返回
    # 1、__get__() is called 2、C2 object 3、C2 4、__getattribute__ 5、__get__ 6、b from getattr
    print('----------------------------------')
    print(c())  # 實例本身調用是調用的call方法
    print('**********************************')
    print(c.c)  # 非文件描述符的還是老思路 getattribute==>getattr
    # 1、__getattribute__ 2、__getattr__ 3、c from getattr

 

參考:https://www.cnblogs.com/saolv/p/6890645.html 並做了簡單修改

 

__getattr__應用 

根據上面的介紹大概可以發現__getattr__函數的作用

在進行屬性查找事,如果在實例跟類上都查找失敗的時候,就會走到__getattr__函數上,如果沒有定義這個函數,就會拋出AttributeError異常。所以,這里大概我們可以人為__getattr__方法是屬性查找的最后一個關卡。

示例1

很簡單的示例,通過__getattr__像訪問屬性一樣訪問鍵值對

class ObjectDict(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)  # 繼承父類dict的構造方法

    def __getattr__(self, name):
        value = self[name]  # self ==> {'asf': {'a': 1}, 'd': True}
        if isinstance(value, dict):
            value = ObjectDict(value)
        return value


if __name__ == '__main__':
    od = ObjectDict(asf={'a': 1}, d=True)  # 實例化對象od ==>  {'asf': {'a': 1}, 'd': True}
    print(od.asf)       # {'a': 1}
    print(od.asf.a)     # 1
    print(od.d)         # True

實例化對象od,通過.attribute的方式來獲取key對應的value

 

示例2

class WidgetShowLazyLoad:
    def fetch_complex_attr(self, attrname):
        return attrname

    def __getattr__(self, name):
        if name not in self.__dict__:  # 沒在__dict__字典內找到key
            self.__dict__[name] = self.fetch_complex_attr(name)  # 添加attribute鍵值對
        return self.__dict__[name]


if __name__ == '__main__':
    w = WidgetShowLazyLoad()
    print('before', w.__dict__)  # 剛開始實例化的時候,__dict__是空的字典
    w.lazy_loaded_attr  # 屬性查找,沒找到,調用__getattr__方法
    print('after', w.__dict__)  # {'lazy_loaded_attr': 'lazy_loaded_attr'}

結果:

before {}
after {'lazy_loaded_attr': 'lazy_loaded_attr'}

這里的核心,就是利用了__getattr__屬性的機制,在查找不存在的屬性的時候進行改寫,動態懶加載出來一個字典。這個例子就是類實例的惰性初始化

 

示例3

import functools
class lazy_attribute:
    """ A property that caches itself to the class object. """
    def __init__(self, func):
        functools.update_wrapper(self, func, updated=[])
        self.getter = func  # complex_attr_may_not_need

    def __get__(self, obj, cls):  # 調用類本身, obj自身調用為空
        value = self.getter(cls)  # complex_attr_may_not_need(Widget)
        setattr(cls, self.__name__, value)  # self ==> complex_attr_may_not_need=lazy_attribute(complex_attr_may_not_need)
        # self 所以是lazy_attribute的對象,裝飾器的原理就是complex_attr_may_not_need=lazy_attribute實例化對象,所以self.__name__就是complex_attr_may_not_need
        # {'complex_attr_may_not_need': 332833500}
        return value

class Widget:
    @lazy_attribute  # complex_attr_may_not_need=lazy_attribute(complex_attr_may_not_need)
    def complex_attr_may_not_need(clz):
        print('complex_attr_may_not_need is needed now')
        return sum(i*i for i in range(1000))

if __name__ == '__main__':
    print(Widget.__dict__.get('complex_attr_may_not_need'))  # <__main__.lazy_attribute object at 0x02B12450>
    Widget.complex_attr_may_not_need                        # complex_attr_may_not_need is needed now
    print(Widget.__dict__.get('complex_attr_may_not_need'))  # 332833500

上面的代碼里面,用到了一個類裝飾器,它的使用其實也還沒沒有離開裝飾器的基礎定義,被裝飾的函數相當於

complex_attr_may_not_need=lazy_attribute(complex_attr_may_not_need)

這相當於實例化了這個函數,所以可以發現,__init__方法內有一個func參數。

functool.update_wrapper

使用update_wrapper(),從原始對象拷貝或加入現有對象

它可以把被封裝函數的__name__、__module__、__doc__和 __dict__都復制到封裝函數去

所以上述過程的執行流程可以理解為:

1、print(Widget.__dict__.get('complex_attr_may_not_need'))

此方法實際上是獲取Wdiget方法的__dict__字典內的complex_attr_may_not_need的key,但是因為complex_attr_may_not_need這個方法被類裝飾器lazy_attribute裝飾(裝飾器的本質,其實就是把被裝飾的方法傳進去),所以此時的結果應該是指向lazy_attribute的實例化對象的。

2、Widget.complex_attr_may_not_need

首先需要注意的是,這個方法傳入的參數complex_attr_may_not_need(clz)。類裝飾器初始化的時候,傳入了func就是被裝飾的方法,並賦值給了實例屬性getter,此時lazy_attribute里面有__get__()方法,所以lazy_attribute是一個描述符descriptor。因為是外部的Widget類調用的complex_attr_may_not_need方法,所以此時會先運訓__get__方法。value = self.getter(cls),其中self.getter=func即complex_attr_may_not_need方法,cls是調用的類本身即Widget,變成value = self.complex_attr_may_not_need(Widget),執行此方法,打印出complex_attr_may_not_need is needed now,value=計算的和,並內部設置了Widget類的complex_attr_may_not_need對應的value為計算和。

3、print(Widget.__dict__.get('complex_attr_may_not_need'))

現在這一句就很好理解了,取值並打印。

關於setattr不理解可以看下下面隨手寫的案例

class Foo:

    def __init__(self):
        setattr(Foo, 'aaa', 'bbb')
    def aaa(self):
        return 'a'
f = Foo()
print(Foo.__dict__)

執行一次,再把f = Foo()注釋掉執行一下,看看aaa的值就知道了。

實例4

class adaptee:
    def foo(self):
        print('foo in adaptee')

    def bar(self):
        print('bar in adaptee')

class adapter:
    def __init__(self):
        self.adaptee = adaptee()

    def foo(self):
        print('foo in adapter')
        self.adaptee.foo()

    def __getattr__(self, name):
        return getattr(self.adaptee, name)


if __name__ == '__main__':
    a = adapter()
    a.foo()  # 1、foo in adapter 2、foo in adaptee
    a.bar()  # 1、bar in adaptee

執行a.foo()應該是沒什么問題的,順序執行而已,但是在執行a.bar()的時候,因為adapter里面沒有此屬性,所以會走到最后一道關卡__getattr__方法,所以就很好理解了。

__getattr__使得實現adapter wrapper模式非常容易,我們都知道“組合優於繼承”,__getattr__實現的adapter就是以組合的形式。

實例5

class AlgoImpA:
    def __init__(self):
        self.obj_attr = 'obj_attr in AlgoImpA'

    def foo(self):
        print('foo in AlgoImpA')

    def bar(self):
        print('bar in AlgoImpA')


class AlgoImpB(object):
    def __init__(self):
        self.obj_attr = 'obj_attr in AlgoImpB'

    def foo(self):
        print('foo in AlgoImpB')

    def bar(self):
        print('bar in AlgoImpB')


class Algo:
    def __init__(self):
        self.imp_a = AlgoImpA()
        self.imp_b = AlgoImpB()
        self.cur_imp = self.imp_a

    def switch_imp(self):
        if self.cur_imp == self.imp_a:
            self.cur_imp = self.imp_b
        else:
            self.cur_imp = self.imp_a

    def __str__(self):
        return 'Algo with imp %s' % str(self.cur_imp)

    def __getattr__(self, name):
        return getattr(self.cur_imp, name)


if __name__ == '__main__':
    algo = Algo()  # imp_a

    print(algo)  # Algo with imp IMPA
    print(algo.obj_attr)  # obj_attr in AlgoImpA
    algo.foo()  # foo in AlgoImpA

    algo.switch_imp()

    print(algo)  # Algo with imp IMPB
    print(algo.obj_attr)  # obj_attr in AlgoImpB
    algo.bar()  # bar in AlgoImpB

 3、延遲初始化(lazy property)

概念

Python對象的延遲初始化是指,當它第一次被創建時才進行初始化,或者保存第一次創建的結果,然后每次調用的時候直接返回結果。

延遲初始化主要是用於提高性能,避免浪費計算,並減少程序的內存需求。

property

首先,再來回顧下property的用法,property可以將屬性的訪問轉變成方法的調用

class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14 * self.radius ** 2


c = Circle(4)
print(c.radius)
print(c.area)

area被定義成一個方法的形式,但是加上@property后,可以直接用c.area來調用,當成屬性訪問。

但這樣寫面臨的一個問題就是,每次調用c.are都會去計算一次,浪費cpu,怎么養才能只計算一次呢?這就是延遲初始化lazy property

lazy property

這里,我們趁熱打鐵,使用文件描述符來來實現。

class lazy(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, cls):
        val = self.func(instance)
        setattr(instance, self.func.__name__, val)
        return val


class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    @lazy
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2

c = Circle(4)
print(c.radius)
print(c.area)
print(c.area)

結果:

4
evalute
50.24
50.24

可以發現evalute只輸出一次。在lazy類里面,因為定義了__get__()方法,所以它是一個描述符。當第一次執行c.are時,python解釋器會先從_c._ditc__中查找,沒有找到就會去Circle.__dict__中進行查找,這個時候因為area被定義為描述符,所以調用__get__方法。

上面已經鋪墊過def __get__(self, instance, cls)里面三個參數的代表什么,所以很明了val = self.func(instance) ,是執行了area方法,並返回結果,最后setattr完成了賦值操作。這樣相當於設置c.__dict__['area']=val

當我們再次調用c.area時,直接從c.__dict__中進行查找,這時就會直接返回之前計算好的值了。

這里再提供另一種方法

def lazy_property(func):
    attr_name = "_lazy_" + func.__name__

    @property
    def _lazy_property(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, func(self))
        return getattr(self, attr_name)

    return _lazy_property


class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    @lazy_property
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2
c = Circle(4)
print("before first visit")
print(c.__dict__  )
c.area
print("after first visit")
print(c.__dict__)

結果:

before first visit
{'radius': 4}
evalute
after first visit
{'radius': 4, '_lazy_area': 50.24}

表示,其實樓主也還是不太懂,等看明白了再來注解。

參考自http://python.jobbole.com/85553/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM