読者です 読者をやめる 読者になる 読者になる

mypy で静的型付け Python プログラミング

python3 testing

Pythonへの型アノテーション導入を目指すGuido van Rossum氏

mypy の型アノテーションの構文を Python に取り込もうとメーリングリストで提案したニュースが話題になりました。今夏に話題になったニュースを今冬にフォローするという季節外れ感ですが、ずっと興味はあって「あとで読む」状態になっていたのを調べてみました。

基本的には mypy のドキュメントから自分の興味のあるところをかいつまんで説明しています。分かりにくいところがあれば原文を参照してください。

Welcome to Mypy documentation!

mypy とは

mypyPython の静的型チェッカー (static type checker) です。wikipedia:静的コード解析wikipedia:en:Lint (software) ツールの一種だと説明されています。

Python2 にはなく、Python3 で PEP 3107 - Function Annotations が追加されました。Python 3.0 がリリースされたのが 2008-12-03 なので6年ぐらい前のことです。

>>> def f(i: int, s: str) -> None: pass
>>> f.__annotations__
{'s': <class 'str'>, 'return': None, 'i': <class 'int'>}

但し、これは情報として保持しているだけで関数アノテーションをどう活用するかはライブラリやサードパーティツールに委ねられていました。mypy はそんなツールの1つであり、先のコードのように関数アノテーションに型シグネチャを記述することで静的解析に利用しようといったものです。重要な点の1つとして、関数アノテーションは実行時に影響を及ぼさないため、仮にこの型シグネチャの定義が間違っていたとしても何も起こりません。

さらに関数定義だけでなく、コレクションや変数定義にも型シグネチャを付加する仕組みがあり、それらをまとめて型アノテーション (type annotation) と呼ぶようです。Python 標準の組み込み型だけでは型シグネチャを記述できないため、Built-in types に mypy で定義された組み込み型が紹介されています。

実際に mypy の型アノテーションの宣言を見てみましょう。

>>> from typing import Dict, List
>>> l = List[int]()       # Create empty list with type List[int]
>>> l
[]
>>> d = Dict[str, int]()  # Create empty dictionary (str -> int)
>>> d
{}

これは空のリストとディクショナリの型アノテーションを宣言しています。型アノテーションを定義しつつも実際に空のリストが作成されています。typing.py に型アノテーションのための型定義があるので少し覗いてみます。

class TypeAlias:
    """Class for defining generic aliases for library types."""

    def __init__(self, target_type):
        self.target_type = target_type

    def __getitem__(self, typeargs):
        return self.target_type

List = TypeAlias(list)
...

TypeAlias に組み込み型を渡して self[key] で評価されたときにその組み込み型を返します。

>>> List is list
False
>>> List[int] is list
True

シンプル且つ実際に動く Python のコードな、うまい仕組みですね。

Python3 への導入

Guido 自ら提案したせいか、メーリングリストで多くの議論が行われたようです。興味がある方は以下のメールのスレッドを追いかけてみると良いと思います。私はそこまで根気がなくて発端となった Guido の提案メールのみを読んでみました。

前者のメールの要約は冒頭に紹介した InfoQ の内容なのでそれを参照してください。後者のメールは、その提案に対する反応が3つの質問に分類されるとみなし、それぞれの質問に対する Guido の回答のようです。いくつか要点を抜き出して意訳してみます。

(A) 選択的静的型付け (Optional static typing) の標準化は本当に必要なのか?

多くの人が肯定的であり、数人は不要だと判断しているが、反対している人の大半はその代替となる自分の提案があるようにみえる。確信はないのだけれど、自分の直感で言うと、できるだけこれはオプションにしておきたい。どんなケースでも、それが本当に価値があるものかどうかは実際に作り始めるまでは分からないし、これは大丈夫だろうと信じて推進するしかない。前提としていることを1つ付け加えると、主な用途は lint 的なこと、IDE、ドキュメント生成になるだろう。これらに1つ共通して言えることは、型チェックが失敗したとしてもプログラムは実行できるし、型を追加することがそのパフォーマンスを下げないということだ (上げることもないが :-) 。

(B) 選択的静的型付け (Optional static typing) の標準としてどんな構文にするか?

たくさんの興味深い質問がみられた。実現方法として、3つか4つの "families" があって、まずやることはその系統を選択することだ。

1. mypy family
関数アノテーションの特徴を活かし、Python の構文としてもそのまま有効だ。標準ライブラリや C 拡張のアノテーションを集約する "stub" ファイルを別に設けることもできる。mypy のアノテーションが (stub ファイルではなく) インラインに記述されると、アノテートされた関数本体の型チェクと同様に呼び出し側の入力の型チェックにも使われる。

