Python Typesのすすめ

Robust Python Programming 2/N

公開日: 2024-03-13
更新日: 2024-03-13

  Table of Contents

データの型付け, typingはなぜ必要か

Pythonランタイムは, 関数と変数のtype annotationを強制しません. にもかかわらず typing が推奨されるのはtypeが次の情報を伝えてくれるからです

  • Mechanical representation: Python側に挙動の範囲や制約を知らせる意味(静的解析などへ)
  • Semantic representation: 他の開発者にに挙動の範囲や制約を知らせる意味(refactoringなどへの活用)

Mechanical Representation

Python compilerはPython programにおけるobjectsのtypeについては何も認識せず, 初めてわかるタイミングが Python run-timeです. わざわざ変数の型を宣言/制約することなく柔軟なcodingが実現できるメリットがある一方, 予期せぬBugsを引き寄せるリスクもあります.

例として, 最大公約数を計算する次の関数を定義したとします

1
2
3
4
def gcd(a, b):
    while a:
        a, b = b%a, a
    return b

シンプルで良いコードですが, 最大公約数を計算するという観点からabfloat型が入ることは開発者は想定 していないのが一般的です. 一方, このままだとPythonはfloat型が入ってきてもそのまま何も気にせず計算を始めてしまいます.

さらにstrが入ってきてしまったときはwhileから抜け出せなくなってしまうというBugが発生します. このようなBugsを回避するために, typingが重要となります. PythonではDynamic typingを採用しているのでtype annotationを用いて以下のように記載するのが良いでしょう:

1
2
3
4
def gcd(a: int, b: int) -> int:
    while a:
        a, b = b%a, a
    return b

Semantic Representation

Semantic representationの意味におけるTypingは, エンティティについての期待される 挙動や制約, 使用方法などの情報を非同期的に他の開発者に対して伝えることを可能にします.

int型の変数が与えられとき,

  • 四則演算などの算術演算が可能
  • <, >, ==, !=といった比較演算子が利用可能
  • Bitwise演算が利用可能
  • ceil, floor, roundといったmethodが利用可能

という情報を得ることができます. 他にも datetime型が与えられたならば, timedeltaによる 加算/減算が可能, TZの変更が可能, .strftime methodを利用してstr型へ変換可能ということがわかります.

Semantic RepresentationがRobust codeへどのようにつながるか以下の関数を例に考えてみます.

1
2
3
4
def close_kitchen_if_past_cutoff_time(point_in_time):
    if point_in_time >= closing_time():
        close_kitchen()
        log_time_closed(point_in_time)

point_in_timeを用いて動かす関数ということはわかりますが,

  • str型で2024-03-13のような値をpoint_in_timeに入れるべきかのか?
  • datetime型を予定しているのか?

この辺が判別できず, 適切な使い方がわからないという事象が発生してしまいます. 以下のようなtypingを用いることでこのような判断の迷いを低減することができます

1
2
3
4
def close_kitchen_if_past_cutoff_time(point_in_time: datetime.datetime):
    if point_in_time >= closing_time():
        close_kitchen()
        log_time_closed(point_in_time)

このようにtypingされることで, 他の開発者はわざわざdocumentationを見たり, unittestを実施したり, わざわざオフィスに出社して対面で聞くなどの労力を割かなくても良くなります.

Typing Systems

Typingの具体的アクションを紹介する前にまず, Pythonは動的かつ強力なTyping Systemをもつことを紹介します. そこから, Pythonは互換性のない型を使用した際にエラーを通知してくれる一方, 動的型付けの性質より変数の型がruntime中に変化してしまうことを確認します.

Strong vs Weak

Typing systemには強弱というものがあります. Haskell, TypeScript, Rustといった言語は 強い型付けシステム(Typing system)の代表例です. 型の意味的表現を破った操作をするとコンパイラエラーやランタイムエラーによって通知されます.

一方, JavaScript, Perlなどの弱い型付けシステム言語は, 型の不整合があっても暗黙的な変換(自動的な変換)により型エラーが検出されなかったりします. この意味でPythonは強い型付けシステム言語に分類されます.

異なるデータ型同士の演算: Python vs JavaScript

Python

1
2
3
4
5
>>>[] + {}
TypeError: can only concatenate list (not "dict") to list

>>> {} + []
TypeError: unsupported operand type(s) for +: 'dict' and list

JavaScript

1
2
3
4
5
>>> [] + {}
"[object Object]"

>>> {} + []
0

しかしながらこのエラーはruntime時に初めてわかるものです. Typingをはじめから実施しておけば, 実行しなくても このようなエラーを見つける & 回避することができるので時間節約という観点からもTypingは重要であることがわかります.

Dynamic Versus Static

Typingの文脈において, Dynamic typingとStatic Typingという分類があります.

  • Dynamic typing: 変数の型が実行時に解釈され, 変数を宣言する際に型を明示的に指定する必要がないことを意味
  • Static typing: 変数の型が定義時に宣言され, runtime中に変化することがない(= static)ことを意味

Pythonは基本的には動的型付け言語で, 変数の値の中に型情報が埋め込まれています. Pythonは変数の型を実行時に変更することについて何のエラーも通知するなく以下のように実行できます:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> a = 5
>>> a = "string"
>>> a
"string"

>>> a = tuple()
>>> a
()

>>> a: int = 5
>>> a = "string"
>>> a
"string"

最後のブロックの例は変数 aint型で定義したにもかかわらず文字列が格納可能というPythonの悪い部分の例です.

Duck Typing

Duck Typing
If it walks like a duck and it quacks like a duck, then it must be a duck.

Duck typingとはlatent typing(潜在的なtyping)structural typing(構造的ななtyping)を意味します.

Duck Typingの有名な引用と照らし合わせるならば,

  • I don’t care what type you really are, as long as you walk() and quack().

ということなります. 例として以下のコードを見てみます.

1
2
3
4
5
6
7
8
from typing import Iterable
def print_items(items: Iterable):
    for item in items:
        print(item)

print_items([1,2,3])
print_items({4, 5, 6})
print_items({"A": 1, "B": 2, "C": 3})

関数 print_items は引数 itemsについて厳密な型確認は行っていませんが, データ型がIterableかどうか の確認はしています. 渡されたオブジェクトについて __iter__ methodが存在するか確認し, 確認できたらloop処理に入るという挙動をします. __iter__ methodが存在しないobjectを渡すと以下のようなエラーが出ます

1
2
3
4
5
6
7
8
9
10
11
>>> print_items(5)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in print_items
TypeError: 'int' object is not iterable

>>> '__iter__' in dir(int)
False
>>> '__iter__' in dir(list)
True

関数で使用される変数やmethodをサポートしている型であれば(=構造的型付け), その関数で自由にその型を使用できるというのがDuck Typingです. だからこそ, I don’t care what type you really are, as long as you walk() and quack().

Appendix: Glossary

Word Explanation
Entity Pythonにおけるobjectやinstance, variable, function, class, moduleを示す言葉
Python runtime Python codeが実行されるシステムや環境のこと
Semantics 意味論. operationの意味とプログラミング文脈では解釈される

References



Share Buttons
Share on:

Feature Tags
Leave a Comment
(注意:GitHub Accountが必要となります)