一覧PG-14-1 · 型の抽象化と構成

インターフェースとポリモーフィズム

保存用のBufferと、大文字化して別の書込み先へ渡すUpperWriterを使います。共通の呼出し、委譲、型の検査、抽象クラスを順に試し、実装を交換するために揃えるべき条件を確認します。

説明

利用側が必要とする操作を決めておくと、その操作を提供する別の実装へ差し替えられます。ここでは「文字列を書き込み、書いた文字数を整数で返す」という契約を扱います。

保存用のBufferと、大文字化して別の書込み先へ渡すUpperWriterを使います。共通の呼出し、委譲、型の検査、抽象クラスを順に試し、実装を交換するために揃えるべき条件を確認します。

1必要な操作を型に記す

writers.pyの先頭に、利用側sendと二つの実装があります。Protocolは利用側が必要とする操作の型を記すためのクラスです。

sed -n '1,23p' writers.py契約・利用側・二つの実装を表示

Writerはwrite(str)->intという操作を要求します。Bufferは文字列を保存して文字数を返し、UpperWriterは保持したtargetへ大文字化した文字列を渡します。sendは保存方法を知らずにwriteを呼びます。

2同じ利用側で実装を交換

Bufferへ直接書き込む場合と、UpperWriterを挟む場合を実行します。

python3 -コードを実行 <<'PYCODE' from writers import Buffer, UpperWriter, send実装と利用側を読み込む plain = Buffer() target = Buffer() print(send(plain)直接書き込む, plain.parts) print(send(UpperWriter(target))大文字化して委譲する, target.parts) PYCODE

どちらも3文字を書き込みますが、保存内容はabcとABCです。BufferもUpperWriterもWriterを明示的には継承していません。必要な操作を同じ形で使えるので、共通のsendへ渡せます。

3保持している書込み先を差し替える

UpperWriterは書込み先を内部で生成せず、外から受け取ります。途中でtargetを別のBufferへ変更します。

python3 - <<'PYCODE' from writers import Buffer, UpperWriter, send first, second = Buffer(), Buffer() writer = UpperWriter(first)使う部品を外から渡す print(send(writer)) writer.target = second書込み先を交換 print(send(writer)) print(first.parts, second.parts)処理が届いた先 print(isinstance(writer, Buffer)Bufferの派生型ではないこと) PYCODE

最初の書込みはfirst、次はsecondへ届きます。UpperWriterはBufferの派生型ではなく、別の対象を部品として保持しています。処理をその部品へ渡すのが委譲、利用する部品を外から渡すのが依存の注入です。

4戻り値の型が異なる実装

BadTypeはwriteという名前を持ちますが、戻り値の注釈がstrです。契約との不一致を型検査します。

sed -n '25,31p' writers.py誤った戻り値の実装 mypy --strict --no-pretty bad_writer.py呼出し側の型を検査

mypyはsendが要求するWriterとBadTypeが適合しないと指摘します。メソッド名だけでなく、引数と戻り値の型も契約の一部です。

5型検査なしで同じ呼出しを実行

同じbad_writer.pyをPythonで実行します。

python3 bad_writer.py型が不適合な呼出しを実行

abcという文字列が返ります。Pythonの通常実行ではProtocol注釈が自動で呼出しを止めるわけではありません。型検査と実行時の処理を別に確認する必要があります。

6型が合っていても意味が異なる場合

WrongCountは整数0を返しますが、文字列を書き込みません。二つの正しい実装と一緒に検査・実行します。

cat use_writers.py三実装への同じ呼出し mypy --strict --no-pretty use_writers.py操作の型を検査 python3 use_writers.py実際の戻り値を比較

型検査は通りますが、WrongCountだけは0です。「abcを保存して3を返す」という動作の契約は満たしていません。型の形が合うことと、実際の処理が要求どおりであることは別です。

7必要な操作がない対象

writeのないobjectと、引数が一つ余分に必要な実装を渡します。

python3 - <<'PYCODE' from writers import send class NeedsPrefix:追加引数が必要な実装 def write(self, text, prefix): return len(prefix + text) for writer in (object(), NeedsPrefix()): try: print(send(writer)同じ呼出しを試す) except (AttributeError, TypeError)操作なし・引数不足を確認 as error: print(type(error).__name__) PYCODE

writeがなければAttributeError、必要な引数が足りなければTypeErrorです。操作が実行できることを重視するダックタイピングでも、名前があるだけで十分ではありません。

8継承で実装を要求する抽象クラス

AbstractWriterはwriteを抽象メソッドとして宣言し、共通のsend_defaultを持ちます。CountWriterが具体的なwriteを実装します。

sed -n '33,43p' writers.py抽象クラスと具体的な派生型

