はじめに

標準ライブラリや外部ライブラリの挙動をほんの少しだけ変えてみたい――でも、わざわざライブラリのファイルを直接書き換えるようなリスキーな手段は取りたくありませんよね。特にPythonの標準ライブラリは数多くのプロジェクトで利用され、バージョンアップや依存関係を考えると、なるべく手を加えずに工夫したいものです。

そんなときに便利なのが、unittest.mock.patchを使った「一時的なモンキーパッチ(Monkey Patching)」です。これを使えば、標準ライブラリを直接変更することなく、一部の関数やメソッドの挙動を「その場限り」で書き換えることができます。

今回例としてご紹介するのは、zipfileモジュールでファイル名をShift JISやUTF-8といった別エンコードでZip内部に格納する、といったニッチな要望をかなえるテクニックです。本来zipfileはASCIIかUTF-8でエンコードする仕様のようで、Shift JISなど任意の文字コードを扱うのは容易ではありません。そこで、unittest.mock.patchを使って、一時的にzipfile内部のエンコード処理を変えてしまおうというわけです。

標準ライブラリをコピーして書き換える必要はありませんし、プロジェクト全体への影響を最小限に抑えながら所望の挙動を実現できます。短期的な検証や特別な要件下でのテスト時など、このアプローチは予想以上に有用です。

以下では、その方法やメリット、実用上のポイントについてご紹介します。これを読めば、標準ライブラリを書き換えることなく、必要なときだけ挙動を差し替える「ちょっとした裏ワザ」が身につくはずです。

背景

先日、ある検証で数パターンのZipファイルを作成する必要があり、その中で「Zipファイル内に格納されるファイル名のエンコードを明示的に変更したい」という要件がありました。その際、パパっとpythonか何かでスクリプト組んで試せばいいやと思っていたんですが、実際やってみるとZipFile.writestr()メソッドに対して、encode('shift_jis')したファイル名を直接渡そうとするとエラーが発生してしまいました。

原因を探ると、zipfileの内部実装ではASCII以外の文字コードが検出されると自動でUTF-8として扱うような処理があるようで、Shift JISを直接扱うことを想定していないようでした。
(参考:Qiita記事 https://qiita.com/bicstone/items/14ef11e80cf8d36004c2 では、標準ライブラリをコピー&修正して独自に対応する手法が紹介されています。)

とはいえ、標準ライブラリを直接いじることには抵抗があります。将来的なメンテナンス性や移植性、バージョンアップ時の影響を考えると、あまりスマートな手法ではありません。そこで検討したのが「一時的なモンキーパッチ(Monkey Patching)」です。

unittest.mock.patchを使った一時的パッチ当て

ここで役立つのが標準ライブラリに含まれているunittest.mock.patch機能です。通常はテストコードで外部APIやクラス、メソッドなどを置き換えるために用いられますが、このpatch()は本番コードでも一時的なパッチ当てとして使うことが可能です。(本番で利用することを推奨するものではありませんが。)

具体的なアプローチとしては次のような流れになります。

  1. zipfile.ZipFilezipfile.ZipInfoといった内部のメソッドまたは属性を対象に、ファイル名エンコード周りの挙動を変更する。
  2. patch()を使って、必要な箇所だけ挙動を差し替える。これにより、Shift JISでのファイル名指定などを一時的に実現できます。

たとえば、patch()はコンテキストマネージャとして利用できるので、以下のようなコードで一時的な変更を試みることができます(あくまでイメージです)。

from unittest.mock import patch
import zipfile

def my_encode_filename(filename):
    # ここでShift JISエンコードを試みるなど、独自処理を行う
    try:
        return self.filename.encode('ascii'), self.flag_bits
    except UnicodeEncodeError:
        return self.filename.encode('shift_jis'), self.flag_bits

with patch('zipfile.ZipInfo._encodeFilenameFlags', my_encode_filename):
    with zipfile.ZipFile('test.zip', 'w') as zf:
        zf.writestr('テスト.txt', '内容')
        # このブロック内ではファイル名が独自エンコーディングで扱われる

patch()ブロックを抜ければ、元のzipfileの挙動に戻るため、標準ライブラリを直接書き換えるリスクを避けつつ、一時的な実験や検証が行えます。

なぜこのアプローチが有効なのか

  • ライブラリ本体を変更しない
    標準ライブラリのソースをコピー・修正する方法は、保守性や将来的なPythonバージョンアップでの互換性維持が難しくなります。その点、unittest.mock.patchを使えば、一切ファイルをいじらずに動作をカスタマイズ可能です。
  • 試行錯誤が簡単
    テストコードを書くような感覚で変更点を試せるため、設定ミスやエンコード不備を素早く発見できます。また、ブロックを抜ければ元通りになるため、環境を汚しません。
  • 短期的・限定的な対応策として有効
    本番運用で常用するのは避けたい手法ですが、一時的な検証や特殊ケースのテストとしては大変有用です。「ちょっとだけ標準ライブラリの挙動を変えたい」なんて要望にぴったりです。

まとめ

標準ライブラリで手っ取り早く独自の挙動を追加するには、unittest.mock.patchによる一時的なモンキーパッチが便利な場合があります。

もちろん、本質的には標準ライブラリ側の制約を理解した上で、その制約を回避する設計を考えるのがベストかもしれません。とはいえ、スピーディーに検証したいフェーズであれば、このようなテクニックが現場での問題解決に役立つでしょう。


今回は、Pythonのzipfileでファイル名のエンコードを変えたいというニッチな要望に対して、一時的パッチ当てという「裏ワザ」をご紹介しました。何らかの理由で標準ライブラリを直接書き換えたくないが挙動を変えたい場合、unittest.mock.patchによるモンキーパッチは一度試してみる価値はありそうです。