2010-03-03 80 views
61

我無法用另一個函數從另一個模塊中替換一個函數,這讓我發瘋。一隻猴子在python中如何修補一個函數?

比方說,我有一個模塊bar.py,看起來像這樣:

from a_package.baz import do_something_expensive 

def a_function(): 
    print do_something_expensive() 

而且我還有一個模塊,看起來像這樣:

from bar import a_function 
a_function() 

from a_package.baz import do_something_expensive 
do_something_expensive = lambda: 'Something really cheap.' 
a_function() 

import a_package.baz 
a_package.baz.do_something_expensive = lambda: 'Something really cheap.' 
a_function() 

我希望得到的結果:

Something expensive! 
Something really cheap. 
Something really cheap. 

而是我得到這個:

Something expensive! 
Something expensive! 
Something expensive! 

我在做什麼錯?

+0

第二個不能工作,因爲你只是重新定義do_something_expensive的意義在你的本地範圍內。但我不知道,爲什麼第三個不工作...... – pajton 2010-03-03 22:18:22

+1

正如尼古拉斯解釋的那樣,您正在複製一個參考文獻並只替換其中的一個參考文獻。 '從模塊導入non_module_member'和模塊級別的猴子打補丁是不兼容的,因爲這個原因,通常都是最好的避免。 – bobince 2010-03-03 22:29:35

+0

首選的軟件包命名方案是小寫的,沒有下劃線,即'apackage'。 – 2010-03-03 22:39:00

回答

69

這可能有助於思考Python名稱空間的工作方式:它們本質上是字典。所以,當你這樣做:

from a_package.baz import do_something_expensive 
do_something_expensive = lambda: 'Something really cheap.' 

,想想它是這樣的:

do_something_expensive = a_package.baz['do_something_expensive'] 
do_something_expensive = lambda: 'Something really cheap.' 

希望你能明白爲什麼這個一旦導入一個名字爲命名空間不那麼:-)工作,您在名稱空間中輸入的名稱空間值無關。你只修改本地模塊的命名空間或上面的a_package.baz命名空間中的do_something_expensive的值。但由於巴進口直接do_something_expensive,而不是從模塊命名空間引用它,你需要寫它的命名空間:

import bar 
bar.do_something_expensive = lambda: 'Something really cheap.' 
19

有一個非常優雅的裝飾這個:Guido van Rossum: Python-Dev list: Monkeypatching Idioms

還有一個dectools包,我看到了一個PyCon 2010,可能也可以在這個上下文中使用,但實際上它可能是另一種方式(monkeypatching在方法聲明級別,重新不​​)

+3

這些修飾器似乎不適用於這種情況。 – 2010-03-03 22:37:13

+1

@MikeGraham:Guido的電子郵件沒有提及他的示例代碼也允許替換任何方法,而不僅僅是添加一個新的方法。所以,我認爲他們確實適用於這種情況。 – tuomassalo 2012-03-25 14:15:24

+0

@MikeGraham Guido的例子完全適用於模擬方法聲明,我只是自己嘗試過! setattr只是說'='的一種奇特的方式;因此,'a = 3'或者創建一個名爲'a'的新變量並將其設置爲3或用3替換現有變量的值 – 2015-05-22 01:00:12

4

在第一個片段中,您使bar.do_something_expensive指的是那個時刻a_package.baz.do_something_expensive所指的功能對象。要真正「monkeypatch」,你需要改變它自己的功能(你只是改變名稱引用);這是可能的,但你實際上並不想這樣做。

在你試圖改變a_function的行爲,你已經做了兩兩件事:

  1. 在第一次嘗試,您在模塊中做出do_something_expensive全局名稱。但是,您打電話給a_function,它不會查看您的模塊來解析名稱,所以它仍然指向相同的功能。

  2. 在第二個示例中,您更改a_package.baz.do_something_expensive引用的內容,但bar.do_something_expensive與其並不相關。該名稱仍然指它在啓動時查找的功能對象。

最簡單的,但遠從理想的辦法是改變bar.py

import a_package.baz 

def a_function(): 
    print a_package.baz.do_something_expensive() 

正確的解決方案可能是以下兩種情況之一:

  • 重新定義a_function以函數作爲參數並調用它,而不是試圖偷偷摸摸並改變它的功能d編碼以指代,或者
  • 存儲要在類的實例中使用的函數;這就是我們在Python中如何做可變狀態。

使用全局變量(這是從其他模塊更換模塊級的東西)是壞事導致不可維護的,混亂的,untestestable,不可擴展的代碼流是很難跟蹤。

0

do_something_expensivea_function()函數只是指向函數對象的模塊名稱空間內的變量。當你重新定義模塊時,你正在使用不同的命名空間。

2

如果你只想打補丁您的垂詢和否則保留原來的代碼,你可以使用https://docs.python.org/3/library/unittest.mock.html#patch(因爲Python 3.3):

with patch('a_package.baz.do_something_expensive', new=lambda: 'Something really cheap.'): 
    print do_something_expensive() 
    # prints 'Something really cheap.' 

print do_something_expensive() 
# prints 'Something expensive!'