2. pytypedecl family
独立した stub ファイルでカスタム構文を使うため、Python の構文に制約を受けず、若干 mypy よりは洗練されているようにみえる。

3. PyCharm family
docstings でのみ使われるカスタム構文である。stub ファイルを使うこともできる。

4. コメントに全て書く方法を4番目の系統になり得るが、誰もが真面目にコメントを書くとは思えないし、利点も分からない。

さぁ、どうやって選択しよう?私は攻撃的にも防御的にもそれぞれのアプローチで熱心に内容を読み取った。3つのプロジェクトは異なる成熟期にある (おもちゃ以上、プライムタイムには及ばない) というのが実感だ。特定の型システムの機能 (前方参照、総称型、ダックタイピング) に関しては、私は全てに満足できるものを期待していて、おそらくはまだやることがある。どのプロジェクトも stub をサポートしているので、既存のコードを修正することなくシグネチャを指定できる。

私にとって、間違いなく mypy が最も Pythonic なアプローチだ。我々が PEP 3107 (function annotations) を議論したとき、最終的に型アノテーションのために使われるのがずっと私の目標だった。当時は型チェックのルールになるといった合意はなかったが、構文上の位置付けからそれを疑う余地はなかった。そのため、我々は、サードパーティの実験の成果が最終的に満足できるものを創り出すのを願って Python3 に "annotation" を導入することを決定した。mypy はそういった実験の1つだ。mypy から私が得た大事な教訓の1つは、型アノテーションは linter に対して最も有益であり、(通常は) 実行時に型を強制するために使われるべきではないということだ。またそれらはコード生成の役には立たない。我々が PEP 3107 で議論していたときには全く分からなかったことだ!

(中略)

ということで、私は mypy family を選択していて、mypy の改善についての議論に移っていこう。そして誰かが pytypedel や PyCharm stub から mypy stub への変換ツールを作ってくれて、これらの2つのプロジェクトの stub 定義を再利用できることを願う。無論、PyCharm や pytypedecl が mypy の構文を導入することも願っている (まずはネイティブの構文に追加して、最終的には1つの構文になる) 。

(C) 他の機能にも対応する/すべき?
(追伸) この質問についてはあまり議論しなかったことに気付いた。わざとだ。特定の mypy の機能については別スレッドでこれから議論しよう (このスレッドでもいいけど :-) 。

Python 3.5 のリリーススケジュールは以下になります。feature freeze は Beta1 (May 24, 2015) のようです。

ちょうど型アノテーションの PEP のドラフト (のドラフト?) が Guido からメーリングリストに投稿されました。

アノテーションと型システムの議論

mypy について調べていて見つけた記事などのリンクです。

私には難しくてまとめきれないため、Guido が参照している漸進的型付け (Gradual Typing) も含め、また別の機会に、、、。

追記: 以下にまとめました。

mypy を使ってみよう

さて、ここからが本題です。

mypy のインストール

PyPI にも登録されていますが、ここでは github からソースをクローンしてインストールします。現時点では、mypy は Python 3.2 以上しかサポートしていませんが、 Python2 対応も開発中だと FAQ にあるのでいずれサポートされるかもしれません。

$ mkvirtualenv -p /opt/local/bin/python3.4 mypy
(mypy)$ git clone git@github.com:JukkaL/mypy.git
(mypy)$ cd mypy/
(mypy)$ python setup.py install
(mypy)$ mypy -h
usage: mypy [option ...] [-m mod | file]

Optional arguments:
  -h, --help         print this help message and exit
  --html-report dir  generate a HTML report of type precision under dir/
  -m mod             type check module
  --verbose          more verbose messages
  --use-python-path  search for modules in sys.path of running Python

Environment variables:
  MYPYPATH     additional module search path

ライブラリーのスタブ

Python の標準ライブラリに含まれるモジュールを使うコードの型チェックを行うには、public なインターフェイスやクラス、変数、関数などのスケルトンをスタブとして定義しないといけません。どんな雰囲気か mypy/stubs at master · JukkaL/mypy · GitHub にあるのを見た方が手っ取り早いと思います。作成したスタブの場所は環境変数で指定できるようです。

$ export MYPYPATH=~/work/myproject/stubs

チュートリアル

関数アノテーションに型アノテーションを指定する簡単な例から見てましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 import typing                                                                    
  3                                                                                  
  4 def greeting1(name: str) -> None:                                                
  5     return 'Hello, {}'.format(name)                                              
  6                                                                                  
  7 def greeting2(name: str) -> int:                                                 
  8     return 'Hello, {}'.format(name)                                              
  9                                                                                  
 10 def greeting3(name: str) -> str:                                                 
 11     return 'Hello, {}'.format(name)                                              
 12                                                                                  
 13 print(greeting1('john'))                                                         
 14 print(greeting2('bob'))                                                          
 15 print(greeting3('mike'))    

