2020年5月24日日曜日

2: Pythonにおいて静的タイプチェックを行なうための最小十分テクニック

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>


動機


mypyオフィシャルドキュメントは、入門時に読み通すためによりは、必要に応じて参照するために有用でしょう。私が見つけたすべてのイントロダクションは、実用的に十分な使用法ガイドというよりは、使い始めることの奨励です。

話題


About: Pythonプログラミング言語
About: 静的タイプチェック

この記事の目次


開始コンテキスト


  • 読者は、Pythonの基本的知識を持っている。

ターゲットコンテキスト



  • 読者は、Pythonにおいて静的タイプチェックを行なうための最小(多分)十分(多分)テクニックをマスターする。

オリエンテーション


Hypothesizer 7
今では、Pythonでは、静的タイプチェックを行なうことができるようだ。

それは「Pythonic」でないと仰るのですか?

えーと、それが「Pythonic」でないことが...私にとって何だというのでしょうか?自分のコードがよりクリーン・より保守容易になり、自分のデバッギングがより罰のよう(罰のようであるデバッギングの私にとっての例は、静的タイプチェックであれば一網打尽で捕まえたであろうバグ群を手動で('手動'はテストコードを書かなければならないことを含む)、見つけなければならないことだ)でなくなれば、それが「Pythonic」であろうが、何icであろうが、私はとても喜んで辛抱するだろう。

それが、伝統的なPythonプログラマーたちが何を予期しているかという問題だ、とは知っているが、アンリーズナブルな伝統には決して従わないと、私は、宣言する。

勿論、私は、「ダックタイピング」を激烈に弁護できるが、前記事にてたっぷりと行なったことをここで繰り返すのは控えよう。

「最小十分」という表現で私が何を意味しているかをいぶかしんでいますか?...ごもっともです: 私にとって最小十分なものは、あなたにとって最小でも十分でもないかもしれません。...えーと、私が意図するのは、私の意見において普通のプログラマーにとって最小十分なものということですが、私のような変な奴の観念の普通性は、勿論、疑わしいものです。したがって、私が保証できるのは、自らの善意だけです。

もっと具体的にいうと、私の認識では、一方では、'mypy'のオフィシャルドキュメントは、全体を読み通すには、'今のところ私は全然興味ない'情報を多くをオファーしすぎだと思えるし(そのドキュメントを責めているのでは全然ない: あるテクニカルドキュメントに対するとても正当な計画は、包括的な情報をオファーすることだ)、他方では、私がインターネット上で見つけた限りの'mypy'の入門の全ては、一部の'これらは明らかに必要'情報オファーしていない(それらの入門を責めているのでは全然ない: 多分、それらは、実用的な使用ガイドとしては意図されておらず、ただPythonにおける静的タイプチェックの存在に注意を喚起しているだけなのだろう)。

例えば、ほとんどの入門は、一部のビルトインタイプ群を使用することにふれているだけだが、実際には、ビルトインタイプ群は、私の主要関心事ではない。...実のところ、あるファンクション引数が文字列であることが知られているならば、その引数については、私はそれ以上あまり心配しないだろう、なぜなら、'str'タイプの詳細はよく知られた事実だからだ。...他方で、あるファンクション引数が「dodo」だとされているのであれば、私はとても心配するだろう、「一体全体、「dodo」って何だ?!」といぶかって。...それはドードーのように歩きかつ鳴くものだ、と仰るのですか?...でも、どのようにドードーが歩きかつ鳴くか、またはあくびするか等をが一体、私がいかにして知っていると期待されているのでしょうか、サー?...したがって、ユーザー定義タイプ群を使用することにふれていない入門は、実用的な使用ガイドとしては通用しない、私にとっては。

別の例として、静的タイプチェックを組み込むと、ほとんど不可避に、タイプキャストが必要となる: もしも、静的に型付けされたプログラミング言語でタイプキャストが許されていなかったら、プログラミングは、受け入れがたく制限的になってしまうだろう。...したがって、タイプキャストにふれていない入門は、実用的な使用ガイドしては通用しない、私にとっては。

また、私は、'ジェネリクス'も必要なものの中に含む ジェネリクス前時代には私は戻りたくない。

