ラベル Pythonプログラミング言語を理解することをお許しください の投稿を表示しています。 すべての投稿を表示
ラベル Pythonプログラミング言語を理解することをお許しください の投稿を表示しています。 すべての投稿を表示

2020年8月16日日曜日

9: Pythonにおいて、任意の標準入力待ちを中断する

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

汎用的解決策、任意のスレッドに、任意のタイミングで、待ちを、開始、中断、再開させられる。入力は、非標準でも構いません。

話題


About: Pythonプログラミング言語

この記事の目次

8: Python文字列パイプ(文字列をシリアルに運ぶ)

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

シリアルなのは、さもなければ、メモリが使い果たされてしまうかもしれないから。'java.io.PipedWriter' + 'java.io.PipedReader'に対する、Python相当物。

話題


About: Pythonプログラミング言語

この記事の目次

2020年7月12日日曜日

7: Pythonは、値渡しかリファレンス渡しか、最も簡潔に

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


要約


広く流布しているああした醜い説明たちには暇をやるべきだ、それらを必要化する賢明でない用語体系と共に。この回答で十分であるはずなのだ。

話題


About: Pythonプログラミング言語

この記事の目次

2020年6月21日日曜日

5: Pythonサイクリックインポートを、構造を歪めることなく解消する

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

サイクリックな依存は極めて自然で問題ありません、ドメインモデル的には。Pythonがそれに対処できないからといって、その構造を歪めるようにあなたがアドバイスされるべきではありません。

話題


About: Pythonプログラミング言語

この記事の目次

2020年6月7日日曜日

3: なぜ、Python(およびJava「リファレンスタイプ」)変数は'ポインター'と呼ばれるべきなのか

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


動機


Python(およびJava「リファレンスタイプ」)変数が'ポインター'であることを認めないことは、とても不得策であり、一部の不必要に混乱した言説の元凶となっている。

話題


About: Pythonプログラミング言語
About: Javaプログラミング言語

この記事の目次


開始コンテキスト


  • 読者は、プログラミング一般に関する基本的な知識を持っている。

ターゲットコンテキスト



  • 読者は、リファレンス(C++におけるようなリファレンス)とポインター('広い意味におけるアドレス'ポインター)の違いを理解し、Python(およびJavaのいわゆる「リファレンスタイプ」)変数は'ポインター'と呼ばれるべきであることを理解する。

オリエンテーション


Hypothesizer 7
実のところ、Python変数はどれもポインターである、Javaのいわゆる「リファレンスタイプ」変数がどれもそうであるように。

お分かりのように、私は、'ポインター'という用語を、一部の人々が言い張っているよりも広い(そして、よりリーズナブルな)意味で使用している。

それらの人々は、Python変数やJavaのいわゆる「リファレンスタイプ」変数は'ポインター'ではなく、'リファレンス'と呼ばれなければならない、と言い張っているが、そのような用語体系は助けになっておらず有害である、と私は論じる。

はあ?用語の定義はどれも恣意的ではないのか?...基本的には、そうだ。それでは、ある用語体系を非難する奴は馬鹿げてるのではないか、なぜなら、誰がどんな用語体系を自分の好きなように恣意的に定義しても構わないのだから?...そうではない、実のところ、少なくとも、適切な用語体系というものを話題にしているのであれば。

どんな用語体系も、適切であるためには、少なくとも2つの要件を満たしていなければならず、それらを満たしていない用語体系は、責められてよいし、責められるべきだ。

実のところ、Python変数およびJavaのいわゆる「リファレンスタイプ」変数に関わる広く流布された用語体系の不適切さは、「値渡し」か「リファレンス渡し」かが問題となる際にあらわになる。


本体


1: Python変数およびJavaのいわゆる「リファレンスタイプ」変数に関わる広く流布された用語体系


Hypothesizer 7
Python変数およびJavaのいわゆる「リファレンスタイプ」変数に関わる広く流布された用語体系は、'リファレンス'という用語を、'C++におけるようなリファレンス'も'広い意味におけるアドレス'ポインターも含む、ある広いカテゴリを意味して使い、'ポインター'という用語を、'物理メモリアドレス'ポインター'しか含まない、ある狭いカテゴリを意味して使う。

その用語体系によれば、すべての非「バリュータイプ」変数は、リファレンスであり、厳密に物理メモリアドレスを(もっと広い意味におけるアドレスをではなく)格納できる変数でなければ、それは決して'ポインター'と呼ばれてはならない。

えーと、一部の人々は、そうした'リファレンス'および'ポインター'の定義を採用する揺るぎない権利が自分たちにはある、と 主張するかもしれないが、彼らの用語体系が全体として適切であるかどうかは、別問題だ。


2: 用語体系が適切であるための2つの要件


Hypothesizer 7
どんな用語体系も、適切であるためには、少なくとも2つの要件を満たしていなければならない。

第1に、それは、全ての重要なコンセプトを区分けしていなければならない。

だって、それが、用語体系の主要目的でしょう?

もしも、ある用語体系が、重要でないコンセプト間の不必要な区別をすることに熱中する一方で、とても重要なコンセプト間の区別をするという主要任務を怠っていれば、その用語体系は、不適切であるとみなされなければならないだろう。

第2に、それは、首尾一貫していなければならない。

というのは、もしも、ある用語がある箇所で登場してあるものを意味し、別の箇所で登場して別のものを意味するのであれば、その用語は、首尾一貫して使われていない。...確かに、どの用語も定義は基本的には恣意的であるが、その用語がひとたび定義されたら、それは、常にその単一の意味で使われなければならない、そうでなければ、話の受け手のの一部は混乱させられかねない。

コンテキストが意味を明らかにしてくれると主張する人々が一部にいるかもしれないが、そのような保証はない、なぜなら、その用語が使われるであろうあらゆるコンテキストを誰も予言できないのだから。それに、話の送り手によって意図されたコンテキストが、話の受け手によって本当に正確に共有されていることなど、ほとんどないのではなかろうか?

その一方、1つのコンセプトに対して複数の用語があるというのも、とても望ましくないことだ。話の受け手はそれぞれ、なぜ各用語がそこで使われているのかの意図を見分けようとするわけ(話を受けるというのはそういう行為だ、と私は考える)であり、いくつか複数の用語が同じものを意味して気まぐれに使われたら、それは、話の送り手と受け手の間の信頼に対する裏切りであろう。...言い換えると、既存の用語によって既に代表されているコンセプトに対して新しい用語が作られるべきではない、なぜなら、そのような行為は、新たなコンセプトがそこにある、と虚偽に示唆するから。

簡潔に言うと、用語の集合は、コンセプトの集合と1対1写像を持っていなければならない。


3: C++におけるようなリファレンスと広い意味におけるアドレスポインターの違いは極めて重要であり、それをどのPythonまたはJavaプログラマーも理解しなければならない


Hypothesizer 7
C++におけるようなリファレンスと広い意味におけるアドレスポインターの違いは極めて重要であり、それをどのPythonまたはJavaプログラマーも理解しなければならない。

その理由は、ある変数がその一方であるか他方であるかによって、そのプログラムの振る舞いが著しく変わってくることだ。

ここでは、各コンセプトのメカニズムを見てみよう。


3-1: C++におけるようなリファレンスのメカニズム、図付き


Hypothesizer 7
C++におけるようなリファレンスが何であるかを、私は既に論じたが、ここでは、それを、図を使って、もっと簡潔に示そう。

1片のC++コードを考えてみる。

@C++ ソースコード
#include <iostream>
#include <string>

class TestClass {
 public:
  ::std::string & i_string;
  TestClass (::std::string & a_string);
  virtual ~TestClass ();
  TestClass & operator= (const TestClass & a_TestClass);
};

TestClass::TestClass (::std::string & a_string): i_string (a_string) {
}

TestClass::~TestClass () {
}

TestClass & TestClass::operator= (const TestClass & a_TestClass) {
 i_string = a_TestClass.i_string;
 return *this;
}

class Test1Test {
 public:
  static int main (int const & a_argumentsNumber, char const * const a_arguments []);
};

void testFunction (TestClass & a_testClassA, TestClass & a_testClassB, ::std::string & a_replacingString) {
 // the phase 2
 a_testClassA.i_string = a_replacingString;
 // the phase 3
 a_testClassB = a_testClassA;
 // the phase 4
}
   