このプログラムは mypy の機能を使っていない純粋な Python3 のスニペットです。

(mypy)$ python check_function_signature.py 
Hello, john
Hello, bob
Hello, mike

普通に実行できました。しかし、せっかく指定した関数アノテーションの返り値が間違っているものがありますね。これを mypy というコマンドラインツールで実行すると lint 的に静的解析してくれます。

(mypy)$ mypy check_function_signature.py 
check_function_signature.py: In function "greeting1":
check_function_signature.py, line 5: No return value expected
check_function_signature.py: In function "greeting2":
check_function_signature.py, line 8: Incompatible return value type: expected builtins.int, got builtins.str
check_function_signature.py: At top level:
check_function_signature.py, line 13: "greeting1" does not return a value

mypy が静的型チェッカーだというのは、Python のプログラムをインタープリターを実行する前に、自分で mypy ツールを実行して型チェックを行い、そのエラーを修正するといった使用方法だからです。

次の例をみてみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Iterable                                                      
  3                                                                                  
  4 def greet_all1(names: Iterable[str]):                                            
  5     for name in names:                                                           
  6         print('Hello, {}'.format(name))                                          
  7         'mr. ' + name                                                            
  8         name + 1                                                                 
  9                                                                                  
 10 greet_all1(['john', 'bob'])                         

こんどは Iterable を使って型アノテーションを定義しています。

(mypy)$ mypy check_function_signature_with_error.py 
check_function_signature_with_error.py: In function "greet_all1":
check_function_signature_with_error.py, line 8: Unsupported operand types for + ("str" and "int")

おや!?8行目に str 型と int 型の加算があります。これを Python インタープリターで実行すると、

(mypy)$ python check_function_signature_with_error.py 
Hello, john
Traceback (most recent call last):
  File "check_function_signature_with_error.py", line 10, in <module>
    greet_all1(['john', 'bob'])
  File "check_function_signature_with_error.py", line 8, in greet_all1
    name + 1
TypeError: Can't convert 'int' object to str implicitly

エラーになりますね。とても単純な例ですが、1 というリテラルの値が int 型だと型推論されて mypy がエラーを指摘しています。

もっと分かりやすい型推論のサンプルも見てみましょう。

  1 # -*- coding: utf-8 -*-                                                                         
  2 import typing                                                                                   
  3                                                                                                 
  4 # int                                                                                           
  5 i = 1                                                                                           
  6 i = 2                                                                                           
  7 i = int(3)                                                                                      
  8 i = 'x'                                                                                         
  9                                                                                                 
 10 # list                                                                                          
 11 l = [1, 2]                                                                                      
 12 l.append(3)                                                                                     
 13 l.append('x')                                                                                   
 14 l.extend([4, 5])                                                                                
 15 l.extend([6, 'y'])                                                                              
 16                                                                                                 
 17 # dictionary                                                                                    
 18 d = {'x': 1}                                                                                    
 19 d['y'] = 2                                                                                      
 20 d['z'] = 'x'                                                                                    
 21 d[3] = 4  

mypy でチェックします。

(mypy)$ mypy check_type_inference.py 
check_type_inference.py, line 8: Incompatible types in assignment (expression has type "str", variable has type "int")
check_type_inference.py, line 13: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
check_type_inference.py, line 15: List item 2 has incompatible type "str"
check_type_inference.py, line 20: Incompatible types in assignment
check_type_inference.py, line 21: Invalid index type "int" for "dict"

変数へ最初に代入した型とあわない値を代入するとエラーとして指摘してくれます。

また型アノテーションをコメントで定義することもできます。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import List                                                          
  3                                                                                  
  4 l1 = []  # type: List[int]                                                        
  5 l1.append('x')                                                                    
  6                                                                                  
  7 l2 = List[int]()                                                                  
  8 l2.append('y')                               

# type: で始まるコメントを型アノテーションとして扱うため、

(mypy)$ mypy check_type_annotations.py 
check_type_annotations.py, line 5: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
check_type_annotations.py, line 8: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"

どちらの定義方法でも mypy では同様にエラーを指摘します。

最後に値の型を明示的に宣言する方法です。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Undefined                                                     
  3                                                                                  
  4 s = Undefined(str)                                                               
  5 if s:                                                                            
  6     print('not defined yet')                                                     
  7 s = 1                              

mypy でチェックすると、str 型の何かを期待しています。

(mypy)$ mypy check_explicit_types.py 
check_explicit_types.py, line 7: Incompatible types in assignment (expression has type "int", variable has type "str")

