コース進捗 コース目次 24レッスン中 24件を公開中
Pythonの基礎
データとコレクション
信頼できるプログラムを作る
オブジェクトでモデル化する
プロフェッショナルなPython
上級Python
Pythonの構文はプロトコルをお願いする
特殊メソッドは魔法の飾りではありません。正確な契約を持つフックです。普通の構文がオブジェクトへ振る舞いを頼みます。
-
len(box)
__len__をお願い -
a == b
__eq__で相談 - for item in box イテレーターをお願い
-
box[0]
__getitem__をお願い
ドメインが正直に守れるプロトコルだけを実装します。たとえば__len__は0以上の整数を返す契約であり、文字列の"2"は返せません。
値に2つの顔と公平な比較を与えよう
repr(value)は開発者向けの安定した診断用の顔、str(value)は利用者向けの読みやすい顔です。__str__がなければ、Pythonは__repr__を使います。
class ReadingItem:
def __init__(self, title, minutes):
self.title = title
self.minutes = minutes
def __repr__(self):
return f"ReadingItem({self.title!r}, {self.minutes!r})"
def __str__(self):
return f"{self.title}({self.minutes}分)"
!rにより引用符の境界が見えます。reprには機密情報を入れません。表現はログやトレースバックへ届くからです。
等価性は相談です。対応しない型には、FalseでもNotImplementedError例外でもなく、シングルトンのNotImplementedを返します。
def __eq__(self, other):
if type(other) is not type(self):
return NotImplemented
return (self.title, self.minutes) == (other.title, other.minutes)
- 左へ聞く この型を比較できる?
- NotImplemented Pythonが相手側にも聞く
- 最後の結果 等価はFalse、順序はエラーの場合
ドメインに1つの意味ある順序がある場合だけ<を定義します。読書優先度を(minutes, title)とする規則は作れますが、電話番号の大小には普通意味がありません。場面ごとに順序が違うなら、呼び出し側が並べ替えキーを渡します。
通常の値では、等価性を反射的、対称的、推移的に保ちます。利用可能だからという理由ではなく、ドメイン契約として使うフィールドを選びます。
等価性の安定した状態だけをハッシュしよう
ハッシュコンテナには1つの法則があります。
a == bなら、hash(a) == hash(b)である。
等価性と同じ、安定したフィールドと型をハッシュします。
def __hash__(self):
return hash((type(self), self.title, self.minutes))
辞書キーや集合要素である間、そのフィールドは変えられません。値の等価性を定義しながら変更可能なクラスは、ハッシュ不可にします。__eq__を定義して__hash__を定義しないと、Pythonは通常__hash__ = Noneにします。この安全な既定値を保ってください。フィールド等価性の横へ同一性ハッシュを戻すと、等しい物のハッシュが異なる可能性があります。
箱の中身を漏らさず質問に答えよう
- len / bool 大きさ、0ならFalse
- in 明確な包含の意味
- iter 毎回新しいイテレーター
- [0]と[1:] 要素と安全なスライス
__bool__がなければ、Pythonは__len__を使い、0を偽、正の長さを真にします。__contains__はinを支えます。入れ子の走査位置が混ざらないよう、__iter__は毎回新しいイテレーターを返します。__getitem__は整数、負のインデックス、スライス、通常のIndexErrorを支えます。スライスはタプルや新しいコンテナにできますが、内部の変更可能なリストそのものは返しません。
Pythonらしい読書キューを作ろう
完全な標準ライブラリのプロジェクトをreading_queue.pyへ保存します。
class ReadingItem:
__slots__ = ("_title", "_minutes", "_sealed")
def __init__(self, title, minutes):
title = title.strip()
if not title:
raise ValueError("title must not be empty")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
object.__setattr__(self, "_title", title)
object.__setattr__(self, "_minutes", minutes)
object.__setattr__(self, "_sealed", True)
def __setattr__(self, name, value):
if getattr(self, "_sealed", False):
raise AttributeError("ReadingItem is immutable")
object.__setattr__(self, name, value)
@property
def title(self):
return self._title
@property
def minutes(self):
return self._minutes
def __repr__(self):
return f"ReadingItem({self.title!r}, {self.minutes!r})"
def __str__(self):
return f"{self.title}({self.minutes}分)"
def __eq__(self, other):
if type(other) is not type(self):
return NotImplemented
return (self.title, self.minutes) == (other.title, other.minutes)
def __lt__(self, other):
if type(other) is not type(self):
return NotImplemented
return (self.minutes, self.title) < (other.minutes, other.title)
def __hash__(self):
return hash((type(self), self.title, self.minutes))
class ReadingQueue:
__hash__ = None
def __init__(self, items=()):
self._items = []
for item in items:
self.add(item)
def add(self, item):
if not isinstance(item, ReadingItem):
raise TypeError("item must be a ReadingItem")
self._items.append(item)
def __len__(self):
return len(self._items)
def __contains__(self, item):
return item in self._items
def __iter__(self):
return iter(tuple(self._items))
def __getitem__(self, index):
if isinstance(index, slice):
return tuple(self._items[index])
return self._items[index]
def main():
python = ReadingItem(" Python data model ", 30)
same = ReadingItem("Python data model", 30)
async_item = ReadingItem("Async I/O", 45)
assert repr(python) == "ReadingItem('Python data model', 30)"
assert python == same and hash(python) == hash(same)
assert len({python, same}) == 1
assert python != ("Python data model", 30)
assert sorted([async_item, python]) == [python, async_item]
queue = ReadingQueue([python, async_item])
left, right = iter(queue), iter(queue)
assert next(left) == next(right) == python
assert len(queue) == 2 and bool(queue) and same in queue
assert queue[-1] == async_item and queue[1:] == (async_item,)
try:
hash(queue)
except TypeError:
pass
else:
raise AssertionError("mutable queue was hashable")
print("テストに合格しました。")
print(f"キュー:{len(queue)}件")
for item in queue:
print(f"- {item}")
if __name__ == "__main__":
main()
出力:
テストに合格しました。
キュー:2件
- Python data model(30分)
- Async I/O(45分)
キューは意図的にハッシュを持たず、反復は毎回新しいスナップショットイテレーターを使い、スライスは非公開リストではなくタプルを返します。
3つの小さなチャレンジと次への準備
- 2つの顔。
Bookmark(title, page)へ診断用reprと読者向けstrを実装し、引用符を含むタイトルで試します。 - 比較の握手。
Duration.__eq__と__lt__が整数へNotImplementedを返すようにします。Duration(5) == 5は偽、Duration(5) < 5はTypeErrorだと確認します。 - 独立した本棚。 非公開ストレージ上で長さ、包含、同時に動く2つのイテレーター、負インデックス、タプルのスライスに対応します。
注意点:特殊メソッドの直接呼び出しは公開意図を飛ばし、NotImplementedErrorは比較用センチネルではありません。作り物の順序はドメインを偽り、変更可能なハッシュ状態は検索を壊し、内部リストの返却は変更経路を漏らします。
- 公開構文をプロトコル要求へ対応付けられる
- 開発者向け
reprと読者向けstrを分けられる - 対応しない比較型へ
NotImplementedを返せる - 本当の順序規則を持つドメインだけを並べられる
- 等価性/ハッシュ法則を守るか、変更可能な値をハッシュ不可にできる
- サイズ、真偽、包含、新しい反復、インデックス、スライスを実装できる
- キューが全assertを通り、内部リストを公開しない
- 3つの小さなチャレンジを終えた
次章では、並行性と並列性を分け、各仕事を安全に実行できるスレッドまたはプロセスを選びます。