【Python】Windows上でtempfile.NamedTemporaryFileで作った一時ファイルの中に既存のファイルの内容をコピーすると PermissionError: [Errno 13] Permission denied になる問題とその解決用コード

 この記事で使っているバージョンはPython 3.12、Windows 11 Proです。

 Windows上のPythonで次のコードを実行するとエラーが発生します(少なくとも自分の環境では再現性のあるエラーです)。

import shutil
import tempfile

if __name__ == '__main__':
    # 一時ファイルの中に既存のファイルの内容をコピーする
    with tempfile.NamedTemporaryFile(suffix='.txt') as temp_file:
        shutil.copyfile('base.txt', temp_file.name)

> python .\with_error.py          
Traceback (most recent call last):
  File "C:\xxx\with_error.py", line 7, in <module>
    shutil.copyfile('base.txt', temp_file.name)
  File "C:\Pythonの場所\Lib\shutil.py", line 262, in copyfile
    with open(dst, 'wb') as fdst:
         ^^^^^^^^^^^^^^^
PermissionError: [Errno 13] Permission denied: 'C:\Users\%USERNAME%\AppData\Local\Temp\tmpsa7bmgjw.txt'

 この件について検索すると同じ問題に突き当たった方の話が読めます。それを読む感じこれはWindows固有の問題で、ファイルを二回開くとエラーになる、らしいです。先ほどのコードであれば temp_file として開き、更に copyfile のあて先として名前から開き、と二回開いた感じです。対策としてはNamedTemporaryFile(delete=False)となるように引数を与え、処理が終わったら手動でファイルを閉じて、削除する、という感じです。粗方の原因と対策は分かりましたが、この解決方法をそのまま使うと書き忘れによるミスを誘発させてしまいそうなでもあります。そこでtempfile.NamedTemporaryFile同様の動きをするWindows用のコードを作ります。実際のコードが次です。

import os
import shutil
import tempfile
from typing import Optional, IO, Any


# NamedTemporaryFileWin: Windows上で一時ファイルを扱うためのクラス
class NamedTemporaryFileWin:
    def __init__(
            self,
            mode: str = 'w+b',
            buffering: int = -1,
            encoding: Optional[str] = None,
            newline: Optional[str] = None,
            suffix: Optional[str] = None,
            prefix: Optional[str] = None,
            dir: Optional[str] = None
    ) -> None:
        self.file: Optional[IO] = None
        self.mode = mode
        self.buffering = buffering
        self.encoding = encoding
        self.newline = newline
        self.suffix = suffix
        self.prefix = prefix
        self.dir = dir
        self._create_temp_file()

    def _create_temp_file(self) -> None:
        # 一時ファイルを作成し、ファイル名を保持します
        tf = tempfile.NamedTemporaryFile(mode=self.mode, buffering=self.buffering, suffix=self.suffix,
                                         prefix=self.prefix, dir=self.dir, delete=False)
        self.temp_file_name = tf.name
        tf.close()
        # ファイルを開き直します
        self.file = open(self.temp_file_name, mode=self.mode, buffering=self.buffering, encoding=self.encoding,
                         newline=self.newline)

    def close(self) -> None:
        # ファイルを閉じて、一時ファイルを削除します
        if self.file:
            self.file.close()
            os.unlink(self.temp_file_name)
            self.file = None

    def __getattr__(self, item: str) -> Any:
        # このクラスで定義していない何かを呼び出したときに、内部のファイルオブジェクトのメソッドや属性へのアクセスを動的に委譲します
        # こうすることで通常のファイルの入出力と同じように使えます
        return getattr(self.file, item)

    def __enter__(self) -> IO:
        # コンテキストマネージャのエントリポイントです
        # with文を使ったときに返すオブジェクトを指定します
        return self.file

    def __exit__(self, exc_type: Optional[type], exc_val: Optional[Exception], exc_tb: Optional[Any]) -> None:
        # コンテキストマネージャのエクジットポイントです
        # with文を抜けるときに呼ばれます
        self.close()


if __name__ == '__main__':
    # 一時ファイルの中に既存のファイルの内容をコピーして読み込む例
    # 今度はエラーにならない
    with NamedTemporaryFileWin(suffix='.txt') as temp_file:
        shutil.copyfile('base.txt', temp_file.name)
        print(temp_file.read())  # base.txtの中身

    # 使用例: 一時ファイルを作成して、テキストを書き込み、読み込む
    t = NamedTemporaryFileWin('w+', encoding='utf-8')
    t_name = t.name
    print(t_name)  # C:\Users\%USERNAME%\AppData\Local\Temp\tmpgm58hvm1
    t.write('何かテキスト')
    t.seek(0)
    print(t.read())  # 何かテキスト
    t.close()
    print(os.path.exists(t_name))  # False 閉じたら自動で消える

    # 使用例: with文を使った例: 一時ファイルにテキストを書き込んで読み込む
    with NamedTemporaryFileWin('w+', encoding='utf-8') as f:
        f_name = f.name
        print(f_name)  # C:\Users\%USERNAME%\AppData\Local\Temp\tmp2imccjr2
        f.write('別のテキスト')
        f.seek(0)
        print(f.read())  # 別のテキスト
    print(os.path.exists(f_name))  # False 閉じたら自動で消える

 やっていることは検索して出てくる解決方法と概ね同じです。エラーが起きないように一時ファイルを作って、一時ファイルの役目が終わったら削除する、という流れです。このコードは通常の NamedTemporaryFile と変わらない使用感を維持する目的とファイルの削除し忘れを防ぐ目的のために色々と追加のコードを書いています。これは __getattr__ でファイルに処理を委譲したり、__enter__、__exit__ で with に対応したり、それらの機能を自然に呼び出すために引数を NamedTemporaryFile に合わせたりするといった部分です。 __getattr__ は何かのラッパーを作る時、with を使って誤りを減らせる __enter__、__exit__ は何かの外部リソースの制御をする時に便利です。

3. データモデル — Python 3.12.1 ドキュメント#object.__getattr__(self, name)
3. データモデル — Python 3.12.1 ドキュメント#object.__enter__(self)
3. データモデル — Python 3.12.1 ドキュメント#object.__exit__(self, exc_type, exc_value, traceback)

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG