1. 程式人生 > 實用技巧 >python 的魔法方法

python 的魔法方法

這裡只分析幾個可能會常用到的魔法方法,像__new__這種不常用的,用來做元類初始化的或者是__init__這種初始化使用的 每個人都會用的就不介紹了。

其實每個魔法方法都是在對內建方法的重寫,和做像裝飾器一樣的行為。理解這個道理 再嘗試去理解每個細節裝飾器會比較方便。

關於__str__和__repr__:

直接上例子:

class Test(object):
    def __init__(self, world):
        self.world = world

    def __str__(self):
        return 'world is %s str' % self.world

    def __repr__(self):
        return 'world is %s repr' % self.world

t = Test('world_big')
print str(t)
print repr(t)

output:

world is world_big str
world is world_big repr

其實__str__相當於是str()方法 而__repr__相當於repr()方法。str是針對於讓人更好理解的字串格式化,而repr是讓機器更好理解的字串格式化。

其實獲得返回值的方法也很好測試,在我們平時使用ipython的時候,在不使用print直接輸出物件的時候,通常呼叫的就是repr方法,這個時候改寫repr方法可以讓他方便的輸出我們想要知道的內容,而不是一個預設內容。

關於__hash__和__dir__:

其實在實際應用中寫了這麼久python,也沒有用到需要這兩個方法出現的地方,但是在有些庫裡面是有看到過。

__hash__是hash()方法的裝飾器版本,而__dir__是dir()的裝飾器版本。

上程式碼展示一下__hash__用法:

class Test(object):
    def __init__(self, world):
        self.world = world


x = Test('world')
p = Test('world')
print hash(x) == hash(p)
print hash(x.world) == hash(p.world)


class Test2(object):
    def __init__(self, song):
        self.song = song

    def __hash__(self):
        return 1241

x = Test2('popo')
p = Test2('janan')

print x, hash(x)
print p, hash(p)

output:

False
True
<__main__.Test2 object at 0x101b0c590> 1241
<__main__.Test2 object at 0x101b0c4d0> 1241

可以看到這裡的hash()方法總是會返回int型的數字。可以用於比較一個唯一的物件,比方說一個不同記憶體的object不會相當,而相同字串hash之後就會相等。然後我們通過修改__hash__方法來修改hash函式的行為。讓他總是返回1241,也是可以輕鬆做到的。

另外一個方法是dir(),熟悉python的人都知道dir()可以讓我們檢視當前環境下有哪些方法和屬性可以進行呼叫。如果我們使用dir(object)語法,可以獲得一個物件擁有的方法和屬性。同樣的道理如果我們在類中定義了__dir__(),就可以指定哪些方法和屬效能夠被dir()方法所檢視查詢到。道理一樣我這裡不再貼出程式碼了,有興趣的朋友可以自己去試試。

關於控制引數訪問的__getattr__,__setattr__,__delattr__,__getattribute__:

__getattr__是一旦我們嘗試訪問一個並不存在的屬性的時候就會呼叫,而如果這個屬性存在則不會呼叫該方法。

來看一個__getattr__的例子:

class Test(object):
    def __init__(self, world):
        self.world = world

    def __getattr__(self, item):
        return item


x = Test('world123')
print x.world4

output:
world4

這裡我們並沒有world4屬性,在找不到屬性的情況下,正常的繼承object的物件都會丟擲AtrributeError的錯誤。但是這裡我通過__getattr__魔法方法改變了找不到屬性時候的類的行為。輸出了查詢的屬性的引數。

__setattr__是設定引數的時候會呼叫到的魔法方法,相當於設定引數前的一個鉤子。每個設定屬性的方法都繞不開這個魔法方法,只有擁有這個魔法方法的物件才可以設定屬性。在使用這個方法的時候要特別注意到不要被迴圈呼叫了。

下面來看一個例子:

class Test(object):
    def __init__(self, world):
        self.world = world

    def __setattr__(self, name, value):
        if name == 'value':
            object.__setattr__(self, name, value - 100)
        else:
            object.__setattr__(self, name, value)

x = Test(123)
print x.world
x.value = 200
print x.value

output:
123
100

這裡我們先初始化一個Test類的例項x,通過__init__方法我們可以注意到,會給初始化的world引數進行賦值。這裡的self.world = world語句就是在做這個事情。

注意,這裡在進行world引數賦值的時候,就是會呼叫到__setattr__方法。這個例子來看world就是name,而後面的值的world就是value。我在__setattr__裡面做了一個行為改寫,我將判斷name 值是'value'的進行特殊處理,把它的value值減少100. 所以輸出了預期的結果。

我為什麼說__setattr__特別容易出現迴圈呼叫?因為任何賦值方法都會走這個魔法方法,如果你在你改寫__setattr__方法裡面使用了類似的賦值,又回迴圈呼叫回__setattr__方法。例如:

class Test(object):
    def __init__(self, world):
        self.world = world

    def __setattr__(self, name, value):
        self.name = value


x = Test(123)
print x.world

output:
RuntimeError: maximum recursion depth exceeded

這裡我們想讓__setattr__執行預設行為,也就是將value賦值給name,和object物件中的同樣方法,做類似的操作。但是這裡我們不呼叫父類__setattr__的方法來實現,做這樣的嘗試得到的結果就是,超過迴圈呼叫深度,報錯。因為這裡在執行初始化方法self.world = world的時候,就會呼叫__setattr__方法,而這裡的__setattr__方法裡面的self.name = value又會呼叫自身。所以造成了迴圈呼叫。所以使用該魔法方法的時候要特別注意。

