Otsuhachi’s diary

プログラミングのことや覚書などその他いろいろ

当ブログの内容を使用したり参考にした場合に生じた問題の責任は負いかねます。

長らく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文を使えるかはクラスの実装次第であるということが納得できた。