[Python, Windows] open 中のファイルを削除・移動できない問題への対処
open
中のファイルを消せない
Windows では 次のコードで、 input
で処理を止めている間に text.txt
を削除してみましょう。
with open("./test.txt", "w") as f:
# ここで test.txt を消す
input()
Linux ではファイルを削除できますが、 Windows ではエラーになります。
ファイルの移動も同様です。
この問題をなんとかしましょう。
Windows は使用中ファイルの削除を禁止していない
実は、 Windows でも使用中のファイルを消せる場合があります。
Windows でファイルを開く際に使う CreateFileW
のドキュメントを見てみましょう。引数 dwShareMode
について、次のように書かれています。
[in] dwShareMode
ファイルまたはデバイスの要求された共有モード。読み取り、書き込み、両方、削除、これらすべて、またはなしを指定できます (次の表を参照)。 属性または拡張属性へのアクセス要求は、このフラグの影響を受けません。
dwShareMode
に適切なフラグを渡せば、使用中のファイルを削除・移動できるのです。
open
は削除許可フラグをセットしない
Python の なぜ Python で open
中のファイルは消せないのでしょうか。
CPython における open
の実装を見てみましょう (該当コード)。
#ifdef MS_WINDOWS
self->fd = _wopen(widename, flags, 0666);
#else
self->fd = open(name, flags, 0666);
#endif
_wopen
が呼ばれています。
では、 _wopen
の定義[1]を見てみましょう。
inline int __CRTDECL _wopen(
_In_z_ wchar_t const* _FileName,
_In_ int _OFlag,
_In_ int _PMode = 0
)
{
int _FileHandle;
// Last parameter passed as 0 because we don't want to validate pmode from _open
errno_t const _Result = _wsopen_dispatch(_FileName, _OFlag, _SH_DENYNO, _PMode, &_FileHandle, 0);
return _Result ? -1 : _FileHandle;
}
_SH_DENYNO
が目につきます。これは共有モード変数と呼ばれています。CreateFileW
の dwShareMode
では、読み・書き・削除を設定できましたが、こちらはどうでしょうか。
残念なことに、共有モード変数では、読み・書きの設定しかできません。 _SH_DENYNO
の場合、読み・書きは許可されますが、削除は許可されません[2]。
つまり、 CPython の提供する open
では、削除を許可するフラグがセットされないのです。
ではどうすればよいのか
CreateFileW
関数に適切なフラグを渡せば削除を許可できます。ならば、 open
の実装をそのように上書きしてしまえばよいのです[3]。
ありがたいことに、 open
関数には opener
という引数があります。 opener
に、「path
と flags
を受け取ってファイルディスクリプタを返す処理」を渡すと、 open
がそれを使ってファイルを開いてくれます。これを使いましょう。
具体的には次のようにします。
(フラグの設定やエラー処理等に問題がありましたら、コメントいただけると幸いです。)
import sys
if sys.platform == "win32":
import os
import ctypes
import builtins
#----------------------------------
# Win32API の関数を使う準備
#----------------------------------
from ctypes.wintypes import HANDLE, LPCWSTR, DWORD
from ctypes import c_void_p, c_bool, POINTER
import msvcrt
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
FILE_SHARE_DELETE = 0x00000004
CREATE_NEW = 1
CREATE_ALWAYS = 2
OPEN_EXISTING = 3
OPEN_ALWAYS = 4
FILE_ATTRIBUTE_NORMAL = 0x00000080
CreateFileW = ctypes.windll.kernel32.CreateFileW
CreateFileW.argtypes = [LPCWSTR, DWORD, DWORD, c_void_p, DWORD, DWORD, HANDLE]
CreateFileW.restype = HANDLE
INVALID_HANDLE_VALUE = HANDLE(-1).value
CloseHandle = ctypes.windll.kernel32.CloseHandle
CloseHandle.argtypes = [HANDLE]
CloseHandle.restype = c_bool
#----------------------------------
# CreateFileW でファイルを開く関数
#----------------------------------
def custom_opener(path, flags):
match flags & 0b11:
case os.O_RDONLY: # 0
access = GENERIC_READ
case os.O_WRONLY: # 1
access = GENERIC_WRITE
case os.O_RDWR: # 2
access = GENERIC_READ | GENERIC_WRITE
case _:
raise OSError(f'Unexpected flags: {flags}')
if flags & os.O_CREAT:
if flags & os.O_TRUNC: # w
create = CREATE_ALWAYS
elif flags & os.O_APPEND: # a
create = OPEN_ALWAYS
elif flags & os.O_EXCL: # x
create = CREATE_NEW
else:
raise OSError(f'Unexpected flags: {flags}')
else: # r
create = OPEN_EXISTING
handle = CreateFileW(
path,
access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
None,
create,
FILE_ATTRIBUTE_NORMAL,
None,
)
if handle == INVALID_HANDLE_VALUE:
raise ctypes.WinError()
# この fd は _close で閉じなくてはならない。
# handle に対して CloseHandle を呼んではならない。
# CPython の builtin.close は _close (のエイリアスである close) で閉じてくれる。
# - https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/posix-close?view=msvc-170
# - https://github.com/python/cpython/blob/31c9f3ced293492b38e784c17c4befe425da5dab/Modules/_io/fileio.c#L128
fd = msvcrt.open_osfhandle(handle, flags)
if fd == -1:
if CloseHandle(handle) != 0:
raise ctypes.WinError()
raise OSError('Failed to convert handle to fd.')
# Append の場合はファイル末尾にシークする
if flags & os.O_APPEND:
os.lseek(fd, 0, os.SEEK_END)
return fd
#----------------------------------
# open の実装を上書き
#----------------------------------
original_open = builtins.open
def patched_open(file, mode="r", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=custom_opener):
return original_open(file, mode, buffering, encoding, errors, newline, closefd, opener)
builtins.open = patched_open
if __name__ == "__main__":
with open("test.txt", "w") as f:
# test.txt を削除・移動できる
input()
これで、 open
中のファイルを削除・移動できるようになりました。
まとめ
Windows 環境において、 Python で open
中のファイルを削除・移動できない問題と、その対処法を紹介しました。移植等で挙動の違いに悩んでいる人の助けになればと思います。
-
Visual Studio の定義ジャンプを使うと見ることができます。 ↩︎
-
このことはドキュメントには明記されていませんが、 Micorosoft のブログ で言及されています。 ↩︎
-
モンキーパッチと呼ばれる手法です。 ↩︎
Discussion