int Test1Test::main (int const & a_argumentsNumber, char const * const a_arguments []) {
 ::std::string l_stringA ("A");
 ::std::string l_stringB ("B");
 ::std::string l_stringC ("C");
 TestClass l_testClassA = TestClass (l_stringA);
 TestClass l_testClassB = TestClass (l_stringB);
 // the phase 1
 testFunction (l_testClassA, l_testClassB, l_stringC);
 ::std::cout << "### 'l_testClassA': " << l_testClassA.i_string << ", " << "'l_testClassB': " << l_testClassB.i_string << ::std::endl;
 return 0;
}

上記におけるメモリ状態を以下のようにカリカチュア化する。


上記の図(および以下の図たち)において、メモリ状態は、スプレッドシートのようにカリカチュア化されており(厳密に正確というわけではないが、正確にすると、図が無益(本記事の目的において)に複雑になってしまう)、セルのアドレスは'A1'のように表わされ、セル上のまたはセルに重ねられた(C++におけるようなリファレンスに対して私はそう形容する)ボックスは、変数である。

第1フェーズでは、通常変数、'l_stringA'、'l_stringB'、'l_stringC'、'l_testClassA'、'l_testClassB'、およびC++におけるようなリファレンス変数、'l_testClassA.i_string'、'l_testClassB.i_string'がある。

注目すべきは、C++におけるようなリファレンス変数はそれぞれ、既に占有されたセルに重ねられて定義されているということで、それがC++におけるようなリファレンスのエッセンスだ。...これは余談だが、C++におけるようなリファレンス変数はそれぞれ、独立したボックスとして表わされなければならない、なぜなら、C++におけるようなリファレンスはどれも、変数の別名などではなく、それ自体として、れっきとした新たな変数なのだから

第2フェーズでは、C++におけるようなリファレンス変数、'a_replacingString'、'a_testClassA'、'a_testClassB'が、既に占有された3つのセルに重ね られる形で追加されているが、それが、いわゆる「リファレンス渡し」が本当に意味することなのだ。

第3フェーズでは、'a_replacingString'のコンテンツ('C1'セルの値である)である"C"が、'a_testClassA.i_string'('A1'セルに重ねられている)にセットされているが、それは、'A1'セルが"C"という値を獲得するということだ。

第4フェーズでは、代入オペレーターが、'a_testClassB'に対して'a_testClassA'という引数で働き、それ(その代入オペレーター)が、'a_testClassA.i_string'('A1'セルに重ねられており、値が"C"である)のコンテンツを'a_testClassB.i_string'('B1'セルに重ねられている)にセットするが、それは、'B1'セルが"C"という値を獲得するということだ。

アウトプットは以下のとおりだ。

@出力
### 'l_testClassA': C, 'l_testClassB': C

プロセス全体が水晶のように透明だ。


3-2: 広い意味におけるアドレスポインターのメカニズム、図付き


Hypothesizer 7
広い意味におけるアドレスポインターとは何かを、図付きで示そう。

また別の1片のC++コードを考えてみる。

@C++ ソースコード
#include <iostream>
#include <string>

class TestClass {
 public:
  ::std::string * i_string;
  TestClass (::std::string * a_string);
  virtual ~TestClass ();
  TestClass & operator= (const TestClass & a_TestClass);
};

TestClass::TestClass (::std::string * a_string): i_string (a_string) {
}

TestClass::~TestClass () {
}

TestClass & TestClass::operator= (const TestClass & a_TestClass) {
 i_string = a_TestClass.i_string;
 return *this;
}

class Test1Test {
 public:
  static int main (int const & a_argumentsNumber, char const * const a_arguments []);
};

void testFunction (TestClass * a_testClassA, TestClass * a_testClassB, ::std::string * a_replacingString) {
 // the phase 2
 a_testClassA->i_string = a_replacingString;
 // the phase 3
 a_testClassB = a_testClassA;
 // the phase 4
}

int Test1Test::main (int const & a_argumentsNumber, char const * const a_arguments []) {
 ::std::string * l_stringA = new ::std::string ("A");
 ::std::string * l_stringB = new ::std::string ("B");
 ::std::string * l_stringC = new ::std::string ("C");
 TestClass * l_testClassA = new TestClass (l_stringA);
 TestClass * l_testClassB = new TestClass (l_stringB);
 // the phase 1
 testFunction (l_testClassA, l_testClassB, l_stringC);
 ::std::cout << "### 'l_testClassA': " << *(l_testClassA->i_string) << ", " << "'l_testClassB': " << *(l_testClassB->i_string) << ::std::endl;
 delete l_testClassA;
 delete l_testClassB;
 delete l_stringA;
 delete l_stringB;
 delete l_stringC;
 return 0;
}