加えて、ほとんどのケースで必要ないくつかのトリックがある。

このように、Pythonにおいて静的タイプチェックを行なうための簡潔であるが実用的な使用ガイドを、既存では、未だ私は見つけておらず、そして...思うには、私が自分で作成するべきなのだろう、他の方が何かをやってくれないと泣き言を言う代わりに。


本体


1: いくつかの注釈


Hypothesizer 7
私は'mypy'を使用する。

実際には、それは、唯一のオプションというわけではないが、最も流布しているものらしい。多数派に盲目的に従ったりしないことを私は誇りとしているので、よりよいものがあれば、喜んで、別のオプションに行くが、私の注意を最初に引いたものを試してみない理由がなかった。

私はPython 3.6を使用する。

理由は、1) 古いPython 2を使用するモチベーションが私には全然なく、2) 私が使うLubuntu 18.04用のレポジトリ内のPython 3のバージョンが3.6であること。

Pythonにおける静的タイプチェックは、それなりに新しい機能なので、そのPythonバージョンは、本記事のコンテンツにとって重要だ。


2: 任意の変数または任意のファンクションリターンにアノテーションを付ける


Hypothesizer 7
任意の変数にアノテーションを付ける際は、以下のようにする。

@Python ソースコード
from collections import OrderedDict
from typing import Callable
from typing import Collection
from typing import Container
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Sized
from typing import Tuple

# The built-in non-container types Start
l_integer: int = 1
l_float: float = 1.1
l_bool: bool = True
l_string: str = "ABC"
l_bytes: bytes = b"ABC"
l_classAClass: type = ClassAA # 'ClassAA' is a class.
# The built-in non-container types End
# The built-in container types Start
l_list: List [int] = [1, 2]
l_set: Set [float] = {1.1, 2.2}
l_dictionary: Dict [bool, str] = {True: "ABC", False: "DEF"}
l_elementsOrderPreservedDictionary: "OrderedDict [bytes, int]" = OrderedDict ({b"DEF": 2, b"ABC": 1})
l_tuple: Tuple [float, ...] = (1.1, 2.2, 3.3, )
# The built-in container types End
# The user-defined class instance types
l_classA: "ClassA" = ClassA ("ABC") # 'ClassA' is a class with the one-string-argument constructor
# Making it optional
l_optionalInteger: Optional [int] = None
# The function types
l_function: Callable [ [int, int], Tuple [int, int]] = divmod
# Some implicit-interface types Start
l_iterable: Iterable [int] = [1, 2]
l_iterator: Iterator [int] = iter ([1, 2])
l_sized: Sized = [1, 2]
l_container: Container [int] = [1, 2]
l_collection: Collection [int] = [1, 2]
# Refer to 'https://mypy.readthedocs.io/en/stable/protocols.html' for the details of each implicit-interface type
# Refer to 'https://mypy.readthedocs.io/en/stable/protocols.html' for the other predefined so-called "protocol" types (I call them 'implicit-interface types').
# Some implicit-interface types End

注意すべきは、一部のタイプ("ClassA"および"OrderedDict [bytes, int]")はダブルクウォーテイションで囲まれているということだ。実際は、いかなるタイプもダブルクウォーテイションで囲むことができる。実は、あるユーザー定義クラスタイプが、それが完全に定義された後に使用されたら(そのクラス定義内でなければ)、それは、ダブルクウォーテイションで囲む必要はない。私は、表現が一貫していることを好むので、どのユーザー定義クラスもどの場所でもダブルクウォーテイションで囲むことをルールにする。

上記記法は、いかなる変数にも使用できる、それが、クラスインスタンス変数であろうが、クラス変数であろうが、ローカル変数であろうが、ファンクション引数であろうが、その他の変数であろうが。

クラスインスタンスメソッドやクラスメソッドの第1引数について、'mypy'のマニュアルや私が読んだ他の入門は、それにアノテーションを付けないことを推奨しているが、私はその方法に不満を持っている: スタブ(以降で説明される)ジェネレーターである'stubgen'は、そのような引数に'Any'というアノテーションを付ける。

