長らくwith文を勘違いしていたらしい
久々の更新。
あなたのwith文
はどこから? 私はopen()
から。
この時点で察したかもだが、with文で生成したインスタンスは使い捨てだと思っていたというだけの話。
では、なぜ勘違いをしていたのかという言い訳から。
さっそく以下のコードを見てほしい。
# shop.txtは後々書くコードでの出力結果。 io = open("shop.txt", "r", encoding="utf-8") for i in range(2): with io: for s in io: print(s.strip())
io
というTextIOWrapper
インスタンスを生成して2回with文
で内容を出力しようとしているだけのコードである。
おわかりいただけただろうか?
出力をご覧いただこう。
--- X月1日 --- 開店: OtsuShop 入店: C(22) 入店: C(29) 入店: C(19) 入店: A(23) 退店: A(23) 退店: C(29) 退店: C(22) 退店: C(19) 閉店: OtsuShop B(27)は営業時間外のため入店できませんでした。 --- X月2日 --- 開店: OtsuShop 入店: A(27) 入店: A(29) 入店: C(20) 入店: C(22) 退店: C(20) 退店: A(27) 退店: C(22) 退店: A(29) 閉店: OtsuShop C(29)は営業時間外のため入店できませんでした。 Traceback (most recent call last): File ..., line 107, in <module> with io: ValueError: I/O operation on closed file.
2回目のwith文
を使おうとするとエラーになるのである。
これはTextIOWrapper
が不親切なのか趣味で書いてる程度の雑魚にはわからない仕方ない事情があってのことなのか定かではないが、withブロック終了時に呼び出される__exit__
メソッドでファイルを閉じる処理?はしてくれるものの__enter__
メソッドでファイルを開く処理?はしてくれないというだけのことらしい。
__enter__
, __exit__
は自作クラスでも実装することはあるものの、TextIOWrapper
よろしく__enter__
ではselfを返すだけでなにも行わず、__exit__
で終了処理を実装するだけのものばかりであったので、今まで気づくことがなかった。
ではなぜ気付いたのかというと、threading
の排他制御Lock
について調べていた時にwith lock:
のようなインスタンスでwithブロックに入るようなコードを見かけたからである。
冷静に考えると、インスタンス生成 -> __enter__呼び出し
という流れなので当たり前なのだが、盲点であった。
しかし、Lock
が特殊なのでは?と疑心暗鬼になっていたので以下のコードで試してみることにした。
import random as rnd from typing import Iterator, Self class Human: ID = {} __name: str __age: int def __new__(cls, name: str, age: int) -> Self: data = (name, age) if data in cls.ID: return cls.ID[data] self = super().__new__(cls) self.__name = name self.__age = age cls.ID[data] = self return self def __str__(self) -> str: return f"{self.__name}({self.__age})" class Shop(list[Human]): def __init__(self, name: str) -> None: self.__name = name self.__is_open = False super().__init__() def __enter__(self) -> Self: self.open() return self def __exit__(self, *ex) -> None: self.close() def __str__(self) -> str: return f"{self.__name}" def append(self, human: Human) -> None: if human in self: return if not self.__is_open: print(f"{human}は営業時間外のため入店できませんでした。") return print(f"入店: {human}") super().append(human) def remove(self, human: Human) -> None: if human not in self: return print(f"退店: {human}") super().remove(human) def close(self) -> None: while self: human = rnd.choice(self) self.remove(human) self.__is_open = False print(f"閉店: {self}") def open(self) -> None: self.__is_open = True print(f"開店: {self}") def create_human() -> Iterator[Human]: names = tuple( chr(x) for x in range( ord("A"), ord("D"), ) ) ages = tuple(range(18, 30)) while True: yield Human(rnd.choice(names), rnd.choice(ages)) def main() -> None: shop = Shop("OtsuShop") gen_customer = iter(create_human()) for day in range(2): print(f"--- X月{day+1}日 ---") with shop: while len(shop) < 4: shop.append(next(gen_customer)) shop.append(next(gen_customer)) print() if __name__ == "__main__": main()
Shop
クラスは開店
, 閉店
という状態を持ち、閉店中はappend
できないリストの拡張クラス。
__enter__
呼び出しで開店し、__exit__
呼び出しで今いる客が全員退店し、閉店する。
あとは先ほどと同じようにwith shop:
が2回発生するようにfor文を回している。
withブロック中にshopに客を4人入れ、さらにwithブロック外でshopに客を1人入れている。
--- X月1日 --- 開店: OtsuShop 入店: C(22) 入店: C(29) 入店: C(19) 入店: A(23) 退店: A(23) 退店: C(29) 退店: C(22) 退店: C(19) 閉店: OtsuShop B(27)は営業時間外のため入店できませんでした。 --- X月2日 --- 開店: OtsuShop 入店: A(27) 入店: A(29) 入店: C(20) 入店: C(22) 退店: C(20) 退店: A(27) 退店: C(22) 退店: A(29) 閉店: OtsuShop C(29)は営業時間外のため入店できませんでした。
withブロック入りで開店: OtsuShop
が出力され、4人入店した時点でwithブロックを抜けると退店ログが続いた後閉店: OtsuShop
と出力される。
その後入店しようとした客に対しては入店ができていないことが分かる。
しかし、もう一度withブロックに入ると再び客を受け入れることができるようになっている。
このように複数回with文を使えるかはクラスの実装次第であるということが納得できた。