実のところ、C++における広い意味におけるアドレスポインターは、物理メモリアドレスポインターでもあるのだが、それは全然どうでもいいことだ。

上記におけるメモリ状態を以下のようにカリカチュア化する。


第1フェーズでは、広い意味におけるアドレスポインター、'l_stringA'、'l_stringB'、'l_stringC'、'l_testClassA'、'l_testClassB'、'l_testClassA.i_string'、'l_testClassB.i_string'がある。

注目すべきは、セル、'A1'、'B1'、'C1'、'A4'、'B4'のそれぞれが値を持っているが、その上にボックスはないということで、それは、そこには何の変数も割り当てられていないことを意味している。もう1つ注目すべきは、広い意味におけるアドレスポインターはそれぞれ、'=A1'のような値を持っていることで、それは、その値がアドレスであることを意味しており、それが、広い意味におけるアドレスポインターのエッセンスだ。

第2フェーズでは、広い意味におけるアドレスポインター、'a_replacingString'、'a_testClassA'、'a_testClassB'が、それまで占有されていなかった3つのセル上に追加されて、それらが、渡されたデータを値として獲得するが、それが、いわゆる「値渡し」が本当に意味することだ。...本ケースでは、引数たちが広い意味におけるアドレスポインターだから、それらの値がたまたまアドレスである(広い意味におけるアドレスポインターの値はアドレスまたはnullである)のだが、そういうことに関係なく、それは、まさしく「値渡し」である。

第3フェーズでは、'a_replacingString'のコンテンツ('D1'セルの値であり、'=C1'である)が、'a_testClassA.i_string'('A3'セル上にある)にセットされるが、それは、'A3'セルが'=C1'という値を獲得することを意味する。

第4フェーズでは、'a_testClassA'のコンテンツ('D2'セルの値であり、'=A4'である)が、'a_testClassB'('D3'セル上にある)にセットされるが、それは、'D3'セルが'=A4'という値を獲得することを意味する。

'l_testClassB.i_string'も、それに指される'B1'セル内のデータも、第4フェーズにおいて変化しない(それらは、'D3'セルの値の変化などに全く関知しない)ことは、明白に分かる。

アウトプットは以下のとおりだ。

@出力
### 'l_testClassA': C, 'l_testClassB': B

プロセス全体が、ここでも、水晶のように透明だ。


3-3: Python(または、Javaのいわゆる"リファレンスタイプ")変数のメカニズム


Hypothesizer 7
それでは、Pyhon変数のメカニズムを吟味してみよう、以下の1片のPythonコードを通して。

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

class TestClass:
 def __init__ (a_this: "TestClass", a_string: str) -> None:
  a_this.i_string: str
  
  a_this.i_string = a_string

class Test1Test:
 @staticmethod
 def main (a_arguments: List [str]) -> None:
  l_stringA: str = "A"
  l_stringB: str = "B"
  l_stringC: str = "C"
  l_testClassA: "TestClass" = TestClass (l_stringA)
  l_testClassB: "TestClass" = TestClass (l_stringB)
  # the phase 1
  testFunction (l_testClassA, l_testClassB, l_stringC)
  sys.stdout.write ("### 'l_testClassA': " + l_testClassA.i_string + ", " + "'l_testClassB': " + l_testClassB.i_string + "\n")

def testFunction (a_testClassA: "TestClass", a_testClassB: "TestClass", a_replacingString: str) -> None:
 # the phase 2
 a_testClassA.i_string = a_replacingString
 # the phase 3
 a_testClassB = a_testClassA
 # the phase 4

if __name__ == "__main__":
 Test1Test.main (sys.argv)

メモリ状態のカリカチュア?上掲の、広い意味におけるアドレスポインターのものを参照してください、なぜなら、Python変数のメモリ状態カリカチュアは、広い意味におけるアドレスポインターのそれと全く同じだから 。

アウトプット?勿論、同じです。

事情は、Javaのいわゆる「リファレンスタイプ」変数においても全く同じだ。


4: 広く流布している用語体系の適切さを検証する


Hypothesizer 7
それでは、広く流布している用語体系の適切さを検証しよう。

