# 型ヒントと typing と mypy
めちゃくちゃ長いですが 変数や属性に代入されるオブジェクトの型名を書く という、ただそれだけの話です。
# 1. はじめに
型ヒントの書き方については mypy のマニュアルにチートシートがありました。 わかる方は、直接こちらをご参照ください。
また型ヒントをいつ使うのか、と言った 「使い分け」 、型ヒントが登場した 「背景」 については、 以下の記事にまとめました。本記事の後編にあたります。
この記事では主に 「書き方」 と 「使い方」 について、書いていきます。
# 1.1. 型ヒント
変数に代入される型の名前です。 正確には違います。引用先の用語集をご参照ください。
i: int # <--- 型ヒント
i = 0
type hint (opens new window)
(型ヒント) 変数、クラス属性、関数のパラメータや返り値の期待される型を指定する annotation (opens new window) です。
# 1.2. typing
変わった型、例えば list の中に int がはいるよ、 みたいなことを表現したい時に使います。 typing は、標準ライブラリです。
from typing import List
lst: List[int]
lst = [0, 1, 2]
# 1.3. mypy
型ヒントに沿ってコードがかけているか確認したい時に使います。 mypy は、標準ライブラリではありません、pip install しないと使えません。
例えば sample.py というファイルを作ったとします。 int がはいるよ、と宣言した変数 i に、 str を代入したとします。
# sample.py
i: int
i = 'a'
mypy で型検査をすることで、 少なくとも型に関するエラーは、実行する前に弾くことができます。
vim sample.py
pip install mypy
mypy sample.py
$ mypy sample.py
sample.py:3: error: Incompatible types in assignment
(expression has type "str", variable has type "int")
Found 1 error in 1 file (checked 1 source file)
$
# ◯ まとめ
型ヒントは文法あるいは仕様です。PEP で定められています。 typing は標準ライブラリです。mypy は標準ライブラリではありません。 この3つを区別してください。 僕は長いこと区別できてませんでした。
型ヒントと typing と mypy を区別する。
# 2. mypy
繰り返しになりますが mypy は pip install して使うツール、「実装」です。 mypy は Python で書かれています。 対して型ヒントは「仕様」です。型ヒント は主に PEP 484 に英語で書かれています。
# 2.1. pyright
型検査をするツールは、mypy 以外にも存在します。 mypy は python.org 製のツールです。 pyright は Microsoft 社製のツールです。 ちなみに pyright は Python ではなく TypeScript で書かれているそうです。 本稿では pyright については扱いません。
# 2.2. 型推論
また mypy は別に型を明示しなくても 最初に代入されたオブジェクトから、わかる範囲で自動的に型を割り出してもくれます。 0 を代入した後に 'a' を代入すると弾かれます。
# mypy は i を int とみなす。
i = 0
# mypy は i への str の代入を
# エラーとして弾きます。
i = 'a'
$ mypy sample.py
sample.py:6: error:
Incompatible types in assignment
(expression has type "str", variable has type "int")
Found 1 error in 1 file (checked 1 source file)
$
Type inference and type annotations - mypy (opens new window)
mypy は、最初の代入を変数の定義と見なします。 変数の型を明示的に指定しない場合、 mypy は値式の静的な型に基づいて型を推測します。
Mypy considers the initial assignment as the definition of a variable. If you do not explicitly specify the type of the variable, mypy infers the type based on the static type of the value expression:i = 1 # i に対して "int" と型推論する。 l = [1, 2] # l に対して "List[int]" と型推論する。
"わかる範囲" ってなんだろうって感じですが、 自分もよくわかっていません。 この型推論 type inference は PEP にはほとんど記述がなく、 mypy のドキュメントに記述がありました。 そのため、この mypy の章に書きました。
# 2.3. 設定ファイル
また mypy の設定ファイルの書き方については、以下にありました。
それによると設定ファイルを置く場所は以下の通りです。
The mypy configuration file - mypy (opens new window)
mypy はファイルから設定を読み取ることができます。
Mypy supports reading configuration settings from a file.デフォルトでは、 まずカレントディレクトリの mypy.ini を、
見つからなければ setup.cfg を使います、
次に $XDG_CONFIG_HOME/mypy/config 、
次に ~/.config/mypy/config 、
もし、それらのどれも見つからなければ
最後にユーザーホームディレクトリの .mypy.ini を使用します。
By default it uses
the file mypy.ini with fallback to
setup.cfg in the current directory,
then $XDG_CONFIG_HOME/mypy/config,
then ~/.config/mypy/config,
and finally .mypy.ini in the user home directory
if none of them are found;--config-file コマンドラインフラグを使用して、代わりに別のファイルを読み取ることができます(構成ファイルを参照)。
the --config-file command-line flag can be used to read a different file instead (see Config file).
# 3. 書き方 - ignore
mypy のエラーを解消できない時に # type: ignore
を指定することで、エラーの表示を消すことができます。
書き方は PEP 484 で定められています。
#type:ignore
コメントは、エラーが参照する行に配置する必要があります:
The# type: ignore
comment should be put on the line that the error refers to:import http.client errors = { 'not_found': http.client.NOT_FOUND # type: ignore }
# 4. 書き方 - 簡単なもの
# 4.1. 変数への型ヒント
# 変数: 型ヒント
x: int
x = 0
c: str
c = 'a'
lst: list
lst = [0, 1, 2]
短くも書けます。
# 変数: 型ヒント = オブジェクト
x: int = 0
c: str = 'a'
lst: list = [0, 1, 2]
変数の横に書く位置については PEP 526 で定められました。
# 4.2. 関数の引数への型ヒント
変数へのアノテーションと書式は同じです。
# def 関数名(引数: 型名):
def f(x: int):
return x**2
# 4.3. 関数の返り値への型ヒント
矢印を書きます。
# def 関数名(引数) -> 型名:
def f(x) -> int:
return x**2
関数の引数と返り値の型ヒントをどこに書くかは PEP 3107 で定められました。
# 4.4. インスタンス変数への型ヒント
クラス変数を書く位置に、型ヒントを書きます。 すこしややこしいですね。
class Person:
name: str
age: int
def __init__(self, name, age):
self.name = name
self.age = age
person = Person('岩倉玲音', 14)
# 4.5. クラス変数への型ヒント
ClassVar
を使います。
from typing import ClassVar
class GirlFriend:
name: str
age: int
min_intimacy: ClassVar[int] = 0
max_intimacy: ClassVar[int] = 100
def __init__(self, name, age):
self.name = name
self.age = age
girl_friend = GirlFriend('岩倉玲音', 14)
typing.ClassVar (opens new window)
クラス変数であることを示す特別な型構築子です。 PEP 526 で導入された通り、 ClassVar でラップされた変数アノテーションによって、 ある属性はクラス変数として使うつもりであり、 そのクラスのインスタンスから設定すべきではないということを示せます。
インスタンス変数とクラス変数への型ヒントの書き方は PEP 526 で定められました。
# ◯ PEP 484, PEP 526, PEP 3107 の違い
PEP 484, PEP 526, PEP 3107 が錯綜しています。
PEP 526 は変数の型ヒントを書く位置あるいは書き方を定めました。 PEP 3107 は関数の型ヒントの書く位置あるいは書き方を定めました。 これら2つは型ヒントの中身をどう書くかは規定していません。
PEP 3107は関数注釈の構文を導入しましたが、その意味は意図的に未定義のままになっています。
PEP 3107 introduced syntax for function annotations, but the semantics were deliberately left undefined.
PEP 484 -- Type Hints (opens new window)
それに対して PEP 484 は、型ヒントそのものをどうやって書くかを規定しています。 なぜ、書く位置の PEP と中身の PEP が、分かれているのでしょうか? 型ヒント以外の用途にも使うことを想定していたらしいです。
PEP-3107 (Function Annotations) は Python の文法にアノテーションを導入した。その際、アノテーションの用途として型ヒントや DB クエリーのマッピング、RPS のマーシャリング情報などを挙げていた。
PEP-593 (Flexible function and variable annotations) を読んだよメモ- Qiita (opens new window)
ここからは標準ライブラリ typing を import して、動作を見ていきます。 標準ライブラリ typing のドキュメントはここにあります。 しかし、公式ドキュメントの記述だけではわからないところがあり、 PEP を見ざる得ない時があります。
# ◯ 補足
ちなみにですが mypy は float を int の子クラスとみなしてくれます。 この挙動は PEP では記述が見つけられず、mypy で書かれていました。
a: float
a = 1
$ mypy sample.py
Success: no issues found in 1 source file
$
Python では、いくつかの型は、 相互にサブクラスではなかったとしても、互換性があります、 たとえば int オブジェクトは float オブジェクトが期待される場合に有効です。
In Python, certain types are compatible even though they aren’t subclasses of each other. For example,int
objects are valid wheneverfloat
objects are expected.
Duck type compatibility - mypy (opens new window)
# 5. 書き方 - ジェネリクス その1
オブジェクトの中身のオブジェクトについても、 型ヒントをかける型ヒントをジェネリクスと表現されているのを目にします。
26.1.4. ジェネリクス - 標準ライブラリ (opens new window)
コンテナ内のオブジェクトについての型情報は 一般的な方法では静的に推論出来ないため、 抽象基底クラスが拡張され、 コンテナの要素に対して期待される型を示すために添字表記をサポートするようになりました。
lst[0]
のように書くことを公式ドキュメントで
添字表記 subscription と表現されているのを目にします。
# 5.1. リスト
# ◯ before
例えば、リストについて考えてみます。あるリストは要素に int を持っていますし、あるリストは要素に str を持っています。 そのままだと「list 型」としか言えなかったものが...
lst0: list = [0, 1, 2]
lst1: list = ['a', 'b', 'c']
# ◯ after
標準ライブラリ typing から List を import すれば 「中身は int 型の List 型」 と言えるようになります。
from typing import List
lst0: List[int] = [0, 1, 2]
lst1: List[str] = ['a', 'b', 'c']
# 5.2. タプル
# ◯ before
student: tuple = ('岩倉玲音', 14, True)
# ◯ after
from typing import Tuple
student: Tuple[str, int, bool]
student = ('岩倉玲音', 14, True)
# 型引数の個数
List は、1つしか型引数を受け取れません。 それに対して Tuple 複数個の型ヒントを受け取れます。 これは Tuple が元々は、 簡易的なクラス定義文として使われることを意識していたからだと思われます。
# 任意長
普通に書こうとすると固定長でしか書けません。
# これはエラーになります。
from typing import Tuple
name_list: Tuple[str] = ('岩倉玲音', 'やる夫', 'サーバルちゃん')
可変長のタプルも規定できるようです。 勉強になります。ありがとうございます。 リンク先をご参照ください。
# 5.3. 辞書
from typing import Dict
# Dict[キーの型ヒント, 値の型ヒント]
a: Dict[str, int] = {'a': 0, 'b': 1, 'c': 2}
b: Dict[int, str] = {0: 'a', 1: 'b', 2: 'c'}
# 5.4. 辞書(複雑なもの)
Dict では、キーと値に対してしか型ヒントを書けませんでした。 ユーザ定義クラスを使うほどではないけど、似たようなことを辞書で使い時があります。 そんな時は、もうちょっと複雑に型をつけたいです。
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
person0 = Person(name='Tom', age=12)
person1 = Person(name='Jelly', age=13)
# クラスからインスタンス化しているように見えますが
# 中身はただの辞書です。
person0
person1
>>> # クラスからインスタンス化しているように見えますが
>>> # 中身はただの辞書です。
>>> person0
{'name': 'Tom', 'age': 12}
>>> person1
{'name': 'Jelly', 'age': 13}
>>>
PEP 589 で規定されました。
もし Python 3.7 以前であれば typing_extensions から TypedDict を import する必要があります。 後述させていただく Protocol という型も Python 3.7 以前であれば typing_extensions から import する必要があります。
$ python3 -m pip install --upgrade typing-extensions
from typing_extensions import TypedDict
TypedDict と dataclass との使い分けは、 個人の好みによるところが大きいのかなと思うのですが、 新しいメソッドを定義しない場合は、TypedDict を使っても良いのかなと思いました。
辞書を使うメリットは、辞書ならメソッドがないことが自明になり、コードを読むときの負担が軽くなります。
辞書を使うデメリットは、辞書はクオテーション '
を書くのがとても面倒です。
辞書ではなくクラスが採用された例として WSGI があります。 ここで書かれている移植可能が何を指しているかはわからないのですが、 おそらく「独自のメソッドが定義されて互換性がないサーバとアプリケーションの組み合わせが生じる事態」を避けたかったからなのかなと思っています。
environ はなぜ辞書でなければならないのか? サブクラスを使用すると 何が問題なのか?
辞書を必要とする理由は、サーバ間の移植可能性を最大にすることである。
PEP 3333: Python Web Server Gateway Interface v1.0.1 (opens new window)
# 5.5. ユーザ定義クラス
# ◯ before
これをジェネリクスで表現します。
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
# 中身が int のベクトル型
vector0 = Vector(0, 1 )
# 中身が float のベクトル型
vector1 = Vector(0.0, 0.1)
# ◯ after その1
Generic クラスを継承したクラスを作成します。 与えたい引数の個数だけ TypeVar クラスのインスタンス化して、Generic クラスの引数に加えます(添字表記の中に加えます)。
from typing import Generic, TypeVar
T = TypeVar('T')
class Vector(Generic[T]):
def __init__(self, x: T, y: T):
self.x, self.y = x, y
# 中身は int の Vector
vector0: Vector[int] = Vector(0, 1)
# 中身は float の Vector
vector1: Vector[float] = Vector(0.0, 0.1)
# 中身は int の Vector と宣言して
# float を使うとエラーになる。
vector2: Vector[int] = Vector(0, 0.1)
# 中身は float の Vector と宣言して
# int を使ってもエラーに **ならない** 。
vector3: Vector[float] = Vector(0.0, 1)
class typing.TypeVar (opens new window)
型変数です。class typing.Generic (opens new window)
ジェネリック型のための抽象基底クラスです。
ジェネリクスの意味は、 このようにしてオブジェクトが持っている属性、オブジェクトの中身についても、型ヒントが書けるようになります。
ジェネリクスの書き方は、 関数をイメージするといいかなと思います。型ヒントを引数に取る型ヒントのよう感じです。
# ◯ after その2 短くも書ける。
ツールが 、型推論してくれるなら、このように短くも書けます。
vector0 = Vector[int](0, 1)
vector1 = Vector[float](0.0, 0.1)
vector2 = Vector[int](0, 0.1) # これはエラーになる。
vector3 = Vector[float](0.0, 1)
上記のコードを smaple.py として mypy をインストールしましょう。 mypy を実行して型チェックをすると、1つだけエラーで弾かれます。
vim sample.py # ファイルの保存
pip install mypy
mypy sample.py
$ mypy sample.py
sample.py:19: error: Argument 2 to "Vector" has incompatible type "float"; expected "int"
Found 1 error in 1 file (checked 1 source file)
$
vector3 がエラーにならないのでしょうか? それは mypy が float を int の子クラスとして取り扱っているからです。
# これがエラーにならないのは...
vector3: Vector[float] = Vector(0.0, 1)
# これがエラーにならないことから
# 雰囲気が伝わればと...
a: float
a = 1
# 5.5. 型引数 - TypeVar
このようにして名前を与えるのはデバックの際に表示したいからということなのでしょうか。
T = TypeVar('T')
assert T.__name__ == 'T'
クラス定義文や関数定義文では自動的に str 型の名前が付与されます。
class C:
pass
assert C.__name__ == 'C'
def f():
pass
assert f.__name__ == 'f'
文字列を与えているのは、名前を与えたいからだと思われます。
T = TypeVar('T')
assert T.__name__ == 'T'
D = type('D', (), {})
assert D.__name__ == 'D'
g = lambda x: x**2
g.__name__ = 'g'
assert g.__name__ == 'g'
なぜ名前がわざわざ付与されているのでしょうか? PEP 8 で lambda が使用できない理由を思い出してください。
# ◯ 参考
またジェネリクス周りはこちらのツイートも大変参考になります。 ありがとうございます。
ジェネリクスのパラメータは、TypeScriptでは1文字(特にT)が普通っぽいけど、他の言語どうなんだろうと調査したところ、Swiftはしっかり名前つけることもあるような印象を受けたけど、実際はどうなんだろう? pic.twitter.com/NfKoFSDKZQ
— suin❄️TypeScriptが好き (@suin) February 5, 2020
ジェネリクスの性質について考えたら、こんな図ができた😌 #TypeScript pic.twitter.com/aJ5DZ2HrFr
— suin❄️TypeScriptが好き (@suin) February 5, 2020
# 6. 書き方 - ジェネリクス その2
# 6.1. Union 型
例えば dict.get メソッドはキーがなかった場合 None を返します。
d = {'a': 0, 'b': 1, 'c': 2}
v = d.get('d')
assert v is None
変数 v には int がはいるかもしれないし type(None) がはいるかもしれません。 このように2種類以上の型がはいるかもしれない変数には Union を使います。
from typing import Union
d = {'a': 0, 'b': 1, 'c': 2}
v: Union[int, None]
v = d.get('d')
assert v is None
type(None) は例外的に None と書きます。後述します。 また Union のことを日本語で直和型と呼ばれるのを、たまに目にします。
# 6.2. Optional 型
実際のところ None を返してくるかもしれない関数は、よくあります。 そのため None のための Union が専用に定義されています。 Optional です。
from typing import Optional
d = {'a': 0, 'b': 1, 'c': 2}
v: Optional[int]
v = d.get('d')
assert v is None
# 6.3. Callable 型
稀に関数を要素に持つリストを定義してあげたい時があります。 例えば足し算、引き算、掛け算、割り算を行う関数を保存するリストを定義しましょう。
from typing import Callable, List
def add(x: int, y: int) -> int:
return x + y
def sub(x: int, y: int) -> int:
return x - y
def mul(x: int, y: int) -> int:
return x * y
def div(x: int, y: int) -> int:
return x // y
# Callable[[引数1の型, 引数2の型, ...], 返り値の型]
# このようにして長い型は、変数に代入できます。
# これを型エイリアスと言います。後述します。
ArithmeticOperation = Callable[[int, int], int]
arithmetic_operations: List[ArithmeticOperation]
arithmetic_operations = [add, sub, mul, div]
for arithmetic_operation in arithmetic_operations:
print(arithmetic_operation(1, 1))
>>> for arithmetic_operation in arithmetic_operations:
... print(arithmetic_operation(1, 1))
...
2
0
1
1
>>>
Callable は、複雑な引数は型を明示できず、位置引数のみが指定できます。
複雑な場合は ellipsis を使って引数の型を省略します。
elipsis とは ...
です。
本来は numpy などの数値計算ライブラリのために導入された定数だそうです。
疑似コードを書く時に使われるのを目にします。
型ヒントの実引数の型を ellipsis で置き換えることで呼び出しシグニチャを指定せずに callable の戻り値の型を宣言することができます:
Callable[..., ReturnType]
。
呼び出し可能オブジェクト - typing 標準ライブラリ (opens new window)
複雑な引数の型を明示したい場合は、Protocol を使います。Protocol の詳細は、次章の構造的部分型で説明します。
# 6.4. Final 型
変更しない変数や属性を定義できます。
# 7. 書き方 - そのほかの型
# 7.1. None
type(None) ではなく None と書けます。
def get_my_girlfriend() -> type(None):
return None
def get_my_girlfriend() -> None:
return None
型ヒントとしての None は特別なケースであり、 type(None) によって置き換えられます。
When used in a type hint, the expression None is considered equivalent to type(None).
PEP 484 -- Type Hints (opens new window)
# 7.2. NoReturn 型
例外しか返してこないような関数には NoReturn を使うそうです。
型付けモジュールは、正常に戻らない 関数に注釈を付けるための特別な型 NoReturn を提供します。たとえば、無条件に例外を発生させる関数:
The typing module provides a special type NoReturn to annotate functions that never return normally. For example, a function that unconditionally raises an exception:from typing import NoReturn def stop() -> NoReturn: raise RuntimeError('no way')
# ◯ 型エイリアス
型ヒントは、変数に代入できます。 わかりやすく書くことができるようになります。
from typing import Tuple
lain: Tuple[str, int, bool]
lain = ('岩倉玲音', 14, True)
from typing import Tuple
Person = Tuple[str, int, bool]
lain: Person
lain = ('岩倉玲音', 14, True)
from typing import Tuple
name = str
age = int
gender = bool
Person = Tuple(name, age, gender)
lain: Person
lain = ('岩倉玲音', 14, True)
型エイリアスは型をエイリアスに代入することで定義されます。 ... 中略 ... 型エイリアスは複雑な型シグネチャを単純化するのに有用です。 例えば: ... 後略
型エイリアス - typing 標準ライブラリ (opens new window)
# 7.3. NewType 型
型エイリアスを使うことでわかりやすく書けるようになりました。 NewType を使うことで安全なコードを書けるようになります。 NewType は既に定義された型に別の意味を与えたい時に使います。
from typing import NewType
Name = NewType('Name', str)
こんなものいつ使うのでしょうか? tuple を元にして Person という型を新しく定義して雰囲気だけ示してみます。
#
# 新しい型を定義します。
#
from typing import NewType, Tuple
Name = NewType('Name', str )
Age = NewType('Age', int )
Gender = NewType('Gender', bool)
Person = NewType('Person',
Tuple[
Name,
Age,
Gender
])
#
# 変数に代入します。
#
lain = Person((
Name('岩倉玲音'),
Age(14),
Gender(True)
))
#
# 新しい型を定義します。
#
yaruo = Person((
'やる夫' # <--- mypy に弾かれます
Age(18),
Gender(False)
))
print(lain) # <--- 実行はできます。
print(yaruo)
mypy で型検査をかけると、 Person の第一引数には str なんて型は受け付けられないよとエラーになります。
$ mypy sample.py
sample.py:35: error:
Argument 1 to "Person" has incompatible type "Tuple[str, Age, Gender]";
expected "Tuple[Name, Age, Gender]"
Found 1 error in 1 file (checked 1 source file)
$
すごそうなことをしているように見えます。 しかし、変数 lain, yaruo に代入されているのは、 中身は str, int, bool の tuple でしかありません。
>>> print(lain) # <--- 実行はできます。
('岩倉玲音', 14, True)
>>> print(yaruo)
('やる夫', 18, False)
>>>
型エイリアスを使うことで可読性を上げることができました。 NewType を使うことで可読性と安全性を上げることができました。 各々のデメリットは、わざわざ定義しないといけないので、 コードを書くことも読むことも煩雑になってしまうことです。
ニュータイプらしい(^ω^)
— くにくに (@Rznagashi) February 4, 2020
女の人はだれ? pic.twitter.com/sAC5NfJKzl
# 7.4. Any 型
Any はすべてのクラスの子クラスです。 こんなものいつ使われるのでしょうか? ご自身で書くことはないと思います。 型ヒントが明示されなかった変数、属性は、すべて Any として型付けされるそうです。
object 型ではダメなのでしょうか? ダメです。object 型はすべてのクラスの親クラスです。 以下の Person クラスを object 型で型付けしてしたとします。 object 型には name なんていう属性はないから弾かれてしまいます。
class Person(object):
name: str
def __init__(self, name):
self.name = name
person: object = Person('岩倉玲音')
print(person.name) # <--- object に name はないと弾かれます。
$ mypy sample.py
sample.py:8: error: "object" has no attribute "name"
Found 1 error in 1 file (checked 1 source file)
$
The Any type --- PEP 484 (opens new window)
特別な種類の型は Any です。すべての型は Any と見なすことができます。 Any をすべての値とすべてのメソッドを持つ型と見なすことができます。 Any と組み込み型オブジェクトは完全に異なることに注意してください (訳注: この一文が具体的になにを指しているかはわかりません)。
A special kind of type is Any. Every type is consistent with Any. It can be considered a type that has all values and all methods. Note that Any and builtin type object are completely different.値の型が object の場合、型検査機は その値に対するほとんどすべての操作を拒否します、 object 型の値をより特殊な型の変数に割り当てる (または戻り値として使用する)ことは型エラーとなります。
When the type of a value is object, the type checker will reject almost all operations on it, and assigning it to a variable (or using it as a return value) of a more specialized type is a type error.一方、値の型が Any である場合、 型検査機はその値に対するすべての操作を許可します、 型 Any の値はより制約の多い型の変数に割り当てることができます (または戻り値として使用できます)。
On the other hand, when a value has type Any, the type checker will allow all operations on it, and a value of type Any can be assigned to a variable (or used as a return value) of a more constrained type.
# 8. 書き方 - 構造的部分型
ダックタイピングがしたいです。
class カルガモクラス:
def 鳴く(self):
return 'ガー'
class マガモクラス:
def 鳴く(self):
return 'ガーガー'
def カモの合唱(カモリスト):
for カモ in カモリスト:
print(カモ.鳴く())
カモリスト = [カルガモクラス(), マガモクラス()]
カモの合唱(カモリスト)
>>> カモの合唱(カモリスト)
ガー
ガーガー
>>>
カモクラスを継承していないクラス、カルガモクラスとマガモクラスのインスタンスが、 List[カモクラス] として宣言された変数に代入できるようにしたいです。
カモリスト: List[カモクラス] = [カルガモクラス(), マガモクラス()]
"If it walks like a duck and quacks like a duck, it must be a duck"
タック・タイピング - Wikipedia (opens new window)
# 8.1. 問題
しかし、普通にやろうとすると mypy にエラーで弾かれてしまいます。 型ヒントを使いながら、ダックタイピングをするにはどうすればいいでしょうか?
from typing import List
class カモクラス:
def 鳴く(self) -> str:
...
class カルガモクラス:
def 鳴く(self) -> str:
return 'ガー'
class マガモクラス:
def 鳴く(self) -> str:
return 'ガーガー'
def カモの合唱(カモリスト: List[カモクラス]):
for カモ in カモリスト:
print(カモ.鳴く())
カモリスト: List[カモクラス] = [カルガモクラス(), マガモクラス()]
カモの合唱(カモリスト)
$ mypy sample.py
sample.py:20: error:
List item 0 has incompatible type "カルガモクラス";
expected "カモクラス"
sample.py:20: error:
List item 1 has incompatible type "マガモクラス";
expected "カモクラス"
Found 2 errors in 1 file (checked 1 source file)
$
expected "カモクラス"
って面白いですね笑
カモクラスのインスタンスが期待されていましたが、
カルガモクラスのインスタンスとマガモクラスのインスタンスが代入されていました。
みたいなそんなエラーです。
# 8.2. 解決策 1
継承すればエラーは消えますが、これはダックタイピングではありません。
from typing import List
class カモクラス:
def 鳴く(self) -> str:
...
class カルガモクラス(カモクラス):
def 鳴く(self) -> str:
return 'ガー'
class マガモクラス(カモクラス):
def 鳴く(self) -> str:
return 'ガーガー'
def カモの合唱(カモリスト: List[カモクラス]):
for カモ in カモリスト:
print(カモ.鳴く())
カモリスト: List[カモクラス] = [カルガモクラス(), マガモクラス()]
カモの合唱(カモリスト)
$ mypy sample.py
Success: no issues found in 1 source file
$ vim sample.py
# 8.3. 解決策 2
構造的部分型は Protocol を使うことで実現できます。
from typing import List, Protocol
class カモクラス(Protocol):
def 鳴く(self) -> str:
...
class カルガモクラス:
def 鳴く(self) -> str:
return 'ガー'
class マガモクラス:
def 鳴く(self) -> str:
return 'ガーガー'
def カモの合唱(カモリスト: List[カモクラス]):
for カモ in カモリスト:
print(カモ.鳴く())
カモリスト: List[カモクラス] = [カルガモクラス(), マガモクラス()]
カモの合唱(カモリスト)
$ mypy sample.py
Success: no issues found in 1 source file
$
このように Protocol を継承したクラスを事前、静的に定義します。 structural subtyping, 構造的部分型という名称とは別に statick duck typing, 静的ダックタイピングと表現されているのを目にします。 PEP 544 にて策定されました。
PEP 544 構造的部分型が導入された背景については、 イテラブルってなに?の中で説明させていただきました。
この構造的部分型を利用することで デフォルト引数値のあるような複雑な引数を取る関数にも 型ヒントをつけることができます。
# 9. 前方参照
# 9.1. 問題
Person を文字列にしないと NameError で弾かれます。
from typing import List
class Person:
name: str
age: int
friends: List[Person] # <--- NameError で弾かれます。
def __init__(self, name, age, friends):
self.name = name
self.age = age
self.friends = friends
person = Person('岩倉玲音', 14, [])
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in Person
NameError: name 'Person' is not defined
>>>
# 9.2. 解決策 1
なぜでしょうか? それは、変数 Person が定義されていないからです。 これを避けるには文字列で型名を記述します。
from typing import List
class Person:
name: str
age: int
friends: List['Person'] # <--- 文字列にする。
def __init__(self, name, age, friends):
self.name = name
self.age = age
self.friends = friends
person = Person('岩倉玲音', 14, [])
# 9.3. 解決策 2
from __future__ import annotations
を使うことで
文字列でない状態で使用することができます。
from __future__ import annotations
from typing import List
class Person:
name: str
age: int
friends: List[Person]
def __init__(self, name, age, friends):
self.name = name
self.age = age
self.friends = friends
person = Person('岩倉玲音', 14, [])
上記で記された機能は Python 3.7 から下記の特別な import を使うことで有効にすることができます。
The functionality described above can be enabled starting from Python 3.7 using the following special import:from __future__ import annotations
この機能の参照実装は GitHub から閲覧できます。
A reference implementation of this functionality is available on GitHub (opens new window).Enabling the future behavior in Python 3.7 (opens new window)
# 10. クラスと型の違い
「クラス」は、インスタンスオブジェクトに共通した値や処理をまとめた「もの」を指します。 「型」は、インスタンスオブジェクトの「種類」を指します。
# 10.1. PEP 483 で具体例を見ると...
PEP 483 によると int, str などの組み込み型と クラス定義文で定義したユーザ定義クラスは型 type でありかつクラス class に該当するそうです。 しかし typing モジュールから import する Union, List は、 型 type ではあってもクラス class ではないそうです。
- int は クラスであり、かつ型である.
- UserID は クラスであり、かつ型である.
- Union[str, int] は型であるが、 適切なクラスではない:
- int is a class and a type.
- UserID is a class and a type.
- Union[str, int] is a type but not a proper class:
- Types vs. Classes - PEP 483 (opens new window)
なぜでしょうか?Union や List は、インスタンス化や継承ができないからだそうです。 実体となる値や処理、あるいはクラス変数やメソッドを持っていない場合、クラスとはみなされません。
class MyUnion(Union[str, int]): ... # raises TypeError
Union[str, int]() # raises TypeError
この辺りの話をまとめると、 「クラス」は、インスタンスオブジェクトに共通した値や処理をまとめた「もの」を指していて、 「型」は、インスタンスオブジェクトの「種類」を指していると考えるといいかなと思います。
# 10.2. 用語集を確認すると...
用語集の定義は以下のようになっています。
class - 用語集 (opens new window)
(クラス) ユーザー定義オブジェクトを作成するためのテンプレートです。 クラス定義は普通、そのクラスのインスタンス上の操作をするメソッドの定義を含みます。
type - 用語集 (opens new window)
(型) Python オブジェクトの型はオブジェクトがどのようなものかを決めます。 あらゆるオブジェクトは型を持っています。オブジェクトの型は __class__ 属性でアクセスしたり、 type(obj) で取得したり出来ます。
# 10.3. むかしむかしの話...
用語集の定義だとユーザ定義クラスだけが、クラス class で、 組み込み型は、クラス class ではないことになってしまいます。 なぜでしょうか? おそらく更新されていないだけだからだと思います。
昔は Python 2 では組み込み型を type 型と表記し、 ユーザ定義クラスを class クラス と表記して区別していました。 Python 3 では完全に統合されました。 そのため「組み込み型」も「クラス」と考えて良いかなと思います。
>>> # Python 2
>>> C
<class __main__.C at 0x10ad70a78>
>>> int
<type 'int'>
>>>
>>> # Python 3
>>> C
<class '__main__.C'>
>>> int
<class 'int'>
>>>
- Unifying types and classes in Python 2.2 - Guido van Rossum (opens new window)
- Python : terminology 'class' VS 'type' - Stackoverflow (opens new window)
# 11. 追記予定
# 11.1. 不変, 共変, 反変
# 11.2. stub ファイル
# 11.3. typeshed
# 11.4. 問題
このオブジェクトに型をつけてください... 的な...
# 12. 参考文献
PyCon の動画です。多くを参考にさせていただきました。 ありがとうございます。
そのほか、参考にさせていただいた資料です。
- Python と型アノテーション - SlideShare (opens new window)
- Python と型ヒント (Type Hints) - SlideShare (opens new window)
- Pythonと型チェッカー - SlideShare (opens new window)
- What is Gradual Typing: 漸進的型付けとは何か - Qiita (opens new window)
- [翻訳] Python の静的型、すごい mypy! - Qiita (opens new window)
- [翻訳] PEP 0484 -- 型ヒント (Type Hints) - Qiita (opens new window)
- Revenge of the Types: 型の復讐 - Qiita (opens new window)
- Adding Optional Static Typing to Python - Matzにっき (opens new window)
- Optional Static Typing -- Stop the Flames! (opens new window)
- Python への型アノテーション導入を目指す GvR 氏 (opens new window)
- 型安全性とは何か - POSTD (opens new window)
# 13. おわりに
ここまで以下のように見てきました。
次は、動的型付けと静的型付けを比較しながら、 型ヒントをいつ使うのかの使い分けについて考えていきます。