この記事で使っているバージョンは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)