この Undefined はもう1つ仕掛けがあります。s に str 型の値が代入される前に操作を行うと例外が発生します。

(mypy)$ python check_explicit_types.py 
Traceback (most recent call last):
  File "check_explicit_types.py", line 5, in <module>
    if s:
  File "path/to/.virtualenvs/mypy/lib/python3.4/site-packages/typing.py", line 359, in __bool__
    raise TypeError("'Undefined' object is not valid as a boolean")
TypeError: 'Undefined' object is not valid as a boolean

ちょっと実装をみてみると、

class Undefined:
    def __repr__(self):
        return '<typing.Undefined>'

    def __setattr__(self, attr, value):
        raise AttributeError("'Undefined' object has no attribute '%s'" % attr)

    def __eq__(self, other):
        raise TypeError("'Undefined' object cannot be compared")

    def __call__(self, type):
        return self

    def __bool__(self):
        raise TypeError("'Undefined' object is not valid as a boolean")

Undefined = Undefined()

Undefined に対する操作 (特殊メソッド) を行うと例外を発生させています。if 文の場合は Python3 からは特種メソッド__bool__ に変更されています。Python2 のときは __nonzero__ でした。

静的型付けプログラミング

チュートリアルで静的型チェッカーとしての mypy の雰囲気は掴めました?ここではさらに mypy が提供する強力な型アノテーションをみていきましょう。

様々な型

ユーザー定義型
  1 # -*- coding: utf-8 -*-                                                          
  2 import typing                                                                    
  3                                                                                  
  4 class A:                                                                         
  5     def f(self) -> int:  # Type of self inferred (A)                             
  6         return 2                                                                 
  7                                                                                  
  8 class B(A):                                                                      
  9     def f(self) -> int:                                                          
 10          return 3                                                                
 11                                                                                  
 12     def g(self) -> int:                                                          
 13         return 4                                                                 
 14                                                                                  
 15 b = B()       # OK infer type B for b                                            
 16 print(b.g())                                                                     
 17                                                                                  
 18 a = B()       # type: A  # OK (explicit type for a; override type inference)     
 19 print(a.f())  # 3                                                                
 20 print(a.g())  # Type check error: A has no method g                             

18行目で型アノテーションで明示的に A と指定したら g メソッドがないと指摘されます。

(mypy)$ mypy check_user_defined_types.py 
check_user_defined_types.py, line 20: "A" has no attribute "g"

ちなみにこの型アノテーションの隣の実コメント部の # を取り除くと、

 18 a = B()       # type: A  OK (explicit type for a; override type inference)

パースエラーになりました。

(mypy)$ mypy check_user_defined_types.py 
check_user_defined_types.py, line 18: Parse error before "OK"

アノテーションとコメントは分割して書かないといけないようです。

任意の型を表す Any とキャスト

wikipedia:動的型付け したい値を定義するときは Any を使います。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Any, Undefined, cast                                                         
  3                                                                                                 
  4 x = Undefined(Any)                                                                              
  5 x = 1                                                                                           
  6 x = 'x'

このとき、普通の Python のコードのように mypy では静的チェックをしなくなります。

(mypy)$ mypy check_any_type.py 

さらに Any で定義した値をキャストすることもできます。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Any, Undefined, cast                                                         
  3                                                                                                 
  4 x = Undefined(Any)                                                                              
  5 x = 1                                                                                           
  6 x = 'x'                                                                                         
  7                                                                                                 
  8 y = cast(int, x)  # cast x as int to y                                                          
  9 y = 'y'                                                                                         
 10 y = 2                                                                            
 11                                                                                  
 12 z = Any(y)  # cast y as Any to z                                                 
 13 z = 3                                                                            
 14 z = 'z'                                           

今度は x を int でキャストしたものを y として定義し、y をまた Any にキャストして z として定義しています。

(mypy)$ mypy check_any_type.py 
check_any_type.py, line 9: Incompatible types in assignment (expression has type "str", variable has type "int")

Any(...)cast(Any, ...) は等価なようです。

Callable 型

組み込み型の Function を使います。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Function, List                                                               
  3                                                                                                 
  4 def label_data(name: str, data: List[int]) -> str:                                              
  5     return '{}: {}'.format(name, ', '.join(str(i) for i in data))                               
  6                                                                                                 
  7 def caller(name: str, data: List[int], func: Function[[str, List[int]], str]) -> str:           
  8     return func(name, data)                                                                     
  9                                                                                                 
 10 print(caller('numbers', [1, 2, 3], label_data))                                                 
 11 print(caller('mixed', [1, 'x', 3], label_data))                                        

構文は Function[[引数1の型, ..., 引数nの型], 返り値の型] なので複雑な関数だとちょっと苦しい感じはあります。

