HOWTO · Python

Python でのモンキー パッチ

この記事では、モンキー パッチとは何か、Python でどのように実装できるかについて説明します。 2つのセクションがあります。 1つ目は関数内でのサルのパッチ適用を示し、2つ目はファイルの単体テストでどのように実装できるかを示します。

このページの内容

ユーザーからデータベースにデータを送信するなど、目的の結果を達成するためのコードが記述されます。 ただし、コードが正しく実行されるかどうかやバグがないかどうかのチェックなど、テスト フェーズ中にコードを微調整する必要があります。

モンキー パッチは、スタブまたは同様のコードを割り当てて、コードの既定の動作を変更するプロセスです。 この記事では、Python でモンキー パッチを適用するさまざまな方法に焦点を当てます。

モンキーパッチにおける動的言語の重要性

Python が優れた例である動的言語のみが、モンキー パッチに使用できます。 すべてを定義する必要がある静的言語では、モンキーパッチは不可能です。

例として、モンキー パッチは、オブジェクトの説明を変更するのではなく、実行時に属性 (メソッドまたは変数) を追加する方法です。 これらは、ソース コードが利用できないモジュールを操作するときに頻繁に使用され、オブジェクト定義の更新が困難になります。

既存のオブジェクトまたはクラスを変更するのではなく、パッチを適用したメンバーをデコレーター内に使用して新しいバージョンのオブジェクトをビルドする場合、Python でのモンキー パッチが役立ちます。

Python でモンキー パッチを実装する

このプログラムでは、Python でのモンキー パッチのデモンストレーションを行います。 実行時にモンキー パッチを適用するために、新しい装飾されたメソッドにメソッドが割り当てられます。

コード:

import pandas as pd


def word_counter(self):
    """This method will return all the words inside the column that has the word 'tom'"""
    return [i for i in self.columns if "tom" in i]


pd.DataFrame.word_counter_patch = word_counter  # monkey-patch the DataFrame class
df = pd.DataFrame([list(range(4))], columns=["Arm", "tomorrow", "phantom", "tommy"])
print(df.word_counter_patch())

出力:

"C:\Users\Win 10\main.py"
['tomorrow', 'phantom', 'tommy']

Process finished with exit code 0

コードを分解して、Python でのモンキー パッチ適用を理解しましょう。

コードの最初の行は、プログラムでデータ フレームを作成するために使用されるライブラリ pandas をインポートします。

import pandas as pd

次に、Python 3 では関数とバインドされていないメソッドの区別はほとんど役に立たないため、バインドされておらず、クラス定義のスコープ外で自由に存在するメソッド定義が確立されます。

def word_counter(self):
    """This method will return all the words inside the column that has the word 'tom'"""
    return [i for i in self.columns if "tom" in i]

pd.Dataframe.word_counter を使用して新しいクラスが作成されます。 次に、新しく作成されたクラスがメソッド word_counter にアタッチされます。

それが何をするかというと、メソッド word_counter にデータ フレーム クラスをモンキー パッチすることです。

pd.DataFrame.word_counter_patch = word_counter  # monkey-patch the DataFrame class

メソッドがクラスにアタッチされたら、単語を格納するために新しいデータ フレームを作成する必要があります。 このデータ フレームには、オブジェクト変数 df が割り当てられます。

df = pd.DataFrame([list(range(4))], columns=["Arm", "tomorrow", "phantom", "tommy"])

最後に、データ フレーム df を渡すことでモンキー パッチ クラスが呼び出され、出力されます。 ここで何が起こるかというと、コンパイラがクラス word_counter_patch を呼び出すと、モンキー パッチがデータ フレームをメソッド word_counter に渡します。

動的プログラミング言語ではクラスとメソッドをオブジェクト変数として扱うことができるため、Python のモンキー パッチは、他のクラスを使用するメソッドに適用できます。

print(df.word_counter_patch())

Python での単体テストに Monkey Patch を使用する

ここまでで、Python のモンキー パッチが関数に対してどのように実行されるかを学びました。 このセクションでは、Python を使用してグローバル変数にモンキー パッチを適用する方法について説明します。

この例を示すためにパイプラインが使用されます。 パイプラインを初めて使用する読者にとって、これは機械学習モデルをトレーニングしてテストするプロセスです。

パイプラインには、テキストや画像などのデータを収集するトレーニング モジュールと、テスト モジュールの 2つのモジュールがあります。

このプログラムが行うことは、データ ディレクトリ内の複数のファイルを検索するパイプラインが作成されることです。 test.py ファイルでは、プログラムは 1つのファイルを含む一時ディレクトリを作成し、そのディレクトリ内のファイルの数を検索します。

単体テスト用のパイプラインをトレーニングする

このプログラムは、ディレクトリ data 内に格納された 2つのプレーン テキスト ファイルからデータを収集するパイプラインを作成します。 このプロセスを再現するには、data フォルダが保存されている親ディレクトリに pipeline.py および test.py Python ファイルを作成する必要があります。

pipeline.py ファイル:

from pathlib import Path

DATA_DIR = Path(__file__).parent / "data"


def collect_files(pattern):
    return list(DATA_DIR.glob(pattern))

コードを分解してみましょう:

pathlibPath としてインポートされ、コード内で使用されます。

from pathlib import Path

これは、データ ファイルの場所を格納するグローバル変数 DATA_DIR です。 Path は、親ディレクトリ data 内のファイルを示します。

