Last Updated: 2/6/2024, 5:44:57 AM

# 型ヒントと 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 whenever float 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 が使用できない理由を思い出してください。

# ◯ 参考

またジェネリクス周りはこちらのツイートも大変参考になります。 ありがとうございます。

# 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 を使うことで可読性と安全性を上げることができました。 各々のデメリットは、わざわざ定義しないといけないので、 コードを書くことも読むことも煩雑になってしまうことです。

# 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'>
>>> 

# 11. 追記予定

# 11.1. 不変, 共変, 反変

# 11.2. stub ファイル

# 11.3. typeshed

# 11.4. 問題

このオブジェクトに型をつけてください... 的な...

# 12. 参考文献

PyCon の動画です。多くを参考にさせていただきました。 ありがとうございます。




そのほか、参考にさせていただいた資料です。

# 13. おわりに

ここまで以下のように見てきました。

次は、動的型付けと静的型付けを比較しながら、 型ヒントをいつ使うのかの使い分けについて考えていきます。