【Python】【TypeHint】ImmutableなオブジェクトにProtocolを使った時にmypyがエラーとなった
普段mypyを使って開発をしているのですが、Immutableなオブジェクトにtyping.Protocolを使ったらmypyがエラーを出して困ったので備忘録として記事を書きます。
Protocolとは
Wikipediaには「複数の者が対象となる事項を確実に実行するための手順について定めたもの」と書かれいています。
情報工学の文脈については、「情報工学分野でマシンやソフトウェア同士のやりとりに関する取り決め(通信規約)を指すためにも用いられるようになった」と書かれています。
https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB
異なるもの同士がやりとりをするための取り決めが「プロトコル」と言えそうです。
では、PyhtonのProtocolは何でしょう。
PythonのProtocolはPEP544で定義されました。PEP544を読んでみると
Currently, PEP 484 and the typing module [typing] define abstract base classes for several common Python protocols such as Iterable and Sized.
collections.abc --- コレクションの抽象基底クラス — Python 3.8.2 ドキュメント
(みずき訳) 現在、PEP484とタイピングモジュールではIterableやSizedなどのPythonのプロトコルの抽象基底クラスが定義されています。
IterableやSizedはプロトコルだそうです。
Iterableだと、__iter__
メソッドを持っているのが規約、Sizedは__len__
メソッドを持っているのが規約のプロトコルです。
これらの規約を満たしているオブジェクトに対してiter()やlen()が使用できます。
逆に、満たしていないオブジェクトには使用できないので、__iter__
や__len__
を実装して規約を満たす必要があります。
PythonはIterableやSizedなどのプロトコルを用意してくれていますが、それらでは足りない独自の規約を作りたい時があります。
そんな時に、typing.Protocolを継承したクラスを作って独自の規約を作ります。
PEP 544 -- Protocols: Structural subtyping (static duck typing) | Python.org
typing.Protocolの使用例
https://www.python.org/dev/peps/pep-0544/ を少し改変しました。
from typing import Protocol, Iterator class Template(Protocol): name: str # プロトコルの要素 value: int = 0 # プロトコルの要素(デフォルト値つき) def method(self) -> None: # プロトコルの要素 pass class Concrete: def __init__(self, name: str, value: int, used: bool) -> None: self.name = name self.value = value # この要素があってもname, value, method()があるので # Templateクラスのプロトコルを満たしていると言える self.used = used def method(self) -> None: return # ConcreteクラスのオブジェクトをTemplateクラスと型推論しても # プロトコルに則っているのでエラーにならない sample: Template = Concrete(name='aaaa', value=1, used=True) # Iteratorのプロトコルに則っていないのでエラーになる # Incompatible types in assignment (expression has type "Concrete", variable has type "Iterator[int]") sample: Iterator[int] = Concrete(name='aaaa', value=1, used=True)
エラーになった例
ImmutableなオブジェクトにProtocolを使用しようとした時にmypyがエラーを出しました。
import dataclasses import datetime from typing import ( Iterable, List, Protocol, ) class Item(Protocol): name: str release_date: datetime.date @dataclasses.dataclass(frozen=True) class Book: name: str release_date: datetime.date author: str @dataclasses.dataclass(frozen=True) class Movie: name: str release_date: datetime.date director: str casts: List[str] def aggregate_items(items: Iterable[Item]): pass book_1 = Book(name='本1', release_date=datetime.date(2020, 1, 1), author='著者名') book_2 = Book(name='本2', release_date=datetime.date(2019, 1, 1), author='著者名') movie_1 = Movie(name='映画1', release_date=datetime.date(2010, 2, 1), director='監督名', casts=['女優', '俳優']) items = aggregate_items([book_1, book_1, book_2, movie_1])
エラー内容
error_example.py:38: error: List item 0 has incompatible type "Book"; expected "Item" error_example.py:38: note: Protocol member Item.name expected settable variable, got read-only attribute error_example.py:38: note: Protocol member Item.release_date expected settable variable, got read-only attribute error_example.py:38: error: List item 1 has incompatible type "Book"; expected "Item" error_example.py:38: error: List item 2 has incompatible type "Book"; expected "Item" error_example.py:38: error: List item 3 has incompatible type "Movie"; expected "Item"
なお、frozen=Trueを外したり、他のMutableな型で試した結果、エラーにはならなくりました。 このことから、ImmutableなオブジェクトにProtocolクラスを参照させようとするとエラーになるとわかりました。
解決策
Protocolを継承したクラスのアトリビュートに対して@propertyを使用することで、Immutableなオブジェクトがアトリビュートになった場合でもエラーにならなくなります。
propertyは、例えばアトリビュートnameの場合はget_nameのようなものです。nameという名前のアトリビュートの値が返されます。
https://github.com/python/typing/issues/622
class Item(Protocol): @property def name(self) -> str: pass @property def release_date(self) -> datetime.date: pass
なぜ、name: str
ではいけなくてpropertyでないといけないのでしょう。調べていたところ、この記事に出会いました。
pythonのtyping_extensions.Protocolがなぜ嬉しいか(propertyの例) - podhmo's diary
先程のエラーにerror_example.py:38: note: Protocol member Item.name expected settable variable, got read-only attribute
というメッセージがありました。
Protocolクラスの属性はwriteableな変数を期待しているが、read-onlyな属性渡されている
と言っています。
name: str
だとread-onlyなアトリビュートとみなされます。
そのため、@propertyにして書き込み可能なアトリビュートに宣言してあげます。
ただ、Protocolクラスではwritableとなっているだけで、BookクラスやMovieクラスで属性の値を再代入したらエラーとなるので、安全です。