1匹のサルがPythonの関数にどのようにパッチを当てますか?


80

別のモジュールの関数を別の関数に置き換えるのに問題があり、それが私を夢中にさせています。

次のようなモジュール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!

私は何が間違っているのですか?


ローカルスコープでdo_something_expensiveの意味を再定義するだけなので、2番目のものは機能しません。しかし、なぜ3番目のものが機能しないのかわかりません...
pajton 2010

1
Nicholasが説明しているように、参照をコピーして、参照の1つだけを置き換えています。from module import non_module_memberこのため、モジュールレベルのモンキーパッチは互換性がなく、どちらも一般的に回避するのが最善です。
bobince 2010

推奨されるパッケージの命名スキームは、アンダースコアのない小文字apackageです。つまり、。
マイクグラハム

1
@bobince、このようなモジュールレベルの可変状態は回避するのが最善であり、認識されてから長い間、グローバルの悪影響があります。ただし、問題from foo import barはなく、実際には適切な場合に推奨されます。
マイクグラハム

回答:


87

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の値を変更しています。ただし、barはモジュールの名前空間からdo_something_expensiveを参照するのではなく、直接インポートするため、その名前空間に書き込む必要があります。

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

ここのようなサードパーティからのパッケージはどうですか?
Tengerye

21

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

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


4
これらのデコレータは、このケースには当てはまらないようです。
マイクグラハム

1
@MikeGraham:Guidoの電子メールには、彼のサンプルコードでは、新しいメソッドを追加するだけでなく、任意のメソッドを置き換えることもできるとは記載されていません。ですから、この場合にも当てはまると思います。
tuomassalo 2012年

@MikeGraham Guidoの例は、メソッドステートメントをモックするのに完全に機能します。自分で試してみました。setattrは、「=」と言うための空想的な言い方です。したがって、「a = 3」は、「a」という新しい変数を作成して3に設定するか、既存の変数の値を3に置き換えます
Chris Huang-Leaver 2015年

6

呼び出しに対してのみパッチを適用し、それ以外の場合は元のコードを残したい場合は、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!'

3

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

の動作を変更しようとすると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つのうちの1つです。

  • a_function関数を引数として取り、それを呼び出すように再定義します。参照するようにハードコーディングされている関数に忍び込んで変更しようとするのではなく、
  • クラスのインスタンスで使用される関数を格納します。これは、Pythonで可変状態を実行する方法です。

グローバルを使用すること(これは他のモジュールからモジュールレベルのものを変更することです)は悪いことであり、その流れを追跡するのが難しい、保守不可能、混乱、テスト不可能、拡張不可能なコードにつながります。


0

do_something_expensivea_function()関数関数オブジェクトにモジュールポインティングの名前空間内だけの変数です。モジュールを再定義すると、別の名前空間で実行します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.