web-dev-qa-db-ja.com

ある猿は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!

私は何を間違えていますか?

73
guidoism

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.'

うまくいけば、なぜこれが機能しないのか理解できます:-)名前空間に名前をインポートすると、インポートした名前空間の名前の値fromは無関係です。上記のローカルモジュールの名前空間、またはa_package.bazの名前空間でdo_something_expensiveの値を変更するだけです。ただし、barはモジュール名前空間から参照するのではなく、do_something_expensiveを直接インポートするため、その名前空間に書き込む必要があります。

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'
83
Nicholas Riley

これには本当にエレガントなデコレータがあります: Guido van Rossum:Python-Devリスト:Monkeypatching Idioms

dectools パッケージもあります。このコンテキストでも使用できるPyCon 2010がありますが、実際には別の方法になる可能性があります(メソッド宣言レベルでのモンキーパッチ...)あなたがいないところ)

21
RyanWilcox

呼び出しにのみパッチを適用し、それ以外の場合は元のコードのままにする場合は、 https://docs.python.org/3/library/unittest.mock.html#patch (since = 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!'
4
Risadinha

最初のスニペットでは、bar.do_something_expensiveがその時点でa_package.baz.do_something_expensiveが参照する関数オブジェクトを参照するようにします。本当に「monkeypatch」するには、関数自体を変更する必要があります(名前が参照するものだけを変更しています)。これは可能ですが、実際にはそうしたくありません。

a_functionの動作を変更しようとして、2つのことを行いました。

  1. 最初の試みでは、do_something_expensiveをモジュール内のグローバル名にします。ただし、名前を解決するためにモジュールを検索しないa_functionを呼び出しているため、同じ関数を引き続き参照しています。

  2. 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()

適切なソリューションは、おそらく次の2つのいずれかです。

  • a_functionを再定義して、関数を引数として使用し、それを呼び出して、参照するようにハードコードされている関数をこっそりと変更しようとするのではなく、または
  • クラスのインスタンスで使用される関数を保存します。これが、Pythonで可変状態を行う方法です。

グローバルを使用する(これは、他のモジュールからモジュールレベルのものを変更することです)悪いことです。

3
Mike Graham

a_function()関数のdo_something_expensiveは、関数オブジェクトを指すモジュールの名前空間内の単なる変数です。モジュールを再定義すると、別のネームスペースで実行します。

0
DavidG