Python
Scheme
Python3

Schemeの`with-output-to-file`的なものをPythonで実現する


はじめに

Schemeにはwith-output-to-fileとかwith-input-from-fileみたいな手続きがあります.例えば以下のように使います.

(with-output-to-file "file"

(lambda ()
(display "contents")))

"file"というファイルの中に"contents"という文字列が書き込まれます.Pythonを普段書いている人はなんとなく以下のようなコードを想起したと思います.

with open("file", "w") as f:

f.write("contents")

やっていることも(大体)同じで,Pythonのwith構文は明らかにSchemeのwith-系の手続きに影響を受けているものだと思います.(違ったらごめんなさい)

追記

書いてから思いましたがPythonの上記のwithの使い方はCommon Lispのwith-open-fileの方が近いですね.いずれにせよwithの出処はPythonに限らずLisp系言語だと思っているのですが実際はどうなのでしょうか.

見た目もだいたい同じ両者ですが,決定的に違うところがあります.

Schemeの例ではdisplayというPythonで言うところのprintに相当する,標準出力用の手続きを用いているのに対してPythonではFileオブジェクトのwriteメソッドを用いています.Scheme方式のいいところは画面に出力する意図の処理をファイルにリダイレクトするみたいなことが簡単にできてしまうところだと思います.例を示します.

(define (hello)

(display "Hello")
(newline))

(with-output-to-file "file" hello)

もともとhelloはファイルに書き込む目的のものではありませんが簡単に出力先をファイルにすることができました.これと同じことをPythonでやると以下の様になります.

import sys

def hello():
print("Hello")

stdout = sys.stdout
with open("file", "w") as sys.stdout:
hello()
sys.stdout = stdout

これが例えば以下のようにスッキリ書けないだろうかというのが今回のネタです.

def hello():

print("Hello")

with output_to_file("file"):
hello()


Context Manager

Pythonのwith構文は何もopenだけに使うものではありません.with構文ではwithに続けて渡されたオブジェクトの__enter__というメソッドを実行した後,with以下を実行し,最後に__exit__というメソッドを実行します.このようなメソッドをもち,withコンテキストの振る舞いを定義するクラスのことをContext Managerと呼びます.例を示します.

class Test(object):

def __enter__(self):
print("enter")

def __exit__(self, *args):
print("exit")

with Test():
print("body")

これを実行すると"enter","body","exit"が順に出力されます.Context Managerの(with構文の)ミソはwith内のコードブロック実行時に例外が送出された場合にも__exit__が呼ばれることです.例を示します.

class Test(object):

def __enter__(self):
print("enter")

def __exit__(self, *args):
print("exit")

with Test():
raise ValueError

これを実行すると例えば以下のような出力を得ます.

enter

exit
Traceback (most recent call last):
File "test.py", line 10, in <module>
raise ValueError
ValueError

以上を利用してwith-output-to-fileをPythonで実装します.


実装

Schemeには同様の手続きにwith-error-to-filewith-input-from-fileもありますがとりあえずwith-output-to-fileだけ実装してみます.いきなりですが以下のようになります.

import sys

class _OutputToFile(object):
def __init__(self, fname):
self.fname = fname

def __enter__(self):
self.file = open(self.fname, "w")
self.stdout = sys.stdout
sys.stdout = self.file

def __exit__(self, *args):
sys.stdout = self.stdout
self.file.close()

def output_to_file(fname):
return _OutputToFile(fname)

with output_to_file("file"):
print("Hello") #fileにHelloと書き込まれる.

あっさり実装できてしまいました.一応説明ですが,__enter__で現在の標準出力を保存し,出力先をファイルに変更します.__exit__では逆に出力先を元通りにしつつファイルを閉じます.

のこりのwith-error-to-filewith-input-from-fileも同様なので一気に実装してみます.

import sys

class _IOContextManager(object):
def __init__(self, fname, type, target):
self.fname = fname
self.type = type
self.target = target

def __enter__(self):
self.file = open(self.fname, self.type)
self.original = sys.__dict__[self.target]
sys.__dict__[self.target] = self.file

def __exit__(self, *args):
sys.__dict__[self.target] = self.original
self.file.close()

class _OutputToFile(_IOContextManager):
def __init__(self, fname):
super().__init__(fname, "w", "stdout")

class _InputFromFile(_IOContextManager):
def __init__(self, fname):
super().__init__(fname, "r", "stdin")

class _ErrorToFile(_IOContextManager):
def __init__(self, fname):
super().__init__(fname, "w", "stderr")

def output_to_file(fname):
return _OutputToFile(fname)

def input_from_file(fname):
return _InputFromFile(fname)

def error_to_file(fname):
return _ErrorToFile(fname)

例えば以下のように使います.

with output_to_file("file"):

print("Hello")

with input_from_file("file"):
print(input())

"file"の中にまず”Hello"と書き込まれ,画面に"Hello"と出力されます.


おわりに

大したネタじゃありませんでしたがここまでお読みいただきありがとうございました.