(mypy)$ mypy check_callable_types.py 
check_callable_types.py, line 11: List item 2 has incompatible type "str"

lambda もコンテキストから双方向に型推論されます。

  1 # -*- coding: utf-8 -*-                                                                         
  2 from typing import Iterable, Undefined                                                          
  3                                                                                                 
  4 l1 = Undefined(Iterable[int])                                                                   
  5 l1 = map(lambda x: x + 1, [1, 2, 3])  # infer x as int and l as List[int]                       
  6                                                                                                 
  7 l2 = Undefined(Iterable[int])                                                                   
  8 l2 = map(lambda x: x + '+test', ['a', 'b'])                                                     
  9                                                                                                 
 10 l3 = Undefined(Iterable[int])                                                                   
 11 l3 = map(lambda x: str(x) + '+test', [1, 2])                                

あくまで例なので実際には lambda のような用途にわざわざ Undefined を定義するとは思いませんが、こういったコンテキストから凡ミスを防ぐのには良さそうにみえます。

(mypy)$ mypy check_lambda.py 
check_lambda.py, line 8: Incompatible return value type: expected builtins.int, got builtins.str
check_lambda.py, line 8: Argument 1 to "map" has incompatible type Function[[str], str]; expected Function[[str], int]
check_lambda.py, line 11: Incompatible return value type: expected builtins.int, got builtins.str
check_lambda.py, line 11: Argument 1 to "map" has incompatible type Function[[int], str]; expected Function[[int], int]

ダックタイピング

wikipedia:ダック・タイピング のコード例です。

  1 # -*- coding: utf-8 -*-                                                                         
  2 def quack(a: A) -> None:                                                                      
  3     print(a.f())                                                                                
  4                                                                                                 
  5 class A:                                                                                        
  6     def f(self) -> str:                                                                         
  7         return 'x'                                                                              
  8                                                                                                 
  9 quack(A())                                                                                      
 10                                                                                                 
 11 class B:                                                                                        
 12     def f(self) -> int:                                                                         
 13         return 1                                                                 
 14                                                                                  
 15 quack(B())                   

B は A と継承関係にないため、15行目の quack(B()) でエラーになります。

(mypy)$ mypy check_duck_typing.py 
check_duck_typing.py, line 15: Argument 1 to "quack" has incompatible type "B"; expected "A

ducktype というクラスデコレーターを使うことで回避できます。

 11 from typing import ducktype                                                                     
 12 @ducktype(A)                                                                                    
 13 class B:                                                                                        
 14     def f(self) -> int:                                                                         
 15         return 1                                                                 
 16                                                                                  
 17 quack(B())                   

さらに鋭い方は気付いたかもしれませんが、このコードは Python で実行できません。

(mypy)$ python check_duck_typing.py 
Traceback (most recent call last):
  File "check_duck_typing.py", line 2, in <module>
    def quack(a: A) -> None:
NameError: name 'A' is not defined

Python は前方参照 (forward reference) をサポートしていません。
関数アノテーションで指定したクラス名が解決できなくてエラーになります。mypy は前方参照をサポートしているのでこのままでも実行できますが、それでは実用的に意味がないので文字列リテラルでクラス名を指定することもできます。

  1 # -*- coding: utf-8 -*-                                                                         
  2 def quack(a: 'A') -> None:                                                                      
  3     print(a.f())            

ドキュメントには記載されていませんが、あるスライドに Go 言語風のインターフェースを提供する Protocol というのがあるのを知りました。

  1 # -*- coding: utf-8 -*-                                                          
  2 from abc import abstractmethod                                                   
  3 from typing import Protocol                                                      
  4                                                                                  
  5 class Sized(Protocol):                                                           
  6     @abstractmethod                                                              
  7     def __len__(self) -> int:                                                    
  8         pass                                                                     
  9                                                                                  
 10 def not_empty(x: Sized) -> bool:                                                 
 11     return len(x) > 0                

試しにコードを書いて実行してみたらエラーになりました。

(mypy)$ mypy check_protocols.py 
check_protocols.py, line 3: Module has no attribute 'Protocol'
check_protocols.py, line 5: Name 'Protocol' is not defined
(mypy)$ python check_protocols.py 

Feature proposal: Golang style interfaces · Issue #497 · JukkaL/mypy · GitHub によると、この仕組みはまだ開発中のようです。

class AbstractGenericMeta(ABCMeta):
    ...

class Protocol(metaclass=AbstractGenericMeta):
    """Base class for protocol classes."""

実装をみれば分かるように abc を使った抽象化で実現しています。abc をあまり使ったことがないので私にはまだピンときていませんが、まだまだ議論の余地はありそうです。