抽象メソッドが残っているクラスは、通常のインスタンス化が拒否されます。Protocolで操作の型を示す方法とは異なり、この例ではAbstractWriterの継承関係に基づいて実装の有無を扱います。

9抽象メソッドの実装前と実装後にインスタンスを生成する

二つのクラスを同じ手順で生成し、共通メソッドを呼びます。

python3 - <<'PYCODE' from writers import AbstractWriter, CountWriter for cls in (AbstractWriter, CountWriter): print(cls.__name__, sorted(cls.__abstractmethods__未実装の抽象メソッド)) try: writer = cls()インスタンス化を試す print(writer.send_default()継承した共通処理から呼ぶ) except TypeError: print("TypeError") PYCODE

AbstractWriterにはwriteが未実装として残り、生成でTypeErrorになります。CountWriterは生成でき、親から継承したsend_defaultが子のwriteを呼んで3を返します。抽象メソッドの実装が存在しても、保存などの動作まで自動的に保証されるわけではありません。

まとめ

共通の呼出しには、名前・引数・戻り値・動作の意味を揃える必要があります。

継承は型の関係を作り、合成は別のオブジェクトを部品として保持します。委譲と依存の注入により、その部品を交換できます。

Protocolや抽象クラスによる検査だけで、保存内容や返り値の意味まで証明されるわけではありません。

操作履歴と解説を振り返れます。

シェル / Python・mypy

1
sed -n '1,23p' writers.py
from typing import Protocol
from abc import ABC, abstractmethod

class Writer(Protocol):
    def write(self, text: str) -> int: ...

def send(writer: Writer) -> int:
    return writer.write("abc")

class Buffer:
    def __init__(self) -> None:
        self.parts: list[str] = []

    def write(self, text: str) -> int:
        self.parts.append(text)
        return len(text)

class UpperWriter:
    def __init__(self, target: Writer) -> None:
        self.target = target

    def write(self, text: str) -> int:
        return self.target.write(text.upper())
2
python3 - <<'PYCODE' from writers import Buffer, UpperWriter, send plain = Buffer() target = Buffer() print(send(plain), plain.parts) print(send(UpperWriter(target)), target.parts) PYCODE
3 ['abc']
3 ['ABC']
3
python3 - <<'PYCODE' from writers import Buffer, UpperWriter, send first, second = Buffer(), Buffer() writer = UpperWriter(first) print(send(writer)) writer.target = second print(send(writer)) print(first.parts, second.parts) print(isinstance(writer, Buffer)) PYCODE
3
3
['ABC'] ['ABC']
False
4
sed -n '25,31p' writers.py mypy --strict --no-pretty bad_writer.py
class WrongCount:
    def write(self, text: str) -> int:
        return 0

class BadType:
    def write(self, text: str) -> str:
        return text
bad_writer.py:2: error: Argument 1 to "send" has incompatible type "BadType"; expected "Writer"  [arg-type]
bad_writer.py:2: note: Following member(s) of "BadType" have conflicts:
bad_writer.py:2: note:     Expected:
bad_writer.py:2: note:         def write(self, text: str) -> int
bad_writer.py:2: note:     Got:
bad_writer.py:2: note:         def write(self, text: str) -> str
Found 1 error in 1 file (checked 1 source file)
5
python3 bad_writer.py
abc
6
cat use_writers.py mypy --strict --no-pretty use_writers.py python3 use_writers.py
from writers import Buffer, UpperWriter, WrongCount, send

for writer in (Buffer(), UpperWriter(Buffer()), WrongCount()):
    print(type(writer).__name__, send(writer))
Success: no issues found in 1 source file
Buffer 3
UpperWriter 3
WrongCount 0
7
python3 - <<'PYCODE' from writers import send class NeedsPrefix: def write(self, text, prefix): return len(prefix + text) for writer in (object(), NeedsPrefix()): try: print(send(writer)) except (AttributeError, TypeError) as error: print(type(error).__name__) PYCODE
AttributeError
TypeError
8
sed -n '33,43p' writers.py
class AbstractWriter(ABC):
    @abstractmethod
    def write(self, text: str) -> int: ...

    def send_default(self) -> int:
        return self.write("abc")

class CountWriter(AbstractWriter):
    def write(self, text: str) -> int:
        return len(text)
9
python3 - <<'PYCODE' from writers import AbstractWriter, CountWriter for cls in (AbstractWriter, CountWriter): print(cls.__name__, sorted(cls.__abstractmethods__)) try: writer = cls() print(writer.send_default()) except TypeError: print("TypeError") PYCODE
AbstractWriter ['write']
TypeError
CountWriter []
3

演習完了