TL;DR: クイックフィックス
PythonでTypeError: cannot pickle '<lambda>' objectが発生するのは、multiprocessingモジュールが他のCPUコアにコードを送信するために、コードを「パッケージ化(シリアライズ)」する必要があるからです。標準のpickleライブラリは、ラムダのような匿名関数やネストされたロジックの扱い方を知りません。
解決策: ロジックをラムダから切り出し、スクリプトのトップレベルで定義された名前付き関数に移動します。これにより、Pythonは新しいプロセス内でその関数を見つけるための明確な「マップ」を持つことができます。
import multiprocessing
# ❌ これは失敗します
# with multiprocessing.Pool(4) as p:
# p.map(lambda x: x * 10, [1, 2, 3])
# ✅ これは動作します
def multiply_by_ten(x):
return x * 10
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as p:
print(p.map(multiply_by_ten, [1, 2, 3]))
なぜシリアライズに失敗するのか
Poolを開始すると、Pythonは新しいワーカープロセスを生成します。これらのワーカーはメインスクリプトとメモリを共有しません。関数をワーカーに渡すために、Pythonはそれを「pickle化」します。つまり、関数をバイトストリームに変換し、受信側で再構築できるようにします。
Pickleはオブジェクトを特定の名前で検索することで動作します。lambdaは匿名(名前がない)であるため、ワーカープロセスがインポートするための固有の名前を持ちません。以下のケースでも同様の問題に直面します:
- **ネストされた関数:** 他の関数の中で定義された関数。
- **インスタンスメソッド:** クラスにアタッチされたメソッド(特にWindows/macOS)。
- **システムリソース:** オープンされたファイルハンドル、データベース接続、ネットワークソケット。
OSの違いがここで影響します。Linuxは多くの場合forkをデフォルトとしており、メモリ空間全体をコピーするため、pickle化できないオブジェクトでも動作することがあります。しかし、macOS(Python 3.8以降)とWindowsはspawnを使用します。これはクリーンな状態でプロセスを開始するため、厳格なpickle化ルールが適用されます。
3つの修正方法
1. トップレベル関数を使用する
これが最もクリーンな方法です。ワーカー関数をモジュールレベル(関数の外)に配置することで、すべてのワーカープロセスが名前でその関数を見つけてインポートできることを保証します。信頼性が高く、読みやすく、追加のライブラリも必要ありません。
# logic.py
def heavy_lifting(data):
return sum(data) / len(data)
# main.py
from multiprocessing import Pool
from logic import heavy_lifting
if __name__ == "__main__":
items = [[1, 2], [3, 4], [5, 6]]
with Pool() as pool:
results = pool.map(heavy_lifting, items)
2. PickleをPathosに置き換える
プロジェクトでラムダや複雑なクラス構造を多用している場合は、pathosライブラリを試してみてください。これはpickleの代わりにdillを使用します。dillはより強力で、実際のバイトコードをキャプチャすることで、クロージャやラムダを含むほぼすべてのオブジェクトをシリアライズできます。
ターミナルからインストールします:
pip install pathos
その後、インポートを書き換えます:
from pathos.multiprocessing import ProcessingPool as Pool
if __name__ == "__main__":
# pathosを使えば問題なく動作します
with Pool(4) as p:
result = p.map(lambda x: x ** 2, [10, 20, 30])
print(result)
3. メソッドを静的メソッドに変換する
self.my_methodをpickle化しようとすると、Pythonはオブジェクトインスタンス(self)全体をpickle化しようとするため、失敗することがよくあります。オブジェクトがデータベース接続や500MBのデータキャッシュを保持している場合、プロセスはクラッシュします。代わりに、@staticmethodを使用して、ロジックをインスタンスの状態から切り離します。
class DataProcessor:
@staticmethod
def clean_string(text):
return text.strip().lower()
def run(self, data_list):
with multiprocessing.Pool() as p:
# 静的メソッドを渡すことで、'self'のpickle化トラップを回避します
return p.map(self.clean_string, data_list)
修正の検証方法
ローカルのLinuxマシンで動作したからといって、安心しないでください。ポータビリティを確保するために、以下の3つのステップを実行してください:
- **「spawn」互換性のテスト:** スクリプトの最上部に`multiprocessing.set_start_method('spawn', force=True)`を追加します。これで動作すれば、どのOSでも動作します。
- **`__main__` ガード:** エントリポイントは必ず`if __name__ == "__main__":`でラップしてください。これがないと、ワーカーが自身のワーカーを生成しようとし、CPUを使い果たす再帰ループに陥る可能性があります。
- **小規模なバッチ実行:** 2つのワーカーと10個のアイテムでテストを実行します。シリアライズの不一致がある場合、Pythonは数秒以内にエラーを報告します。
参考文献
- [Pythonドキュメント:Pickle化可能なオブジェクト](https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled)
- [Spawn vs. Forkの理解](https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods)