'Any'は、フルネームで'typing.Any'であり、その変数またはファンクションリターンをからめるどんなオペレーションも静的タイプチェックから除外されることを意味するが、そのような扱いは、私はいかなる場合にも望まない。...実のところ、その名前は誤解を招くものだ: その名前は、そのタイプの要点は、'任意のデータを取る'ことにあるかのように思わせるが、それは主旨ではない、まあ、実際、そのタイプは任意のデータを取りはするが。...ある変数またはファンクションリターンに任意の値を取らせることが私の意図なのであれば、私は、代わりに、'object'を使用するべきだ。

スタブジェネレーターである'stubgen'は、アノテーションが付けられていない全ての引数に勝手に'Any'というアノテーションを付けてしまうが、そのくせ、'typing.Any'をインポートする労を取ってくれないので、そのスタブを使用するとエラーが起きる('from typing import Any'を私が明示的に追加しなければ)。...それに、それらの引数に'Any'というアノテーションを付けてしまうと、いわゆる"self"引数に任意のデータが渡せることになってしまう、いわゆる「アンバウンドメソッド」を直接呼ぶ(いわゆる「バウンドメソッド」を介するのではなく)ことによって。

したがって、任意のクラスインスタンスメソッドや任意のクラスメソッドの第1引数に、私はあえて、以下のようにアノテーションを付けることにする。

@Python ソースコード
from typing import Type
from typing import TypeVar

l_classBoundByClassB = TypeVar ("l_classBoundByClassB", bound="ClassB")

class ClassB:
 def methodA (a_this: "ClassB", a_string: str) -> str:
  return a_string
 @classmethod
 def methodB (a_class: Type [l_classBoundByClassB], a_string: str) -> str:
  return a_string

注目すべきは、'ClassB'の任意のサブクラスインスタンスが'a_this'に渡される可能性があるが、それは問題ない、なぜなら、'"ClassB"'という指定は、'ClassB'の任意のサブクラスインスタンスを受け取れるから、ということだ。

ファンクションリターンにアノテーションを付ける場合は、えーと、実は、それは、既に、上記の例に示されている。


3: 'typing.TypeVar'および'typing.Type'とは何で、それらをどのように使うか


Hypothesizer 7
前セクションで、'typing.TypeVar'および'typing.Type'を使用した。それらは、何なのか、正確には?

えーと、'typing.TypeVar'は、クラスであって、そのインスタンスは、タイプアノテーション内で使用され、'mypy'タイプチェック毎に1つのタイプを表わす。...不可解ですか?上記意味を段階を踏んで明らかにしよう。

「そのインスタンスは、タイプアノテーション内で使用され」という部分については、'typing.TypeVar'インスタンスが生成された後、それは、タイプアノテーションの外で使用するようには意図されていないということ。誤ったコードの例を見てみよう。

@Python ソースコード
from typing import TypeVar

T = TypeVar ("T")

def functionA (a_type: TypeVar) -> None: # A wrong usage
 None

functionA (T) # A wrong usage

'typing.TypeVar'クラス自体(そのインスタンスではなく)は、タイプアノテーション内のタイプとして使用できない。確かに、「TypeVar」というそのタイプアノテーションを'object'に変更すれば、'T'というインスタンスを'functionA'というファンクションに渡すことはできるが、そのインスタンスがタイプアノテーションの外でどのように役に立つか、私には、全く思いつかない。

正しい(しかし、全然役に立たない)例を見てみよう。

@Python ソースコード
from typing import TypeVar

T = TypeVar ("T")

def functionB (a_type: T) -> object:
 return a_type

l_object1: object = functionB ("ABC") # The 1st 'functionB' call checking is done on this line.
l_object2: object = functionB (1)     # The 2nd 'functionB' call checking is done on this line.

注目すべきは、'T'というインスタンスがタイプアノテーション内で使用されているということだ。

'functionB'が2度呼ばれていて、各呼び出しが'mypy'によってチェックされる。実は、第1の'functionB'呼び出しチェックでは、'T'は'str'を表わし、第2の'functionB'呼び出しチェックでは、'T'は'int'を表わすが、それが、「インスタンスは、'mypy'タイプチェック毎に1つのタイプを表わす」という表現で私が意味していることだ。

