銀月の符号

Python 使い見習いの日記・雑記

ジェネレータをうまく作れるようになりたい

今晩の目的、「同名のファイルが存在しても、重ならないファイル名でコピーをする関数」の作成。仮名 copy_safe 。a\spam.txt, b\spam.txt がすでにあるときに、copy_safe(r'a\spam.txt', r'b\spam.txt') すると b\spam(0).txt という名前でコピーするというもの。

試作したときは一つの関数だったけれど、「別名を作る」と「コピーする」は別の機能だからと考え直したら分けることに成功。すっきり。ファイル名を変えつつ生成し続けるジェネレータがあればいいことに、気づくのが遅すぎた。頭固いオレ。

itertools.count で連番生成

今後は悩まないためにメモ。 C:\a\spam.txt を与えると C:\a\spam.txt, C:\a\spam(0).txt, C:\a\spam(1).txt … という文字列を生成するジェネレータ。i=0, while True: i+=1 … でも連番は作れるけれど、こういうときのために itertools.count がある。 range や xrange との違いは長さを事前に決めなくてよいところ。

import itertools

def path_generator(path, limit=1000):
    yield path
    name, ext = os.path.splitext(path)
    for i in itertools.count():
        if i >= limit:
            break
        yield u''.join((name, '(%d)' % i, ext))

止まらない場合の警戒はこのジェネレータ自身がおこなうようにしてある(デフォルトでは数は999まで増える、出力総数は1001個)。でも無限に生成するようにしておいて、呼び出し側で止めるほうがいいのかも?

既存ファイルを上書きしないコピー、ムーブ(Windows編)

ファイル名生成はできたのでファイル操作のほうに着手。まずは、 Windows 。同名のファイルが存在しても名前を変えつつコピーする関数は CopyFileW の第3引数に True を渡しておくと既存ファイルを上書きしないのを利用して作る。同様にムーブする関数も MoveFileExW の第3引数に MOVEFILE_COPY_ALLOWED を与えるとやはり既存ファイルを上書きしないのを利用して作る。

他の OS だとどうすべきなのかという課題は明日以降に。何も見つからなければ「os.path.isfile で検査して shutil.copy2, os.rename で操作」といった感じで逃げる。アトミックな操作になってないから、運が悪いとファイルが消失するな…。 POSIX だとファイルの置換がアトミックにできたはずだから、一時ファイルを作る方法と同じでいいのかな? O_EXCL で open しておくとか link 作っておくとか。

あと、失敗時の例外が IOError なのは変な気がする。外部要因によるエラーそのものではなく、処理を1001回試みたけれどダメだったのであきらめたというエラーだから。これも明日以降。

import os
if os.name == 'nt':
    import win32file
    import pywintypes
    def copy_safe(old, new):
        for new_path in path_generator(new):
            try:
                win32file.CopyFileW(
                        old,
                        new_path,
                        True)
                break
            except pywintypes.error, e:
                pass
        else:
            raise IOError()

    def move_safe(old, new):
        for new_path in path_generator(new):
            try:
                win32file.MoveFileExW(
                        old,
                        new_path,
                        win32file.MOVEFILE_COPY_ALLOWED)
                break
            except pywintypes.error, e:
                pass
        else:
            raise IOError()

ちなみに、 for 文にも else 節がつけられる。 else 節は break したときには実行されない。