勿論、私は、それが、適切な用語体系の資格を満たしているか否かチェックしなければならない。

えーと、それは、C++におけるようなリファレンスと広い意味におけるポインターというとても重要なコンセプトを区分けしているようには思われない。

'リファレンス'のその広い定義が本当に言っているのは、Pythonの(またはJavaのいわゆる「リファレンスタイプ」の)変数は、C++におけるようなリファレンスか、もしくは広い意味におけるアドレスポインターか、もしくはいわゆる「バリュータイプ」変数ではない何らかのものであるが、実際にその内のどれであるかは知らない...、ということだ。

...そんなぼんやりしたカテゴリー分けでは済まないんですよ、と私は言う。だって、それは、変数のメカニズムを特定していないが、そのメカニズムが重要なわけ、それがプログラムの振る舞いを決定するのだから。

確かに、「Pythonの変数はどれもリファレンスだ」のような言明は、'リファレンス'のその広い意味を採用する限りは、それ自体として間違っているわけではないかもしれないが、その言明は、変数の振る舞いを説明する役に立っていない。

他方で、その定義は、ポインターはすべからく物理メモリアドレス(もっと広い意味におけるアドレスではなく)を保持できるものでなければならない、と言い張っているが、その合理性を私は全然理解しない。...なぜ、そうでなければならないのか?...'ポインター'という用語に、そのように非合理で無益な制限を、おせっかいにかけているように感じざるをえない。だって、そのような区別は、少なくともほとんどのプログラマーにとって、全然どうでもよいことであり、重要なのは、広い意味におけるアドレスポインター全てに共通な上記のメカニズムだ。...実際、C++ポインターが物理メモリアドレスポインターであるか否かなど、私にとって重要だったことは決してない、だって、上記2番目のプログラムにおける広い意味におけるアドレスポインターが物理メモリアドレスポインターか否かなど、そのプログラムの振る舞いに全然関係ないのだから。...もしも、ある物理メモリアドレスポインターを広い意味におけるポインターから区別しなければならないことがあるのであれば(そのような機会は、あったとしても稀だろうと私は推測するが)、'ハードポインター'のような用語を作ればよいだけだ。

その定義は、ポインターにはすべからくアドレス操作が許されなければならない、とも言い張っている。...なぜ、そうでなければならないのでしょうか?...アドレス操作が許されるか否かは、...アドレス操作が許されるか否か(言い換えると、そのプログラミング言語が、アドレス操作を行なうシンタックスを持っているか)についての話であって、変数のエッセンス(おおまかに言って、それが何を保持できるかおよびそれがどこにアロケートされるか、言い換えると、上記に図解されたメカニズム)についての話ではない。

Python(またはJavaのいわゆる「リファレンスタイプ」)変数に対する'.'、'->'、'*'、'+'等の用法(または不用法)が、C++ポインターに対するそれと同じに見えない?...それは、シンタックスの違いにすぎないのであって、変数のエッセンスについての話ではない(後続のセクションででもっと詳細に説明されるように)。.

それでは、広く流布している用語体系は、「重要でないコンセプト間の不必要な区別をすることに熱中する一方で、とても重要なコンセプト間の区別をするという主要任務を怠ってい」るという形容に当てはまっているだろうか?完全に当てはまっていると私は考えざるをえない。

えーと、第2の資格について言えば、問題は、「リファレンス」は、C++におけるようなリファレンスという意味で使われてきた、一部の人々が使うべきだと言い張っている広い意味にではなく、PythonやJavaが誕生するずっと以前から、ということだ。

同意しませんか?しかし、「リファレンス渡し」が顕著な例だ。ご存知のように、「リファレンス渡し」における「リファレンス」は、まさに、C++におけるようなリファレンス意味している、いわゆる「バリュータイプ」変数ではない何ものをも包含する広い意味ではなく。

したがって、結果として、'リファレンス'は、ある出現では、C++におけるようなリファレンス、を意味し、他の出現では、いわゆる「バリュータイプ」ではない何ものか、を意味するということになってしまっており、それが、私が、首尾一貫性の欠如と呼ぶものだ。

注意すべきは、Pythonの用語体系は、ソフトウェアコミュニティ全体のより広範囲の用語体系の中に生きているのであって、「リファレンス渡し」のような用語には我関せずとは言えないということだ。