共用体 直和型 (Union Types) *1

wikipedia:共用体 と聞くと C 言語を思い浮かべますが 、汎用関数ではよくある処理です。Unionコンストラクタに受け入れられる型を指定します。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Union                                                         
  3                                                                                  
  4 def f(x: Union[int, str]) -> None:                                               
  5     x + 1     # Error: str + int is not valid                                    
  6     if isinstance(x, int):                                                       
  7         # Here type of x is int.                                                 
  8         x + 1      # OK                                                          
  9     else:                                                                        
 10         # Here type of x is str.                                                 
 11         x + 'a'    # OK                                                          
 12                                                                                  
 13 f(1)    # OK                                                                     
 14 f('x')  # OK                                                                     
 15 f(1.1)  # Error                               

型に特化した処理は isinstance で分岐することで mypy の型チェックでエラーになりません。

(mypy)$ mypy check_union_types.py 
check_union_types.py: In function "f":
check_union_types.py, line 5: Unsupported operand types for + ("Union[int, str]" and "int")
check_union_types.py: At top level:
check_union_types.py, line 15: Argument 1 to "f" has incompatible type "float"; expected "Union[int, str]"

Union がなかったらどうしたら良いんだろう?と思ってちょっと試してみました。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Any, cast                                                     
  3                                                                                  
  4 class MyModel:                                                                   
  5     def __init__(self, value: Any) -> None:                                      
  6         self.value = value                                                       
  7                                                                                  
  8 class MyInt(MyModel):                                                            
  9     def __add__(self, other: int) -> int:                                        
 10         return self.value + other                                                
 11                                                                                  
 12 class MyStr(MyModel):                                                            
 13     def __add__(self, other: str) -> str:                                        
 14         return self.value + other                                                
 15                                                                                  
 16 def g(y: MyModel) -> None:                                                       
 17     cast(MyInt, y) + 1                                                           
 18     cast(MyStr, y) + 'a'                                                         
 19     y + 1.1                                                                      
 20                                                                                  
 21 def h(z: Any) -> None:                                                           
 22     cast(int, z) + 1                                                             
 23     z + 'a'
 24     z + 1.1

明示的にキャストするか、Any を使う方法しか思いつかなかったのですが、Any を使うとそもそも静的解析できないのでダメですね。

(mypy)$ mypy check_union_types2.py 
check_union_types2.py: In function "g":
check_union_types2.py, line 19: Unsupported operand types for + ("MyModel" and "float")

余談ですが、汎用関数のような処理は 3.4 から標準に入った singledispatch を使うとすっきり書けます。

  1 # -*- coding: utf-8 -*-                                                          
  2 from functools import singledispatch                                             
  3 from typing import Any                                                           
  4                                                                                  
  5 @singledispatch                                                                  
  6 def f(x: Any) -> Any:                                                           
  7     return x                                                                     
  8                                                                                  
  9 @f.register(int)                                                                 
 10 def _(x: int) -> int:                                                            
 11     return x + 1                                                                 
 12                                                                                  
 13 @f.register(str)                                                                 
 14 def _(x: str) -> str:                                                            
 15     return x + 'a'                                                               
 16                                                                                  
 17 print(f(1))                                                                      
 18 print(f('x'))                                                                    
 19 print(f(1.1))

実行してみます。

(mypy)$ python check_singledispatch.py 
2
xa
1.1

試しに mypy でチェックしてみるとエラーになってしまいました。

(mypy)$ mypy check_singledispatch.py 
check_singledispatch.py, line 2: Module has no attribute 'singledispatch'
check_singledispatch.py, line 5: Name 'singledispatch' is not defined
check_singledispatch.py, line 9: 'overload' decorator expected
check_singledispatch.py, line 13: 'overload' decorator expected

functools のスタブに singledispatch の定義がなかったので以下の定義を追加してスタブディレクトリを環境変数にセットしたら2行目と5行目のエラーは消えました。

from typing import Any

def singledispatch(func: Any) -> Any: pass
(mypy)$ vi stub/functools.py 
(mypy)$ export MYPYPATH=./stub/
(mypy)$ mypy check_singledispatch.py
check_singledispatch.py, line 9: 'overload' decorator expected
check_singledispatch.py, line 13: 'overload' decorator expected

型システムの拡張

かなり満足してきましたが、もうちょっと続きがあります。

今夏にメーリングリストで Guido が 3.5 に取り込もうと提案したのは mypy の型アノテーションの仕組みのみだったように思いますが、mypy には関数オーバーロードジェネリクスといった Python の型システムを拡張する機能も提供しています。ただ、先日投稿された PEP のドラフトには総称型にも言及しているため、さらに突っ込んだ仕組みになるのかもしれません。