DATA_DIR = Path(__file__).parent / "data"

関数 collect_files が作成され、検索に必要な文字列パターンを 1つのパラメーターとして受け取ります。

DATA_DIR.glob メソッドは、データ ディレクトリ内のパターンを検索します。 メソッドはリストを返します。

def collect_files(pattern):
    return list(DATA_DIR.glob(pattern))

この時点でグローバル変数が使用されているため、メソッド collect_files を正しくテストするにはどうすればよいでしょうか?

パイプライン クラスをテストするためのコードを格納するために、新しいファイル test.py を作成する必要があります。

test.py ファイル:

import pipeline


def test_collect_files(tmp_path):
    # given
    temp_data_directory = tmp_path / "data"
    temp_data_directory.mkdir(parents=True)
    temp_file = temp_data_directory / "file1.txt"
    temp_file.touch()

    expected_length = 1

    # when
    files = pipeline.collect_files("*.txt")
    actual_length = len(files)

    # then
    assert expected_length == actual_length

コードの最初の行は、pipeline および pytest Python ライブラリをインポートします。 次に、test_collect_files という名前の test 関数が作成されます。

この関数には、一時ディレクトリを取得するために使用されるパラメーター temp_path があります。

def test_collect_files(tmp_path):

パイプラインは、Given、When、Then の 3つのセクションに分かれています。

Given 内で、temp_data_directory という名前の新しい変数が作成されます。これは、data ディレクトリを指す一時パスにすぎません。 これは、tmp_path フィクスチャがパス オブジェクトを返すため可能です。

次に、データ ディレクトリを作成する必要があります。 これは mkdir 関数を使用して行われ、このパス内のすべての親ディレクトリが確実に作成されるように、親が true に設定されます。

次に、このディレクトリ内に file1.txt という名前の単一のテキスト ファイルが作成され、touch メソッドを使用して作成されます。

データ ディレクトリ内のファイル数を返す新しい変数 expected_length が作成されます。 データ ディレクトリ内にはファイルが 1つしかないと予想されるため、値 1 が与えられます。

temp_data_directory = tmp_path / "data"
temp_data_directory.mkdir(parents=True)
temp_file = temp_data_directory / "file1.txt"
temp_file.touch()

expected_length = 1

ここで、プログラムは When セクションに入ります。

pipeline.collect_files 関数が呼び出されると、パターン *.txt を持つファイルのリストが返されます。ここで、* は文字列です。 次に、変数 files に割り当てられます。

ファイルの数は、リストの長さを返す len(files) を使用して取得され、変数 actual_length 内に保存されます。

files = pipeline.collect_files("*.txt")
actual_length = len(files)

Then セクションの assert ステートメントは、expected_lengthactual_length と等しくなければならないことを示しています。 assert は、与えられたステートメントが真かどうかをチェックするために使用されます。

これで、パイプラインをテストする準備が整いました。 ターミナルに移動し、次のコマンドを使用して test.py ファイルを実行します。

pytest test.py

テストを実行すると、失敗します。

 assert expected_length == actual_length
E       assert 1 == 0

test.py:23: AssertionError
=============================== short test summary info ============================================
FAILED test.py::test_collect_files - assert 1 == 0

期待される長さが 1 であるために起こりますが、実際には 2 です。 これは、この時点でプログラムが一時ディレクトリを使用していないために発生します。 代わりに、プログラムの開始時に作成された実際のデータ ディレクトリを使用します。

データ ディレクトリ内には 2つのファイルが作成されましたが、一時ディレクトリ内には 1つのファイルのみが作成されました。 何が起こるかというと、test.py コードは、1つのファイルのみが格納されている一時ディレクトリ内のファイルをチェックするように記述されていますが、代わりに、コードによって元のディレクトリに戻されます。

そのため、expected_length 変数には 1 の値が与えられますが、actual_length と比較すると、テストは失敗します。

グローバル変数にパッチを適用して、モンキー パッチを使用してこの問題を解決できます。

最初に、パラメーター monkeypatch を次のように関数 collect_files に追加する必要があります。

def test_collect_files(tmp_path, monkeypatch):

ここで行う必要があるのは、monkey パッチを使用してグローバル変数にパッチを適用することです。

def test_collect_files(tmp_path, monkeypatch):
    # given
    temp_data_directory = tmp_path / "data"
    temp_data_directory.mkdir(parents=True)
    temp_file = temp_data_directory / "file1.txt"
    temp_file.touch()

    monkeypatch.setattr(pipeline, "DATA_DIR", temp_data_directory)  # Monkey Patch

    expected_length = 1

Python のモンキー パッチには関数 setattr があり、パイプライン モジュール内の DATA_DIR 変数に新しい値を割り当てることができます。 そして、DATA_DIR の新しい値が temp_data_directory に割り当てられます。

テストが再度実行されると、グローバル変数にパッチが適用され、代わりに temp_data_directory が使用されるため、テストに合格します。

platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: C:\Users\Win 10\PycharmProjects\Monkey_Patch
collected 1 item

test.py .                                                                                                                                                      [100%]

================================== 1 passed in 0.02s ====================================

まとめ

この記事では、Python でのモンキー パッチに焦点を当て、モンキー パッチの実際の使用法について詳しく説明します。 読者はモンキー パッチを簡単に実装できます。