実際、「値渡し」か「リファレンス渡し」かという疑問は、Pythonに対しても当然発せられるのであり、広く流布している答えがぐちゃぐちゃなのは、首尾一貫していない用語体系のせいである。

実は、正しい答えは、極めて明快である、Python変数がポインターとして認識されてさえいれば、後続のあるセクションで説明されるとおり。


5: Python変数はどれもポインターだという事実に対する反証だと感じられるかもしれない一部のPythonシンタックスについて


Hypothesizer 7
一部のPythonシンタックスが、Python変数はどれもポインターであるという事実の反証になっている、と感じる人がいるかもしれない。...もしも、そうでなければ、それは結構だ: 本セクションは、ただ読みとばせばよい。

私が考えつく限りのそうしたシンタックスを論じてみよう。

第1に、なぜ、Pythonは、C++が使う'->'でなく、'.'を使うのか?

C++が'->'を使う理由は、'->'を'.'から区別しなければならないからだ、その一方、Pythonは、'->'を何からも区別する必要がなく(なぜなら、'->'しか指定できないから)、したがって、'.'をC++の'->'の代わりに使うことに決めた、なぜなら、そのほうが1文字分短いから。

第2に、なぜ、Python変数は、'*' を用いて定義されないのか、C++のポインターとは違って?

C++が'*'を使うのは、さもなければ、その変数が通常変数になってしまうからであり、その一方、Pythonがそうしないのは、その変数は、どのみちポインターでしかありえないからだ。実際、Pythonは、'*'を使うことを義務付けることもできたが、そうしていたら、'*'を全ての変数定義に付けないといけないことになっていただろうが、それは、とても無駄な指の運動に思えるので、全ての'*'が全部省略されたわけ。

第3に、なぜ、Pythonは、C++のいわゆる実体化オペレーターである'*'を持たないのか?

C++が実体化オペレーターを用いる理由は、アドレス自体ではなくアドレスによって指されるデータが場合により要求されるからであり、その一方、Pythonがそうしない理由は、アドレス自体が常に求められるからだ。

例えば、我々は、'print (l_originalVariableA.i_stringMemberA)'のように書き、'print (*(l_originalVariableA.i_stringMemberA))'のようには書かない('l_originalVariableA.i_stringMemberA'はアドレスを表わしている)が、それは、'print'ファンクションがアドレスを求めるからだ。実際には、どのファンクションも、アドレスしか求めない、なぜなら、ポインターでない引数などというものは存在しないから。

第4に、ある1片のPythonコードを考えてみよう。

@Python ソースコード
l_integerA: int = 1

それは、そのポインターへ'1'というアドレスを入れているように見えるだろうか、'1'というデータのアドレスをではなく?...そのように見えるかもしれないが、それは、問題なく、'1'というデータのアドレスを入れているのだ。お分かりのように、Python変数がどれもポインターであるように、Pythonリテラルのどれもポインターライクなのだ('1'は、本当は、'1'というデータのアドレスを表わしている、そのデータそのものではなく、ということ)。

第5に、別の1片のPythonコードを考えてみよう。

@Python ソースコード
l_integerA: int = 1
l_integerB: int = l_integerA + 2

それは、'2'というデータのアドレスを'1'というデータのアドレスに加えているように見えるだろうか?...直感的に言えば、そう見えるかもしれないが、実際には、'+'オペレータは(またはどのオペレータも)、あるファンクションコールのシンタックスシュガーなのだ(ほとんどのC++プログラマーは、C++オペレータは実際にはどれもファンクションであることを知っているだろう)。つまり、'l_integerA + 1'は、'+ (l_integerA, 1)'を意味しており('+'は、ファンクション名だとみなしてください)、2つの引数によって指されるデータの和(つまり、'3')を保持する新たな'int'インスタンスのアドレスをリターンするのだ。...そのシンタックスは直感的に変に見えるということには賛成だが、Pythonがそういう直感的に変なシンタックスを選択したというだけのことだ。

要約すると、それらのシンタックスがそのようになっているのは、いくつかの理由があってのことであり、Python変数がどれもポインターであるという事実の反証となるものではない。シンタックスと変数のエッセンスは、プログラミング言語の異なる2つのレイヤーに属する事項であることを理解する必要がある。


6: ある政治的な理由


Hypothesizer 7
実のところ、こんなに長々と、なぜ、PythonおよびJava「リファレンスタイプ」変数たちがポインターと呼ばれるべきであるかを説明する必要などないのではないだろうか?