関数オーバーロード

オーバーロードって日本語に訳すと wikipedia:多重定義 になるのですね。私の周りではオーバーロードと呼ぶ方が一般的です。

組み込み関数の absオーバーロードしてみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import overload                                                      
  3                                                                                  
  4 @overload                                                                        
  5 def abs(n: int) -> int:                                                          
  6     print('called int version')                                                  
  7     from builtins import abs                                                     
  8     return abs(n)                                                                
  9                                                                                  
 10 @overload                                                                        
 11 def abs(n: float) -> float:                                                      
 12     print('called float version')                                                
 13     return n if n >= 0.0 else -n                                                 
 14                                                                                  
 15 @overload                                                                        
 16 def abs(s: str) -> str:                                                          
 17     print('called str version')                                                  
 18     return s[1:] if s[0] == '-' else s                                           
 19                                                                                  
 20 print(abs(-2))    # int                                                          
 21 print(abs(-1.5))  # float                                                        
 22 print(abs('-3'))  # str

実行してみます。

(mypy)$ mypy check_function_overloading.py 

(mypy)$ python check_function_overloading.py 
called int version
2
called float version
1.5
called str version
3

オリジナルの組み込み関数を呼び出すには builtins からインポートする必要があります。これは関数を探すときにローカルの名前空間から見つけてしまうのと同様です。

>>> type(abs)
<class 'builtin_function_or_method'>
>>> def abs(): pass
... 
>>> type(abs)
<class 'function'>

前節では singledispatchオーバーロードを実現しましたが、mypy の overload は、もうちょと汎用的な multiple dispatch だとドキュメントにあります。

This enables also a form of multiple dispatch.

Function overloading

実際に試してみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import overload                                                      
  3                                                                                  
  4 @overload                                                                        
  5 def func(n: int, s: str) -> str:                                                 
  6     return 'int, str -> str'                                                     
  7                                                                                  
  8 @overload                                                                        
  9 def func(s: str, n: int) -> str:                                                 
 10     print('called str, int -> str')                                              
 11     return func(n, s)                                                            
 12                                                                                  
 13 print(func(1, 's'))                                                              
 14 print(func('s', 1))                  
(mypy)$ python check_multiple_dipatch.py 
int, str -> str
called str, int -> str
int, str -> str

Python の標準ライブラリにはその名の表す通り singledispatch しか導入されなかったわけですが、multiple dispatch の議論もまた活発になるかもしれません。

ジェネリクス

