これは、なにをしたくて書いたもの?
簡単なPythonスクリプトを書く時はEmacsでlsp-modeを使っていることが多いのですが、型定義を補完してくるのがよく目に入るので
せっかくなので少し押さえておこうかなと思いまして。
Pythonの型ヒント
Pythonの型ヒントに関するドキュメントはこちら。
typing --- 型ヒントのサポート — Python 3.10.19 ドキュメント
参考)
Python最新バージョン対応!より良い型ヒントの書き方 | gihyo.jp
詳細な仕様はPEP 484、
PEP 484 – Type Hints | peps.python.org
導入に関する説明はPEP 483にあります。
PEP 483 – The Theory of Type Hints | peps.python.org
型ヒントを使った例を見てみましょう。
以下はstr型のnameを受け取り、str型の戻り値を返す関数です。
def greeting(name: str) -> str: return 'Hello ' + name
こんな感じで変数の型は:の後ろに書き、関数の戻り値の型は->の後ろに書くようです。
いくつか特徴的なものを見てみましょう。
- typing --- 型ヒントのサポート / 型エイリアス
- ある型のエイリアスを定義
- typing --- 型ヒントのサポート / NewType
- ある型から新しい型を作成する。新しい型は元々の型のサブクラスのように扱われる
- typing --- 型ヒントのサポート / 呼び出し可能オブジェクト
- 関数を変数にバインドする際の型定義
collections.abc.Callableを使う
- typing --- 型ヒントのサポート / ジェネリクス
- コンテナ型のオブジェクトに型情報を指定する
- 使い方によっては
typingモジュールが必要 - 組み込み型でもジェネリクスが使えるものがある
- ユーザーにも定義可能
- typing --- 型ヒントのサポート / Any 型
- すべての型と互換性のある特別な種類の型
指定する型は、基本的には組み込み型のものですね。
また、typingモジュールを使うことで型定義に使える内容が増えます。一部を載せておきましょう。
- 特殊型付けプリミティブ
- 特殊型
typing.Anytyping.NoReturntyping.TypeAlias
- 特殊形式
typing.Tupletyping.Uniontyping.Optionaltyping.Callablecollections.abc.Callableを使うこと
typing.Concatenateclass typing.Typetyping.Literaltyping.ClassVartyping.Finaltyping.Annotatedtyping.TypeGuard
- Building generic types
- ...
- Other special directives
- ...
- 特殊型
- Generic concrete collections
- ...
- 抽象基底クラス
- ...
挙げていくとキリがないので、途中から端折りました…。
ちなみに、Pythonでは型ヒントを必須にするつもりはなく、動的型付け言語であることは変わらないそうです。
It should also be emphasized that Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.
PEP 484 – Type Hints / Rationale and Goals / Non-goals
よって、型ヒントは実行時には機能せず、誤った型を指定してもふつうに実行できます。
bad_type.py
def add(a: int, b: int) -> int: return a + b print(add("foo", "bar"))
$ python3 bad_type.py foobar
とはいえ、lsp-modeやPyCharmといった型ヒントを利用できる環境では、誤った型を指定すると警告が表示されます。
Mypy
MypyはPythonの型チェックツールです。
mypy - Optional Static Typing for Python
GitHub - python/mypy: Optional static typing for Python
例はこちらに記載があり、typingもここでは使われています。
ドキュメントはこちら。
Getting started。
Getting started - mypy 1.18.2 documentation
型の指定方法はこちらのチートシートを見るのがよいでしょうね。
Type hints cheat sheet - mypy 1.18.2 documentation
使い方は、チェック対象のファイルやディレクトリを指定して実行するようです。
$ mypy foo.py bar.py some_directory
The mypy command line - mypy 1.18.2 documentation
設定ファイルも定義できるみたいですね。
The mypy configuration file - mypy 1.18.2 documentation
Pythonスクリプト内にインラインでそのファイルに対する設定を書くこともできるようです…。
Inline configuration - mypy 1.18.2 documentation
今回はMypyは少し試してみるくらいにしましょう。
環境
今回の環境はこちら。
$ python3 --version Python 3.10.12 $ pip3 --version pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)
Pythonの型ヒントを試してみる
それでは、型ヒントを試してみましょう。
ざっと例を。
type_hint_getting_started.py
## str型 message: str = "Hello Python!!" ## bool型 contains_python: bool if "Python" in message: contains_python = True else: contains_python = False ## list[str] programming_languages: list[str] = ["Python", "Java", "JavaScript", "TypeScript", "C#"] for language in programming_languages: print(language) ## 関数定義 def plus(a: int, b: int) -> int: return a + b ## クラス定義 class Person: first_name: str last_name: str age: int def __init__(self, first_name: str, last_name: str, age: int) -> None: self.first_name = first_name self.last_name = last_name self.age = age def say(self) -> str: return f"名前は{self.last_name}{self.first_name}です" katsuo = Person("カツオ", "磯野", 11) print(katsuo.say())
これで終わってもなんなので、いくつか気になることを書いていきましょう。
辞書型は?
dictに2つ型引数を指定します。
people: dict[str, int] = { "磯野カツオ": 11, "磯野ワカメ": 9, "フグ田タラオ": 3 }
for文の中に登場する変数の型の指定は?
先ほどのサンプルでこういうものを書きましたが、language変数の型がありません。
## list[str] programming_languages: list[str] = ["Python", "Java", "JavaScript", "TypeScript", "C#"] for language in programming_languages: print(language)
このlanguageの型を指定するには?とちょっと思ったりするのですが、これは指定しなくて良さそうです。ループに使用している
コレクションの型定義から決まっているようです。
この記述でlsp-modeなどでもlanguageはstr型であることを認識していますし、後述のMypyでの型チェックでもNGになりません。
戻り値のない関数の戻り値の型は?
Noneです。
def hello() -> None: print("Hello World")
コンストラクタの戻り値の型は?
Noneです。
def __init__(self, first_name: str, last_name: str, age: int) -> None:
クラスメソッドでそのクラスのインスタンスを返す場合の戻り値の型は?
こんな感じで書きたくなるのですが
class Person: first_name: str last_name: str age: int def __init__(self, first_name: str, last_name: str, age: int) -> None: self.first_name = first_name self.last_name = last_name self.age = age @classmethod def create(cls, first_name: str, last_name: str, age: int) -> Person: return cls(first_name, last_name, age)
これは実行できなかったりします。
Traceback (most recent call last):
File "/path/to/some_file.py", line 6, in <module>
class Person:
File "//path/to/some_file.py", line 17, in Person
def create(cls, first_name: str, last_name: str, age: int) -> Person:
NameError: name 'Person' is not defined
文字列にする必要があるみたいです。
@classmethod def create(cls, first_name: str, last_name: str, age: int) -> "Person": return cls(first_name, last_name, age)
型ヒントのドキュメントでは、typing.TypeVarを使って戻り値で使う型を定義しています。
typing --- 型ヒントのサポート / モジュールの内容 / Building generic types / class typing.TypeVar
関数を受け取る関数を定義するには?
collections.abc.Callableを使います。
from collections.abc import Callable ## 引数ありの関数を受け取る def filter(languages: list[str], func: Callable[[str], bool]) -> list[str]: return [language for language in languages if func(language)] ## 引数なしの関数を受け取る def hello(name_generator: Callable[[], str]) -> None: print(f"Hello {name_generator()}") print(filter(["Java", "JavaScript", "Python"], lambda l: "JavaScript" in l)) hello(lambda: "Callable" )
Mypyを使ってみる
最後にMypyを使ってみましょう。
インストール。
$ pip3 install mypy
インストールされたライブラリーの一覧。
$ pip3 list Package Version ----------------- ------- mypy 1.10.1 mypy-extensions 1.0.0 pip 22.0.2 setuptools 59.6.0 tomli 2.0.1 typing_extensions 4.12.2
最初に書いた誤った型を指定して関数を呼び出しているスクリプトで試してみましょう。
bad_type.py
def add(a: int, b: int) -> int: return a + b print(add("foo", "bar"))
こんな感じで怒られます。
$ mypy bad_type.py bad_type.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int" [arg-type] bad_type.py:4: error: Argument 2 to "add" has incompatible type "str"; expected "int" [arg-type] Found 2 errors in 1 file (checked 1 source file)
カレントディレクトリ配下や特定のディレクトリ配下のスクリプトをまとめて指定したい場合は、以下のようにディレクトリを指定すれば
OKです。
$ mypy . $ mypy [directory]
また--disallow-untyped-defsオプションを追加することで、型指定のない定義があるとエラーにできます。
$ mypy --disallow-untyped-defs bad_type.py
たとえばこういうコードを追加して実行すると
def message(s): print(s)
型アノテーションがないと怒られます。
bad_type.py:6: error: Function is missing a type annotation [no-untyped-def]
こんなところでしょうか。
オマケ: 型情報がない場合
Mypyを使って型チェックを行う際に、型情報がない場合はスタブファイルというものを作成する必要があるようです。
Stub files - mypy 1.18.2 documentation
typeshedというリポジトリーには、標準ライブラリーやサードパーティ性のライブラリーなどの型情報が含まれているので、こちらを
使うことになるようですね。
GitHub - python/typeshed: Collection of library stubs for Python, with static types
こちらについては、またいずれ見てみたいと思います。
おわりに
Pythonで型ヒントを試してみました。合わせて、Mypyも使ってみました。
ちょっとした導入的なものでしたが、今まで完全に雰囲気で型ヒントを見ていたのでこの機会にある程度向き合っておいてよかったかなと。
Pythonで型ヒントを使うかどうかは微妙なところな気がしますが、個人的にはここで書くスクリプトについては適用していきたいなと
思ったりしています。