えーと、それがどのように役に立つのか?...実際には、上記の例は、全然役に立っていない、なぜなら、なぜ私はそのタイプアノテーションに、'T'の代わりに単に'object'を使用しないのだろうか?

役に立っている例を見てみよう。

@Python ソースコード
from typing import TypeVar

T = TypeVar ("T")

def functionC (a_type: T) -> T:
 return a_type

l_string: str = functionC ("ABC") # The 1st 'functionC' call checking is done on this line.
l_integer: int = functionC (1)    # The 2nd 'functionC' call checking is done on this line.

その例で役に立っている理由は、'T'が、引数のタイプとリターンのタイプを連動させるために使用されていることだ: ファンクションに'str'インスタンスを渡せば、リターンタイプが'str'になる、例えば。

'TypeVar'インスタンスが取ることのできるタイプは、以下のようにして制限することができる。

@Python ソースコード
from typing import TypeVar

T = TypeVar ("T", bound="ClassA")     # can represent only 'ClassA' and its any descendant
U = TypeVar ("U", "ClassA", "ClassB") # can represent only 'ClassA' or 'ClassB'

それでは、'typing.Type'とは何なのか?'typing.Type'は、タイプアノテーション内で使用できるファンクションで、引数のタイプをリターンするものだ。

1つの例を見てみよう。

@Python ソースコード
from typing import Type
from typing import TypeVar

class ClassC:
 def __init__ (a_this: "ClassC", a_string: str) -> None:
  a_this.i_string: str
  
  a_this.i_string = a_string
 
 def methodA (a_this: "ClassC") -> None:
  print ("From ClassC " + a_this.i_string)

class ClassCA (ClassC):
 def __init__ (a_this: "ClassCA", a_string: str) -> None:
  ClassC.__init__ (a_this, a_string)
 
 def methodA (a_this: "ClassCA") -> None:
  print ("From ClassCA " + a_this.i_string)

T = TypeVar ("T", bound="ClassC")

def functionD (a_type: Type [T], a_string: str) -> T:
 return a_type (a_string)

l_classC: ClassC = functionD (ClassC, "ABC")
l_classCA: ClassCA = functionD (ClassCA, "ABC")

なぜ、'a_type'のタイプが'Type [T]'であって、'T'でないのかは理解できる: もしも、それが'T'だったとしたら、その引数は、'ClassC'や'ClassCA'のインスタンスを取ることになってしまうだろう、'ClassC'や'ClassCA'自体ではなく。


4: タイプキャスト


Hypothesizer 7
静的タイプチェックを組み込む限り、タイプキャストがほとんど不可避になる。

1つの例を見てみよう。

@Python ソースコード
from typing import Dict

class ClassD:
 def __init__ (a_this: "ClassD", a_name: str) -> None:
  a_this.i_name: str
  
  a_this.i_name = a_name
 
 def methodA (a_this: "ClassD") -> str:
  return "From ClassD " +  a_this.i_name

class ClassDA (ClassD):
 def __init__ (a_this: "ClassDA", a_name: str) -> None:
  ClassD.__init__ (a_this, a_name)
 
 def methodAA (a_this: "ClassDA") -> str:
  return "From ClassDA " +  a_this.i_name

class ClassDB (ClassD):
 def __init__ (a_this: "ClassDB", a_name: str) -> None:
  ClassD.__init__ (a_this, a_name)
 
 def methodAB (a_this: "ClassDB") -> str:
  return "From ClassDB " +  a_this.i_name

l_dictionary1: Dict [str, "ClassD"] = {"Key1": ClassDA ("Name1"), "Key2": ClassDB ("Name2"), "Key3": ClassDB ("Name3"), "Key4": ClassDA ("Name4")}

そのディクショナリは、'ClassDA'インスタンスも'ClassDB'インスタンスも格納しなければならないから、それは、'Dict [str, "ClassD"]'というタイプを持っている、しかし、もしもそれが、取り出された値が、'ClassDA'インスタンスや'ClassDB'インスタンスとして扱えないことを意味するのであれば、...そのようなプログラミング言語は、受け入れられないであろう(少なくとも、私には受け入れられない)。...したがって、タイプキャストは不可避である。

安心したことに、'mypy'では、タイプキャストが可能である。

以下のコード(上記コードの続きである)が、タイプキャストの使用方法を理解するのに十分であろう。

@Python ソースコード
~
from typing import cast

l_elementKey: str
l_elementValue: "ClassD"
for l_elementKey, l_elementValue in l_dictionary1.items ():
 if isinstance (l_elementValue, ClassDA):
  l_classDA: "ClassDA" = cast ("ClassDA", l_elementValue)
  print (l_classDA.methodAA ())
 if isinstance (l_elementValue, ClassDB):
  l_classDB: "ClassDB" = cast ("ClassDB", l_elementValue)
  print (l_classDB.methodAB ())

しかしながら、'mypy'のタイプキャストはとても心が広いということを知っておくことは重要だろう: いかなるキャストも許される。...例えば、以下のキャストは許される。

@Python ソースコード
l_string: str = "ABC"
l_integer: int = 1

l_integer = cast (int, l_string)

勿論、以下はチェックされるが、チェックされているのは、キャストではなく、代入である。

@Python ソースコード
~
l_integer = cast (float, l_string)

念のために述べると、 'mypy'のタイプキャストは、完全に静的なチェックであって、実行時のタイプ間不整合は一切エラー報告されない、Javaとは違って。例えば、以下は、機嫌良く動作して、アウトプットは、'ABC'になる!

@Python ソースコード
l_object: object  = "ABC"
l_integer = cast (int, l_object)
print (str (l_integer))


5: Generics ジェネリクス


Hypothesizer 7
Generics is also a necessity for me. ジェネリクスも私には不可欠だ。

えーと、'mypy'のマニュアルはジェネリクスをある程度は取り扱っているが、その扱いは、私には少し残念だ。

なぜか?...ジェネリクスファンクションについて、そのマニュアルでは、少なくとも1つの引数が各タイプパラメータに依存しているということが、疑いもなく想定されているが、必ずしもそうではない。

以下は、その想定に合致したコードだ。

@Python ソースコード
from typing import List
from typing import TypeVar

T = TypeVar ("T")

def functionE (*a_items: T) -> List [T]:
 l_list: List [T] = []
 l_item: T
 for l_item in a_items:
  l_list.append (l_item)
 return l_list

l_list1: List [str] = functionE ("ABC", "DEF")
l_list2: List [object] = functionE ("ABC", "DEF") # This does not cause any error, surprisingly.

うむ?'l_list2: List [object]'への代入はエラーを起こすと私は予期していたのだが('List [object]'は'List [str]'のスーパークラスでもサブクラスでもないことを思い起こそう(その記事はJava配列に関するものだが、同一の論法がここにもあてはまる))、そうではなかった。...えーと、驚いたことに(私には特に好ましくないことだが)、'T'は、代入された変数のタイプを考慮にいれるらしい。

しかしながら、それで私の懸念が完全に払拭されるわけではない、なぜなら、以下はエラーを起こすから。。

@Python ソースコード
functionE ("ABC", "DEF").append (1) # Still, this causes an error.

実のところ、私はこのケースでは(いつもというわけではない、勿論: もしも、いつもそうなのであれば、ジェネリクスを使う必要性がない)、'functionE'に'List [object]'をリターンしてほしいのだが、その'functionE'呼び出しは、'List [str]'をリターンする、理解できることではあるが。...注目すべきは、それは、正当な要求だということだ、なぜなら、'functionE'は、そのリストをただ初期化するように想定されているのであって、そのリストを最終型にするように想定されているのではない: 初期要素群の全てがたまたま文字列だっただけであって、そのリストは、その後、非文字列の要素群も受け付けなければならない。

b つまり、リターンタイプは直接にパラメータ化されなければならない、引数群によって決定されるのではなく。

Javaや、ジェネリクスをサポートする私の知る限りのその他のプログラミング言語では、タイプパラメータ値を明示的に指定でき、それゆえ、リターンタイプを、引数タイプ群とは独立して指定できるが、Pythonでは、それが許されないので、唯一の可能なソリューションは、タイプを受け取る1つの引数(またはいくつかの引数、もしも複数のタイプパラメータがあれば)を追加することであるようだ。以下が例だ。

@Python ソースコード
from typing import List
from typing import Type
from typing import TypeVar

T = TypeVar ("T")

def functionF (a_type0: Type [T], *a_items: T) -> List [T]:
 l_list: List [T] = []
 l_item: T
 for l_item in a_items:
  l_list.append (l_item)
 return l_list

l_list1: List [str] = functionF (str, "ABC", "DEF")
l_list2: List [object] = functionF (object, "ABC", "DEF")
l_list3: List [object] = functionF (str, "ABC", "DEF") # I am not happy about this behavior, but that seems unpreventable.
functionF (object, "ABC", "DEF").append (1) # This does not cause any error.
functionF (str, "ABC", "DEF").append (1) # An error, rightfully.

注目すべきは、'object'タイプパラメータを指定することが、'T'が'str'に縮退することを防いでいる、もしもすべての'a_items'引数が文字列であったとしても、ということだ。

クラス全体をパラメータ付きにする必要がある(一部のメソッド群だけでなく)時は、以下のようにできる。

@Python ソースコード
from typing import Generic
from typing import TypeVar

T = TypeVar ("T")

class ClassE (Generic [T]):
 def __init__ (a_this: "ClassE", a_t: T) -> None:
  a_this.i_t: T
  
  a_this.i_t = a_t
 
 def methodA (a_this: "ClassE") -> T:
  return a_this.i_t


6: 'mypy'をインストールする


Hypothesizer 7
ソースファイル群にアノテーションを付ける方法を学んだので、チェックを実行する方法に移ろう。

'mypy'は、以下のコマンドでインストールできる('pip'がインストールされた後で)。

@bash ソースコード
python3 -m pip install mypy


7: 'mypy'を使用するためのいくつかの秘訣


Hypothesizer 7
ネームスペースパッケージは、私には不可欠である。その理由は、私は、私のPythonコード(複数プロジェクトにわたる)のほとんど全てを'theBiasPlanet'パッケージ配下に置いているから。...それは標準的プラクティスだと私は考える: 会社なり個人なりは、自らのコードを、その会社または個人を表わす単一のパッケージ配下に置く、他の主体によるコードとのモジュール名の重複を避けるために。そのような標準的プラクティスに従うのに、なぜ、特別な扱いが必要とされなければならないのか、私には理解できない、実のところ...

それはともかく、ネームスペースパッケージが使用される時は、'mypy'コマンド実行に、'--namespace-packages'フラグを指定しなければならない。

'スタブ'は、モジュールのスケルトンであり、それを'mypy'は、そのモジュール内のクラス群、ファンクション群等がどのようなものであるかを知るために使用できる。...ある意味、それは'C++のヘッダーファイル'に似ている。

例えば、以下は、あるモジュールとそのスタブである。

@Python ソースコード
from datetime import datetime
from typing import Generic
from typing import TypeVar

T = TypeVar ("T")

class ClassF (Generic [T]):
 def __init__ (a_this: "ClassF", a_t: T) -> None:
  a_this.i_t: T
  a_this.i_datetime: datetime
  
  a_this.i_t = a_t
  a_this.i_datetime = datetime.now ()
 
 def methodA (a_this: "ClassF") -> T:
  return a_this.i_t

@Pythonスタブ ソースコード
from datetime import datetime
from typing import Generic
from typing import TypeVar


T = TypeVar('T')

class ClassF (Generic [T]):
    def __init__ (a_this: "ClassF", a_t: T) -> None:
        a_this.i_t: T
        a_this.i_datetime: datetime
        ...
    def methodA(a_this: ClassF) -> T: ...


上記スタブ内で、メソッド内のロジックたちは、"..."で置換されている。

えーと、...スタブのようなものを私は作成しなければならなのか?...実のところ、必ずしもそうではない: スタブファイルの代わりにソースファイルを'mypy'に読ませることができる。...しかしながら、ソースファイル群を配布する意図がなく、コンパイルされた結果の'pyc'ファイル群を配布する意図であれば、スタブ群を配布しなければならないだろう、なぜなら、'mypy'は、コンパイル結果ファイル群を読めないから。

それで、私はスタブ群を手動で作成しなければならないのか?...'mypy'には、スタブジェネレーターである'stubgen'が含まれている、...しかし、それは、スタブ群を満足いくように作成してくれるものではない。

実は、以下が、上記ソースコードに対して'stubgen'がジェネレートするスタブファイルだ。

@Python stub ソースコード
# Stubs for ClassF (Python 3)
#
# NOTE: This dynamically typed stub was automatically generated by stubgen.

from typing import TypeVar

T = TypeVar('T')

class ClassF:
    def __init__(a_this: ClassF, a_t: T) -> None: ...
    def methodA(a_this: ClassF) -> T: ...

...それが満足いくものでない理由は、1) インスタンスメンバー変数群の定義が消えており、2) 'Generic [T]'というスーパークラス指定が消えていること。

1)について、それがオーケーでない理由は、それらのインスタンスメンバー変数にアクセスする'ClassF'の任意のサブクラスが'mypy'によってエラー判定されてしまうこと、もしも、それらのインスタンスメンバー変数群が直接に'ClassF'の外からアクセスされるといことがなくても。...したがって、'__init__'メソッドの最初の何行かはスタブ内に保持しておく必要があり、そのためには、'from datetime import datetime'という行も保持することが必要となる。

2)について、それがオーケーでない理由は、説明の必要がないだろう。

実は、本サイトに挙げられている私のサンプルたちに含まれている私のGradleスクリプト群は、そうした不満足なスタブ群を、いくつかのソースファイルルール(それに私のコードは適合している)に基づいて、不満足度の少ないスタブ群へ自動的に変換する。そのルールとは、1) 'import'行たちは先頭行から連続して(そのブロック内には空行がなく、ブロック後に空行が1つある。'#'で開始する行は許される。'if '行および'else '行は許される)書かれ、 2) クラスインスタンスメンバー変数群は、'__init__'メソッドの先頭連続行群に定義され、そのブロックの後に空行が1行続く。

'mypy'が探索するモジュールパス群は、'MYPYPATH'環境変数にセットする。

以下は、単一ファイルをチェックするコマンドフォーマットだ(単一ファイルをチェックする方法を私が知らなければならない理由は、モジュール群の内のただ1つが変更されたときに、全ソースコードのチェックなどやってられないことだ)。

@bash ソースコード
mypy %the source file path%

以下は、単一ファイルのスタブを、'stubgen'コマンドを使用してジェネレートするコマンドフォーマットだ(私がモジュール毎にスタブをジェネレートしなければならない理由は、モジュール群の内のただ1つが変更されたときに、全ソースコードのスタブの再ジェネレートなどやってられないことだ)。

@bash ソースコード
stubgen -o %the output directory%  %the source file path%

注意すべきは、スタブは、'pyi'というファイル名拡張子を持たなければならないことだ。

あるサードパーティ製ライブラリーがタイプアノテーション付きソースもスタブファイルも提供しなかったらどうなるのか?

えーと、その気があれば、スタブを自分で手動で作成することはできるが、多分、私のソースコードをそのサードパーティ製ライブラリーに対してチェックすることを諦めるだろう。

以下がそうする方法だ: 'mypy.ini'というファイルを、いくつかの指定をその中に入れて作成し、'mypy'実行において、'--config-file'フラグでそのファイルを指定する。

以下は、サンプル'mypy.ini'ファイルで、'aaa.Aaa'および'aaa.Bbb'は無視されるべきモジュールだ。

@'mypy' configuration ソースコード
[mypy]

[mypy-aaa.Aaa]
ignore_missing_imports = True

[mypy-aaa.Bbb]
ignore_missing_imports = True


8: 結びとその先


Hypothesizer 7
これで、Pythonのおいて静的タイプチェックを行なうための実用上十分なテクニックを得たようだ。

勿論、私は、それが絶対的に十分だと主張しはしない、しかし、ほとんどのケースにおいて静的タイプチェックを行なうのに十分だと感じている。

もっと特別なテクニックを見つけた時は、本シリーズで報告しよう。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>