Python のような言語で必要かという疑問はありますが、インターフェイスを明示したいときなどで役に立つのかもしれません。 当初、この例だけをあまりピンと来なかったんですが、コレクションを扱う API を静的解析しようと思ったらジェネリクスがないと不便というのが Javaジェネリクス導入の経緯からも分かることでした (´・ω・`)

ジェネリッククラス (generic class)
  1 # -*- coding: utf-8 -*-
  2 from typing import Generic, List, typevar                                        
  3                                                                                  
  4 T = typevar('T')                                                                 
  5                                                                                  
  6 class Stack(Generic[T]):                                                         
  7     def __init__(self) -> None:                                                  
  8         self.items = List[T]()  # Create an empty list with items of type T      
  9                                                                                  
 10     def push(self, item: T) -> None:                                             
 11         self.items.append(item)                                                  
 12                                                                                  
 13     def pop(self) -> T:                                                          
 14         return self.items.pop()                                                  
 15                                                                                  
 16     def empty(self) -> bool:                                                     
 17         return not self.items                                                    
 18                                                                                  
 19 stack_int = Stack[int]()                                                         
 20 stack_int.push(1)                                                                
 21 stack_int.push(2)                                                                
 22 stack_int.pop()                                                                  
 23 stack_int.push(3)                                                                
 24 stack_int.push('x')                                                              
 25 print(stack_int.items)                                                           
 26                                                                                  
 27 stack_str = Stack[str]()                                                         
 28 stack_str.push(1)                                                                
 29 stack_str.push('x')                                                              
 30 stack_str.push('y')                                                              
 31 print(stack_str.items)

型パラメーターを使うことで型を明示できた、やったー!と思うか、もともと動的型付けなのでこんなことしなくてもそのままコードが書けるのに、、、と思うか。

(mypy)$ mypy check_generic_classes.py 
check_generics.py, line 24: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int"
check_generics.py, line 28: Argument 1 to "push" of "Stack" has incompatible type "int"; expected "str"

実行結果。

(mypy)$ python check_generic_classes.py 
[1, 3, 'x']
[1, 'x', 'y']
ジェネリック関数 (generic function)

同様に関数にも応用できます。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Sequence, typevar                                             
  3                                                                                  
  4 T = typevar('T')                                                                 
  5                                                                                  
  6 def first(seq: Sequence[T]) -> T:                                                
  7     return seq[0]                                                                
  8                                                                                  
  9 print(first([1, 2, 3]))                                                          
 10 print(first('abc'))                                                              
型変数と制限

型変数の応用例です。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import typevar                                                       
  3                                                                                  
  4 AnyStr = typevar('AnyStr', values=(str, bytes))                                  
  5                                                                                  
  6 def concat(x: AnyStr, y: AnyStr) -> AnyStr:                                      
  7     return x + y                                                                 
  8                                                                                  
  9 concat('a', 'b')    # Okay                                                       
 10 concat(b'a', b'b')  # Okay                                                       
 11 concat('a', b'b')   # Error!                                                     
 12 concat(1, 2)        # Error!                   

型チェックをすると11行目と12行目がエラーになります。11行目は str と bytes を連結することはできないからです。

(mypy)$ mypy check_generic_type_variables1.py 
check_generic_type_variables1.py, line 11: Type argument 1 of "concat" has incompatible value "object"
check_generic_type_variables1.py, line 12: Type argument 1 of "concat" has incompatible value "int"

これは一見 Union で代替できそうにみえますが、違うものだとドキュメントにあります。やってみましょう。

  1 # -*- coding: utf-8 -*-                                                          
  2 from typing import Union                                                         
  3                                                                                  
  4 def concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]:     
  5     return x + y  # Error: can't concatenate str and bytes                       
  6                                                                                  
  7 concat('a', 'b')    # Okay                                                       
  8 concat(b'a', b'b')  # Okay                                                       
  9 concat('a', b'b')   # mypy passes this line, but error at runtime                
 10 concat(1, 2)        # Error! 

5行目で Union だとサポートされてないといったエラーが出ますが、9行目のコードはパスしてしまいますね。

(mypy)$ mypy  check_generic_type_variables2.py 
check_generic_type_variables2.py: In function "concat":
check_generic_type_variables2.py, line 5: Unsupported operand types for + (likely involving Union)
check_generic_type_variables2.py: At top level:
check_generic_type_variables2.py, line 10: Argument 1 to "concat" has incompatible type "int"; expected "Union[str, bytes]"
check_generic_type_variables2.py, line 10: Argument 2 to "concat" has incompatible type "int"; expected "Union[str, bytes]"

このコードの意図することは、以下のように関数オーバーロードで定義するのと等価ですが、

@overload
def concat(x: str, y: str) -> str:
    return x + y

@overload
def concat(x: bytes, y: bytes) -> bytes:
    return x + y

ジェネリクスの型変数を使うとより短く書けるという利点があります。
なるほど、これはしっくりきました。

まとめ

全てではないですが、mypy の用途や機能のほとんどは俯瞰できたのではないかと思います。おもしろそうだと思ったら是非プロジェクトのサイトも参照してください。

いまの時点では、型アノテーションを指定しても実行時の最適化 (高速化) にはなりません。

Mypy and PyPy are orthogonal. Mypy does static type checking, i.e. it is basically a linter, but static typing has no runtime effect, whereas the PyPy is an Python implementation. You can use PyPy to run mypy programs.

How is mypy different from PyPy?

意訳すると、

mypy と PyPy は直交する。mypy は、基本的には linter のような静的型付けチェックを行うが、実行時には何の影響も及ぼさない。対して、PyPy は Python 実装の1つであり、mypy のプログラムを実行するのに使う。

PyPy で良いことがあるんじゃないかと妄想しますが、PyPy のコア開発者である Alex Gaynor は、型アノテーションが PyPy にとって価値がないと断言しています。彼は型アノテーションの導入よりも、Python の型システムを改善しようと提案していますが。

PS: You're right. None of this would provide *any* value for PyPy.

[Python-ideas] Proposal: Use mypy syntax for function annotations

静的型付けプログラミングっぽいことを Python でやることの是非は分かりません。Python じゃなくて、最初から静的型付き言語を使えば良いじゃないかというのも正しいでしょう。とはいえ、関数アノテーションが mypy を導いたように、型アノテーションが次のなにか (漸進的型付け?) を導く可能性もあります。何よりも動的型付き言語で実行前にエラーチェックできる範囲が広がることは、Python というプログラミング言語が好きな私にとってはとても嬉しいニュースでした。

2014-12-26 追記

実際に小さいコードで試してみました。
python3 - mypy で適当なスニペットを実際に静的型付けしてみた - Qiita

*1:共用体は C 言語で普及した訳語で型システムでは直和型という訳語の方が一般的な表現のようです

広告を非表示にする