技術的に言えば、それらがそう呼ばれるべきでないと主張するなど、ほとんど馬鹿げている。

多分、ほとんどの人々は、真の理由は政治的なものだと知っているだろう: ポインターに付けられた正しくない汚名: 「ポインターは危険」および「ポインターは難しい」だ。

実のところ、ポインターそのものは全然危険ではなく、制限されないアドレス操作が危険なのだ。それでは、なぜ、アドレス操作を禁止しないのか、ポインターを許可したまま?それを、実際に、彼らは行なったのであり、それは、とてもよ良かったのだが、とてもまずいことに、彼らは、ポインターを駆逐したと不正確に主張してしまったのだ...

我々は技術者なのか政治家なのか、と私は問いたい。

技術者であるならば、「ポインターは危険だ」のような誤った言説を訂正すべきだ、そのような偏見におもねるのではなく。

他方で、ポインターは難しいのか?...えーと、ポインターのエッセンスは、実際には、スプレッドシートにとても馴染み深く出現するものであり、それは、上記カリカチュアで示したとおりだ。スプレッドシートの基本メカニズムを誰が理解できないのか?...少なくとも、プログラマーであれば、誰でもそれを極めて容易に理解できるだろう。

私の意見では、ポインターの知識は、どのプログラマーにも必須であり、必須なものを隠そうとすることは、なんらのプログラマーの助けにも全然なっていない。


7: それでは、Python(またはJava)は、ファンクション引数値を、"値渡し"で渡すのか、それとも、"リファレンス渡し"で渡すのか?


Hypothesizer 7
それでは、Pythonは、ファンクション引数値を、「値渡し」で渡すのか、それとも、「リファレンス渡し」で渡すのか?

実際には、「値渡し」は実体とかけ離れた名前だ: どちらにしろ、値が渡される。本当は、値が、その値を、それまで占有されていなかったスロットにアロケートされた引数にコピーすることで渡されるのか、それとも、引数を、その値の上にかぶせたリファレンスとして作成することで渡されるのか、という問題だ。したがって、'コピー渡し'がより適切だろうが、ここでは、「値渡し」をしぶしぶ使うことにする。

どちらにせよ、実際には、答えは、既に上記で暗黙に与えられているが、ここでは、それを明確に与えよう。

Pythonがファンクション引数値を渡すのは、純粋な「値渡し」以外の何ものでもない。

もっと説明すると、Pythonのファンクション引数はどれもポインターであり、その値はアドレス(または'None')であり、アドレス(または'None')がその引数にコピーされる: 「値渡し」そのものだ。

「値渡し」という古くからある変わらぬコンセプトを表わすために、おかしな新規な用語(「リファレンス値渡し」や「オブジェクト渡し」のような)は決して作られるべきでない(実体とかけ離れた名前の代わりに'コピー渡し'といったより適切な用語を採用するのは歓迎するが)、なぜなら、そのような新たな用語は、そこに新たなコンセプトがあるかのように誤って示唆するから(適切な用語体系の第2の資格を参照のこと) と私は言う。

事情は、Javaのいわゆる「リファレンスタイプ」ファンクション引数についても、まったく同じだ。


8: 結びとその先


Hypothesizer 7
JavaおよびPythonは、不合理に、ポインターを'ポインター'と呼ぶことを拒否してきた。'リファレンス'のより広い定義および'ポインター'より狭い定義採用して。

ある用語体系はどんな定義でも恣意的に採用できるというわけではないんですよ: どんな用語体系であろうと、全ての重要なコンセプトを区別し、首尾一貫していることは求められるわけであり、それを広く流布している用語体系は満たしていない。

問題は、Python変数やJavaの「リファレンスタイプ」変数が、単により広い意味における「リファレンス」としてではなく、ポインターとして理解されなければ、諸プログラムの振る舞いは正しく理解されないということだ。

ポインターのコンセプトを隠そうとする意図が偏在しているように私は推測しているが、それは、隠しうるものではない。

広く流布している用語体系が今のようであるのは、政治的な動機によるものだと私は理解しているが、技術者は技術者のように話すべきではないだろうか?


参考資料


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

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のおいて静的タイプチェックを行なうための実用上十分なテクニックを得たようだ。

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

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


参考資料


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