Help us understand the problem. What is going on with this article?

Python typingまとめ

PyLadies Japan アドベントカレンダー24日目の投稿です。
typingについて自学自習のためにまとめました。

サマリー

  • Python3.5から導入 ( PEP-483 )
  • 型ヒントを定義できる
  • 型ヒント通りに実装されているかどうかはmypy使うと良い
  • 実行時には型ヒントの制約を受けない
  • 独自の型をtyping.NewTypeで定義し、型ヒントとして利用できる

mypyで型チェック

準備

pip install mypy

Hello world

まずは簡単な例を試してみる

example.py
def greeting(name: str) -> str:
    return 'Hello ' + name

msg = greeting('tae')
print(msg)

str の部分が型ヒントです。 name: strが引数の型、 -> strの部分が戻り値の型を表しています。

# とりあえず実行してみる(OKバージョン)
❯❯❯ mypy example.py
Success: no issues found in 1 source file

間違った型の値を渡してみる

example.py
def greeting(name: str) -> str:
    return 'Hello ' + name

msg = greeting(10) # <- int を渡してみる
print(msg)
# とりあえず実行してみる(Errorバージョン)
❯❯❯ mypy example.py
example.py:4: error: Argument 1 to "greeting" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

ちゃんとエラー出してくれた\(^o^)/

独自の型を定義してみる

アプリケーション内で、独自の型を定義できると嬉しいので試してみます。
Typing.NewTypeを利用します。

from typing import NewType

UserId = NewType('UserId', int)
PostId = NewType('PostId', int)

def greeting(name: str, userId: UserId) -> str:
    return 'Hello ' + name + " UserId:{userId}".format(userId=userId)

msg = greeting('tae', PostId(10)) # <--UserId(10)が正しい
print(msg)

このまま実行すると、成功してしまいます。

❯❯❯ python example.py
Hello tae UserId:10

mypyでチェックすると、誤った型の値を渡していることを確認できます。

❯❯❯ mypy example.py
example.py:9: error: Argument 2 to "greeting" has incompatible type "PostId"; expected "UserId"
Found 1 error in 1 file (checked 1 source file)

実行時に型チェックする

実行時にはtypingで指定した型による制約を受けません。ただ、デバッグ時や外部サービスから入力された値の型など、実行時に型チェックしてエラーにしたい場合があると思います。

ここではtypeguardというライブラリを利用してみます。
https://github.com/agronholm/typeguard

異なる型(int <- string)を渡してみる

example.py
from typing import NewType
from typeguard import typechecked

UserId = NewType('UserId', int)
PostId = NewType('PostId', int)

@typechecked
def greeting(name: str, userId: UserId) -> str:
  return 'Hello ' + name + " UserId:{userId}".format(userId=userId)

msg = greeting('tae', 'number') # <-- stringを入れて渡してみる
print(msg)
❯❯❯ python example.py
Traceback (most recent call last):
  File "example.py", line 7, in <module>
    msg = greeting('tae', 'number')
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 809, in wrapper
    check_argument_types(memo)
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 670, in check_argument_types
    raise exc from None
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 668, in check_argument_types
    check_type(description, value, expected_type, memo)
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 596, in check_type
    raise TypeError(
TypeError: type of argument "userId" must be int; got str instead

=> エラーになった\(^o^)/

異なる型(UserId <- PostId)を渡してみる

example.py
from typing import NewType
from typeguard import typechecked

UserId = NewType('UserId', int)
PostId = NewType('PostId', int)

@typechecked
def greeting(name: str, userId: UserId) -> str:
  return 'Hello ' + name + " UserId:{userId}".format(userId=userId)

msg = greeting('tae', PostId(10))
print(msg)
❯❯❯ python example.py
Hello tae UserId:10

=> 実行できてしまった、、

NewTypeの型について

https://docs.python.org/ja/3/library/typing.html#newtype

UserId 型の変数も int の全ての演算が行えますが、その結果は常に int 型になります。 この振る舞いにより、 int が期待されるところに UserId を渡せますが、不正な方法で UserId を作ってしまうことを防ぎます。
これらのチェックは静的型検査器のみによって強制されるということに注意してください。

shell commandで確認してみます

Python 3.8.0 (default, Dec 22 2019, 19:51:13)
>>> type(UserId(10))
<class 'int'>

型指定ない関数を経由して値を渡す

下記のようなケースを考えてみます。

example.py
# type指定がある
def greeting(name: str) -> str:
    return name

# type指定が無い
def print_msg(name):
    print(greeting(name))
    return

print_msg(1) # 本当はstrを渡すべき仕様

この場合mypy検査ではSuccessが返ってきます

❯❯❯ mypy example.py
Success: no issues found in 1 source file

このコードは実行時にもエラーになりません。先程のtypeguardを利用すれば実行時にエラーを補足できます。

example.py
from typeguard import typechecked

# type指定がある
@typechecked
def greeting(name: str) -> str:
    return name

# type指定が無い
def print_msg(name):
    print(greeting(name))
    return

print_msg(10)
❯❯❯ python example.py
Traceback (most recent call last):
  File "example.py", line 13, in <module>
    print_msg(10)
  File "example.py", line 10, in print_msg
    print(greeting(name))
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 809, in wrapper
    check_argument_types(memo)
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 670, in check_argument_types
    raise exc from None
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 668, in check_argument_types
    check_type(description, value, expected_type, memo)
  File "/Users/tae/.local/share/virtualenvs/typing-example-z06iHRBf/lib/python3.8/site-packages/typeguard/__init__.py", line 596, in check_type
    raise TypeError(
TypeError: type of argument "name" must be str; got int instead

所感

mypyを利用することで、仕様通りの型で実装しているかどうかチェックすることでアプリケーションの安全性を高めることができそうです。NewTypeを利用することで、同じ数値表現であるUserId, PostIdを区別して誤った種類のIDを渡すような実装をしてしまった・・みたいな事も防げそうです。運用上は、CI-toolと組み合わせて自動チェックできるようにすると良いと思います。
型ヒントを用いた動的型チェックはアプリケーション全般的に利用するというよりは、限定的なユースケースで利用するのが現実的かなぁという印象です。

参考

この記事のコード: https://github.com/tae124/typing-example
https://docs.python.org/ja/3/library/typing.html
https://qiita.com/icoxfog417/items/c17eb042f4735b7924a3

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした