インターフェースとポリモーフィズム
保存用の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__)
PYCODEwriteがなければ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")
PYCODEAbstractWriterにはwriteが未実装として残り、生成でTypeErrorになります。CountWriterは生成でき、親から継承したsend_defaultが子のwriteを呼んで3を返します。抽象メソッドの実装が存在しても、保存などの動作まで自動的に保証されるわけではありません。
まとめ
共通の呼出しには、名前・引数・戻り値・動作の意味を揃える必要があります。
継承は型の関係を作り、合成は別のオブジェクトを部品として保持します。委譲と依存の注入により、その部品を交換できます。
Protocolや抽象クラスによる検査だけで、保存内容や返り値の意味まで証明されるわけではありません。
操作履歴と解説を振り返れます。
シェル / Python・mypy
sed -n '1,23p' writers.pyfrom 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())python3 - <<'PYCODE'
from writers import Buffer, UpperWriter, send
plain = Buffer()
target = Buffer()
print(send(plain), plain.parts)
print(send(UpperWriter(target)), target.parts)
PYCODE3 ['abc'] 3 ['ABC']
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))
PYCODE3 3 ['ABC'] ['ABC'] False
sed -n '25,31p' writers.py
mypy --strict --no-pretty bad_writer.pyclass 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)python3 bad_writer.pyabc
cat use_writers.py
mypy --strict --no-pretty use_writers.py
python3 use_writers.pyfrom 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 0python3 - <<'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__)
PYCODEAttributeError TypeError
sed -n '33,43p' writers.pyclass 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)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")
PYCODEAbstractWriter ['write'] TypeError CountWriter [] 3
演習完了