__delattr__的行為和__setattr__特別相似,同樣需要注意的也是迴圈呼叫問題,其他都差不多,只是把屬性賦值變成了 del self.name這樣的表示。下面直接上個例子,不再多贅述。

class Test(object):
    def __init__(self, world):
        self.world = world

    def __delattr__(self, item):
        print 'hahaha del something'
        object.__delattr__(self, item)


x = Test(123)
del x.world
print x.world

output:

hahaha del something
Traceback (most recent call last):
File "/Users/piperck/Desktop/py_pra/laplace_pra/practie_01_23/c2.py", line 12, in <module>
print x.world
AttributeError: 'Test' object has no attribute 'world'

可以看到我們將屬性刪除之後,就找不到那個屬性了。但是在刪除屬性的時候呼叫了__delattr__,我在裡面列印了一段話,在執行之前被打印出來了

__getattribute__和__getattr__方法唯一不同的地方是,上面我們已經介紹了__getattr__方法只能在找不到屬性的時候攔截呼叫,然後進行過載或者加入一些其他操作。但是__getattribute__更加強大,他可以攔截所有的屬性獲取。所以也容易出現我們上面提到的,迴圈呼叫的問題。下面上一個例子來說明這個問題:

class Test(object):
    def __init__(self, world):
        self.world = world

    def __getattribute__(self, item):
        print 'get_something: %s' % item
        return item


x = Test(123)
print x.world
print x.pp

output:
get_something: world
world
get_something: pp
pp

可以看到,區別於__getattr__只攔截不存在的屬性,__getattribute__會攔截所有的屬性。所以導致了已經被初始化的world值123,也被改寫成了字串world。而不存在的屬性也被改寫了成了pp。

關於__dict__:

先上個例子:

class Test(object):
    fly = True

    def __init__(self, age):
        self.age = age

__dict__魔法方法可以被稱為系統,他是儲存各分層屬性的魔法方法。__dict__中,鍵為屬性名,值為屬性本身。可以這樣理解,在平時我們給類和例項定義的那些屬性,都會被儲存到__dict__方法中用於讀取。而我們平時使用的類似這樣的語法Test.fly 其實就是呼叫了類屬性,同樣可以寫成Test.__dict__['fly']。除了類屬性,還有例項屬性。當我們用類例項化一個例項,例如上文我們使用p = Test(2)例項化類Test,p也會具有__dict__屬性。這裡會輸出:

{'age': 2}

由上可以發現,python中的屬性是進行分層定義的。/object/Test/p這樣一層一層下來的。當我們需要呼叫某個屬性的時候,python會一層一層往上面遍歷上去。先從例項,然後例項的__class__的__dict__,然後是該類的__base__。這樣__dict__一路找上去。如果最後都沒有找到,就丟擲AttributeError錯誤。

這裡可以延伸一下,沒記錯的話,我前面有篇文章講了一個方法__slot__。__slots__方法就是通過限制__dict__,只讓類例項初始化__slots__裡面定義的屬性,而且讓例項不再擁有__dict__方法,來達到節約記憶體的目的。我將會就上面的那個例子重寫一下,來說明這個問題。

 class Test(object):
     __slots__ = ['age']

     fly = True

     def __init__(self, age):
         self.age = age

output:

In [25]: Test.__dict__
Out[25]:
dict_proxy({'__doc__': None,
            '__init__': <function __main__.__init__>,
            '__module__': '__main__',
            '__slots__': ['age'],
            'age': <member 'age' of 'Test' objects>,
            'fly': True})


In [36]: p.__dict__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-36-3a1cec47d020> in <module>()
----> 1 p.__dict__

AttributeError: 'Test' object has no attribute '__dict__'

In [37]: p.age
Out[37]: 3

In [38]: p.fly
Out[38]: True

可以看到,__slots__方法並沒有阻止由下至上的屬性查詢方法,只是不會再允許沒有包含在__slots__陣列中的屬性再被賦值給例項了。但這並不妨礙,繼續呼叫允許訪問的屬性,以及類屬性。

關於__get__,__set__,__del__:

在前面的文章裡面我也介紹過這三個魔法方法,雖然一般是用不到的,但是在寫庫的時候它們有特別的用途。他們是python另外一個協議descriptor的根基。

同一個物件的不同屬性之間可能存在依賴關係。當某個屬性被修改時,我們希望依賴於該屬性的其他屬性也同時變化。在這種環境下面__dict__方法就無法辦到。因為__dict__方法只能用來儲存靜態屬性。python提供了多種即時生成屬性的方法。其中一種就是property。property是特殊的屬性。比如我們為上面的例子增加一個property特性,使得他能夠動態變化。來看這個例子:

class Test(object):
    fly = True

    def __init__(self, age):
        self.age = age

    def whether_fly(self):
        if self.age <= 30:
            return True
        else:
            return False

    def just_try_try(self, other):
        pass

    whether_fly = property(whether_fly)

p = Test(20)
print p.age
print p.whether_fly
p.age = 40
print p.age
print p.whether_fly

output:

20
True
40
False

可以看到 我們可以使用這種手段,動態修改屬性值。property有四個引數。前三個引數為函式,分別用於處理查詢特性、修改特性、刪除特性。最後一個引數為特性的文件,可以為一個字串,起說明作用。這裡我只是要到了第一個引數,查詢的時候動態修改他的返回值,而第二個引數是在修改值的時候就會體現出來。