達人プログラマーの新装版がでたので読み直してみた

オーム社 さんから献本して頂きました。ありがとうございます!

原著は1999年10月20日に出版されたそうです。このブログ記事を書いているのが2016年11月なので17年前に書かれたことになります。元の出版社であるピアソンエデュケーション社が日本から撤退したことによりオーム社さんに移管され、その機会に翻訳も全面的に見直されたようです。

本書を読む動機付け

変化の速い IT 業界において17年前に書かれた本をいま読む価値があるのか?この問いに答えるのは難しいです。

開発プロジェクトという日々の業務では、様々な要件や移り変わる状況の中で何かしら制約がありつつも判断を下さなければなりません。それはアーキテクチャの策定であったり、開発方法論の実践だったり、個々の技術の選定だったりします。そこで判断を下し、そのときの判断が正しかったのかどうか、本当の意味でその正否はその後の歴史でしか分かりません。さらに何をもって正否と判断するのか。この正否の判断自体も多様な価値観で行われるものですが、達人プログラマーの原題である The Pragmatic ProgrammerPragmatic (実践的) という意味を借りて、広く浸透している考え方やうまく運用がまわっている仕組みをここでは正しいとします。その考え方が本当に広く浸透しているのか、その仕組みが多くの現場で使われているのか、それを私が保証することはできませんが、私の周りで見聞きする分にはそうみえるということで話を進めていきます。

閑話休題。本書を読むことで17年前に書かれた考え方や仕組みが 実践的に 正しかったのかどうかの一端を知ることができます。ともすれば、IT 業界というのは新しい技術や方法論が毎年たくさん出てきて、生き残るものもあれば消えていくものもまた多いです。そんな業界で17年も生き残っているものがあるとしたら、それは普遍的なものだと言ってしまっても過言ではないかもしれません。目新しいものに注意を取られがちな日常で何が本質かを考えたときにその基礎を教えてくれる、歴史を経た 達人プログラマー は単にプラクティスをまとめた本ではなくなっているように私は思います。

ある程度、業務でプログラマー経験のある方は私のような読み方もできますが、一方で経験の少ない若い方にとってはどうでしょうか。もちろん若い方へもお勧めしますが、本書の中で特定技術を例として紹介されているものの中には、いまは廃れてしまって現状にそぐわないものもあります。そのため、若い方が聞いたことのない特定技術が出てきたときは、その技術については自分で調べ直してみることが必要になります。訳注で補足をしている箇所もいくつかあるのであわせて確認してみてください。

達人プログラマーの所感

私が印象に残った節についていくつか抜粋しながら簡単に所感を書いていきます。

4 十分によいソフトウェア

この節ではソフトウェアのリリースをどのタイミングで行うかといった考え方を書いています。内容自体はプロジェクトマネージャーやプロダクトマネージャー向けに書かれているものですが、私は読んでいて、一般の開発者がいつプルリクエスト (以下PR) を送るかというタイミングを考えるときに似ているように思いました。

コードレビューをするのが当たり前のワークフローになりつつある昨今、どのタイミングで PR を送るか悩む人もいると思います。私は自分の中で8割ぐらいの品質になったら気軽に PR を送る方です。言わば、いくつかツッコミ所を残した状態で PR を送っています。その理由は次になります。

  • その PR の課題に対する解決策として設計や考え方があっているかどうかを判断してもらう
  • 全体からすると些細な内容でどう対応するかを自分の中でも迷いがあるので意見がほしい
  • 自分が書いたコードの意図が他人に伝わるかどうかをみてみる

そしてレビューしてもらって設計や考え方があっていないのであれば、やはり気軽に PR を取りやめて再設計します。どんなに時間をかけて品質をあげても他人の視点をもつことはできません。もちろん自分の中ではこの設計方針がベストだと考えて PR を送るわけですが、それまで自分が考えていなかった懸念や概念を他人から与えられることによって、他のやり方が妥当だと思うこともあります。

せっかく書いたコードを捨てるというのは悔しい行為だと思います。時間をかければかけるほど、サンクコストも気になってしまいます。時間 (コスト) のかかる機能開発だと進捗を小まめにレビューしてもらって手戻りを少なくする工夫をすると良いと思います。

7 二重化の過ち

いわゆる DRY原則 を紹介しながら、いろんな状況で二重化が起きることを説明しています。システム開発をしていて最も身近な問題の1つと言っても良いかもしれません。

プログラミングを始めた人にとって、おそらく初期段階でそこそこの規模のアプリケーションを実装していると実感すると思います。引き継いだ既存のアプリケーションの品質がよくないとそういった重複コードに悩むこともあります。

原則はあくまで原則でしかなく、結局のところその場その状況において何が最善であるかを選択する必要がでてきます。メリット/デメリットを考慮した上でやむを得ず二重化するという判断も現実にはあるでしょうが、二重化には多くの弊害があるという事実を知っておくことが重要です。そして、そうならないようにどうすれば良いかを考えていくことも重要です。そういう考え方をしているうちに、言語機能、設計手法や開発方法論で解決しようとしていることへの理解にもつながっていくように私は思います。

8 直交性

ときどき聞く言葉なのに私はちゃんと定義を把握していなかったので再学習しました。

「直交性」とは幾何学の分野から拝借してきた用語です。(中略)。この用語はコンピューティングの分野では、ある種の独立性、あるいは分離性を表しています。2つ以上のものごとで、片方を変更しても他方に影響を与えない場合、それらは直交していると呼ぶわけです。

直交性は DRY の原則とも密接に関係しています。またこの節では設計、ツール、コーディング、テスト、ドキュメントにおいても直交性の概念が適用できると紹介しています。

優れたフレームワークやライブラリ、良いプラクティスが共有されやすい昨今だと、直交性は当たり前過ぎてあまり意識することはないかもしれません。そういった背景にある概念を学ぶ良い機会にも思いました。

12 専用の言語

いまの言葉にすると ドメイン固有言語 (以下DSL) に相当すると思います。Tips としては以下のように書かれています。

問題領域に近いところでプログラミングを行うこと

この概念自体は適切だと思いますが、その事例の1つとしてミニ言語を実装する事例が紹介されています。またそれが必要な根拠として sendmail の設定は複雑なので DSL により制御を容易にする例を紹介しています。

達人プログラマーに書かれているほとんどの内容は、小さい開発プロジェクトにも大きい開発プロジェクトにも両方適用できるように書かれていますが、DSL に関して私は慎重派なので反対の立場にたってその理由を書いてみます。

もちろん巨大で複雑なプロジェクトや十分に成熟したアプリケーションに対して DSL が大きな価値を提供する場面があることは私も同意します。あえて反論しようと思ったのは DSL を開発するデメリットもあるからです。

  • 開発者にとって DSL の開発・保守コストがかかる
  • ユーザーにとって DSL の学習コストがかかる
  • ユーザーにとって DSL で書かれた機能や設定の保守コストがかかる
  • アプリケーション本体の機能と DSL が提供する機能の間にズレが生じる可能性がある
  • アプリケーション本体と DSL の依存関係を管理する必要がある

ざっと思いついたものをあげてみました。自分が取り組んでいる開発プロジェクトでこれらのコストを負担しても DSL を使うメリットがある場合は取り組んでも良いでしょう。逆にこれらのコストを考えずに安易に DSL を使うと将来の技術的負債となることもあると思います。

14 プレインテキストの威力

シンプル且つ意味深なように思えてなぜか印象に残りました。

知識はプレインテキストに保存すること

テキストの利点として以下をあげています。

  • 透明性が保証される
  • さまざまな活用ができる
  • テストが容易になる

いまどきの利点をさらにあげるとバージョン管理システムで差分管理しやすいという点も追加したいです。

これはこれで言っていることは正しいのですが、言わばテキストなら後から何とでもできるということでしかありません。もし特定用途に使いたいとしたらデータを正規化して扱いたくなるので、テキストデータは逆に曖昧で扱いにくいものです。例えば、HTTP/2 でバイナリープロトコルを採用した理由として、解析が容易でエラーチェックが厳密である利点があげられています。

18 デバッグ

私はこの節が好きです。残念ながらデバッグの手法は昔と比べてほとんど進歩がないように思います。大きく分けると以下の2つでしょう。

もちろん言語処理系の型システムが強力になったり、ツール/ライブラリが拡張されてより便利になったりはしています。しかし、エラーが発生して人間がソースコードを読みながら解析して原因を追求していくという行為自体はほとんど変わっていません。この節ではそのための心構えや戦略が丁寧に説かれています。

1つ私も過去にやっていた実践的な手法を紹介します。

問題の原因を探し出すための非常に簡単で効果的なテクニックとして、「誰かに説明する」という手法があります。

これはデバッグに限らず、コードレビューをレビューツール上だけでなく、レビューアに対面で説明するときにも有効です。説明しているうちに自分でもっと良い方法を思いついたりすることもあります。おそらく人間の思考として、ものごとを多面的にみる上でアウトプットの方法を変える手法が強力なんだと思います。

21 契約による設計

この節はどうなんでしょうね?あらかじめ要件や仕様を厳密にしやすい業務系ではこういった設計が定着しているのでしょうか?私は経験がなくて実践的にこの手法が使われているのかどうか知りません。

Web 業界ではまだまだここまで厳格な設計手法は定着していないように私は思います。また別の節にある表明プログラミングもそうですが、厳密さを保証するためのオーバーヘッド (実行効率や保守性など) もかかることから敬遠されがちなところもあると思います。

そう思ってはいるものの、最近契約による設計に関する記事をみかけたので以下を紹介しておきます。

28 時間的な結合

マルチスレッドを用いた並列処理について説明されています。さすがにこの節の内容や例は古くなってしまっているように感じます。非同期/並行処理 *1 は、プログラミング、システム設計、いろんな状況で本質的に難しい課題です。

この節ではワークフロー、アーキテクチャ、設計、インターフェースについて説明されていますが、内容がいまどきの Web 開発の問題とはややあっていないかもしれません。もちろん本書は Web 業界向けに書かれたものではないと思いますが、それでも昨今のクラウド化や開発方法論の変化との乖離により違和感があるのは仕方ないように思います。

1つだけ補足しておくと、昨今はそれぞれのプログラミング言語が非同期/並行処理をサポートする機能を提供していたりします。例えば、Python では asyncio というライブラリが標準で提供されています。そのライブラリはプログラマーが陥りがちな落とし穴を回避してくれます。そのため、自分で一から考えて作り込むのではなく、その言語やライブラリが提供する機能や仕組みに沿って開発するのがいまどきのプラクティスになるのではないかと思います。

31 偶発的プログラミング

本書を読みながらツイートしていて最も Impressions が高かったのがこの節でした。開発者が自分で実装しているコードをあまり理解せずに実装を進めてしまい、あるとき動かなくなってしまうときの背景や状況について説かれています。

いまはプログラミングを学ぶドキュメントがインターネット上にたくさんあっても、おそらくこの問題は昔よりも現在の方が起こりがちではないかと想像します。そして私自身も程度の違いはあれど実際にやっていると思います。誰かが作った優れたフレームワークやライブラリもこのことを助長します。

さらに何らかの問題や分からないことがあっても、大抵は stackoverflowqiita がその解決策として検索にヒットします。ほとんどの場合において、そこに書かれている内容を試せばうまくいくと思います。コードをそのままコピペする人もいるでしょう。それを繰り返した結果、簡単に偶発的プログラミングに陥ってしまいます。

以前ドワンゴの川上さんが仰っていた 膨大な数の二流のウェブエンジニア という言葉がずっと頭の中に残っています。

36 要求の落とし穴

この節も私は好きです。

要求は拾い集めるものではなく、掘り起こすものである

この言葉は的を射ています。要件定義はスキルの有無や頭の回転の速さに関係なく、その人の当事者意識の在り方に影響すると私は過去の経験則から考えています。つまり話を取りまとめて形式化するのが上手なことと、適切な要件を掘り起こすこととは違うということです。その当事者意識とはどうやって得られるのかの最たることも Tips に書かれています。

ユーザーの視点に立つには、ユーザーと働くこと

実際に業務でできるかと言うと難しい状況もあります。開発者がユーザーの気持ちを理解することが重要なのは誰しも経験から分かってくるように思います。ドッグフードを食べるとも言われたりしますが、可能であれば自分が開発しているアプリケーションやシステムを自分で積極的に使うことが最初の一歩になるはずです。

38 準備ができるまでは

この節では自分の勘と経験で直感的におかしいと感じたら一旦立ち止まることの大事さについて書かれています。そして、そのときにプロトタイピングがその不安材料を洗い出すことに役立つともあります。

ちょっと精神的な話になるかもしれませんが、私は無意識に考えることを意識的に活用するときがあります。難しいバグに悩まされている最中、帰ってきて寝て起きたら手がかりを閃いたという経験がある人も多いと思います。

未経験のものごとに取り組んでいると心理的に不安に思ってしまうのは仕方ないことです。そのときは分からなくてもずっと考え続けていることで、あるとき閃くんじゃないかと楽観的に信じ続けることが良い方向に働くときもあります。勉強するときもこのことは役に立ちます。いま分からなくてもずっと勉強しているとあるとき分かるんじゃないかと諦めてしまわない根拠に使えます。

誰しも分からないという状態は辛いです。その分からないストレスを下げる工夫にもなるのではないかと私は思います。

読みながらそのとき思ったこと・考えたこと

本書を読みながらツイートした内容です。余談ですが、先の所感を紹介する節を選定するときに自分のツイート分析から多くの人が関心をもってそうなものも考慮しました。ハッシュタグをつけておけば良かったと後で後悔しました (´・ω・`)

リファレンス

新装版 達人プログラマー 職人から名匠への道

新装版 達人プログラマー 職人から名匠への道

*1:この節では並列と並行の詳細には触れないとあるので、ここでも並列と並行の違いについては触れません。私の感覚的なもので非同期と並行をセットで考えるのでそのように書いています

Python と型ヒント (Type Hints) と #pyconjp

先週末は PyCon JP 2015 に参加してきました。

どうでも良いことですが、たまたま会社がシルバースポンサーになっていましたが、参加そのものは個人でした。結果的には会場に会社ブースもあったため、そこでお手伝いもしつつの参加となりました。私以外にはどうでも良いことですね。

会社のブログにもイベント参加の所感を書いたので興味があればそちらもどうぞ。

型ヒントの発表

年明けから余裕があったので型ヒントの調査をしてきました。私自身、難しいことは分からないのですが、型システムに興味が出てきたところでいろんな言語の型システムをみてコードを書いたり、その特徴を調べたりするのがいまはおもしろいです。最近は Go 言語を主に書いていて型推論を伴う静的型付け言語の強力さを実感したりしています。

そして動的型付け言語に型ヒントを入れるという逆のアプローチもやはり興味深いです。Python では mypy が成功を収め、その型アノテーションの構文が PEP 484 で標準化されました。これまでも何度か mypy についての記事を書いてきました。細かいところは変わってしまっていますが、経緯や背景は変わっていないので興味のある方は以下も参考にしてください。

そして、これまで私が調べたことの集大成として PyCon JP 2015 で発表してきた次第です。

togetter はこちら。

聴講してくれたのは100人前後だと思うのですが、発表者と聴いている人たちとの距離感が近かったので私の中では発表しやすかったように思います。

発表直前、小山さん (@) が私の真前のスペースで

ここ地べたに座って聴いても良いっすか?

と声を掛けてくれて適当に「良いですよー」と答えながら、なんか少し緊張がほぐれて発表を開始しました。

何度か聴衆席に質問を投げかけながら進めました。Pythonのパラドックス を知らない人が大半だったりと世代が変わっているなぁと実感しました。発表の中で Python3 のパラドックスみたいな冗談も言っていますが、意図としては Python3 への移行を阻むものはほとんどないはずなので新しいものを作るときは Python3 を使おうよという呼びかけです。

移行が着実に進んでいるというの以下のアンケートなどから伺えます。

型ヒントについての詳細は PEP 484 型ヒントの翻訳 を眺めてもらうのが一番良いと思います。それなりの分量がありますが、興味のあるところから読み進めるのでも良いでしょう。サンプルコードも付いているので分かりやすいです。

型チェッカー

本節では発表のときにあまり触れなかった、型チェッカーとしての mypy を使ってみた私の所感を書きます。PEP 484 では型アノテーションの構文の標準化のみで型チェッカーは付属していません。そのため、依然として mypy は自分でインストールしなければなりません。

結論から先に書くと、型チェッカーとしての mypy はまだ実用レベルとは言えないです。github の mypy プロジェクトをみても250以上の issue が報告されており、期待する動作には至っていないようにみえます。

ちなみに mypy の PyPI 上のパッケージ名は mypy-lang というパッケージ名です。mypy という名前のパッケージが既に使われているためです。ちなみにこれは wsgi フレームワークだそうです。インストールするときには間違えないようにご注意を。

mypy の最新バージョンは 2015-04-05 に 0.2 というバージョンがリリースされています。この時点では PEP 484 のドラフト状態であり、そのときに決まっていた内容、おそらくは PyCon US 2015 で一定の合意を得た内容だったのではないかと推測します。

その後、PEP 484 が正式に認可されたのが 2015-05-22 です。

なぜか7月前後の master ブランチの開発は停滞していますが、その後8月頃からまた開発がアクティブになっています。それでも Python 3.5 と同じタイミングで 0.3 をリリースできてなく、また雰囲気的にも近々リリースするようにみえません。

github から mypy のソースをクローンしてきて、発表前にいくつかサンプルコードの型チェックなどを試したりしてみました。そのときにちょっと触って気付いたこと = うまく動かないところ = 既知の issue をいくつか見つけました。

やっぱり型チェッカーって大変なんだなぁというのが素朴な感想です。issue のページで label:pep484 でフィルターしても現時点で17個あります。mypy の 0.3 がリリースされるまでは型チェッカーは様子見といったところかもしれません。

今朝 git pull して動かそうとしたら bultins が見つからないというエラーになりました。

(mypy)$ mypy tutorial.py 
tutorial.py:1: error: Could not find builtins
...

パスの問題のようです。

いま mypy にコントリビュートするチャンスがたくさんありそうです。

型チェックは必要?

発表を聴いてくれていた同僚のデータサイエンティストに発表後どんな印象を受けましたか?と尋ねてみました。すると、やはり難しいと答えが返ってきました。ジェネリクスとか、これは Python なの?といった印象を受けたそうです。ジェネリクスの話をする際、会場で Java やったことがある人?と尋ねたら8割以上、手が挙がったので「ジェネリクスとは」みたいな話を省いてどんどん話を進めたのもあるかもしれません。

発表後の質疑応答においても、例えば 共変性と反変性 とか、Python はそういうことを考えずに簡単に使えて良いのに、、、といった質問もありました。私も説明が難しいからこの内容は発表から省いたのですが、確かに型ヒントをちゃんと書こうと思ったら分かっている人しか書けない、直感的に書ける類のものではないという意味で難しいです。

型チェックを誰が必要としているか?というと、その回答は学習コストやメンテナンスコストを払ってでも実行前に型チェックしたいかどうかの動機次第かなと思います。以下の清水川さん (@) の記事で Guido 自身も PyCharm や Google でも似たようなスタブファイルを作っていたから標準化することに意義があるんだと語っています。

私の経験からだと、実行前にエラーが知りたいケースは高い SLA が要求されるアプリ、または大規模なアプリだと思います。チーム開発で日々コードを書いてコミットしていると、他の人が何をやっているか分からないために認識の違いによるバグは常に入ってしまいます。

そういったバグを見つけるために動的型付け言語はテストをたくさん書くことで一定の品質を担保してきたわけですが (もちろん静的型付け言語でもテストは必要ですが)、その規模拡大に伴ってもうしんどくなってきたというのが現状ではないかと私は思います。アプリの規模が大きくなればなるほど、テストで品質を担保するのが難しくなります。

  • 全てのテストケースを網羅できない
  • テストのメンテナンスコストがかかる
  • テストの質が開発者のスキルに依存する

型チェックはテストなのか?と言うと議論はありそうですが、型レベルの操作において不整合がないことは、人間がプログラムを書く上で失敗しやすいミスを防いでくれます。

  • 人間はコードを書き間違える (typo)
  • 人間は全てのソースコードを把握してコードを書けない
  • 人間は時間が経つとそのソースコードを書いたときの経緯を忘れる

これらの失敗に対して型チェックは有効です。自分が全てを把握できていないコードベースにコードを追加していくとき、エラーを未然に防ぐことへの安心感をプログラマーは求めている気がします。

まとめ

型ヒントを扱う型チェッカーや IDE がどのぐらい普及するか、またはさらに便利な用法が出てくるか、まだまだこの先どうなるかは分かりません。とは言え、型ヒントそのものはあくまでオプションの位置付けなので悪い作用をもたらすことは何もないと思います。私ももう少し mypy が安定してきたら、それを使うテストツールなどを作ってみようと考えています。

Python とマクロ、代数的データ型

前回はマクロの概要と Python でマクロを実装するための仕組みについて説明しました。

Python とマクロ、インポートフックと抽象構文木 - forest book

動作原理を理解した上で実際にマクロでどういったことができるのか、MacroPy というライブラリで提供されている機能をみながら考察してみます。

MacroPy の概要

Python でのマクロ実装の1つです。インポートフックでモジュール内のマクロ機能を AST 変換することで動作します。MacroPy で提供されているマクロ機能は以下のデコレーターを使って実装されています。

  • @macros.expr
  • @macros.block
  • @macros.decorator
  • @macros.expose_unhygienic

これらの仕組みを使って自分でマクロを実装することもできます。それにより Python の意味論 (semantics) の拡張を簡単にします。

リポジトリには python3 ブランチがあり、Python 3 対応が試みられているようですが、正常に動作しない機能もあるため、Python 2.7 を使う方が無難だと思います。

MacroPy が提供するマクロや機能は多岐に渡ります。またドキュメントもしっかりしているので学習にはとても良さそうです。ただ、全てを README に記載しているので目を通すだけでもなかなか大変です。以下は MacroPy の README から目次を抜き出したものです。

機能

マクロ

ユーザー定義マクロ

リファレンス

項目がたくさんあるので興味のあるところから読み進めると良いと思います。

今回は README の前半部によく登場する Case クラスという機能を提供するマクロとその背景について解説します。

Case クラス

Scala から Case クラスという機能を提供するマクロです。Case クラス について MacroPy のドキュメントで以下が引用されています。

Case Classes Are Cool - Code Commit

私が Scala をよく知らないため、この記事を読んでもよく分からなくて最初からつまづきました。Case クラスについて調べていると Effetive Scala に Case クラスについて説明があるのを見つけました。

代数的データ型としてのケースクラス

ケースクラス (case class) は、代数的データ型 (algebraic data type) をエンコードする: ケースクラスは数多くのデータ構造をモデリングするのに役に立ち、強力な不変式を簡潔なコードとして提供する。ケースクラスは、パターンマッチと共に利用すると特に有用だ。パターンマッチの解析器は、さらに強力な静的保証を提供する包括的解析 (exhaustivity analysis) を実装している。

Effective Scala 関数型プログラミング-代数的データ型としてのケースクラス

( ゜Д゜)

説明が簡潔過ぎてもっと分からなくなってしまいました。

代数的データ型 (Algebraic data type)

wikipedia:代数的データ型 という用語が新たに出てきました。Case クラスを理解する前にこの型が何なのかを調べることにしましょう。

余談ですが、以前、Python と型ヒント について調べていたときに Alex Gaynor 氏が Python の型システムについての懸念を表明し、その中で代数的データ型がないといったことも挙げられていました。

Python's type system isn't very good. It lacks many features of more powerful systems such as algebraic data types, interfaces, and parametric polymorphism. Despite this, it works pretty well because of Python's dynamic typing. I strongly believe that attempting to enforce the existing type system would be a real shame.

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

そういったやり取りも記憶に残っていて代数的データ型に興味がありました。大雑把にいまの自分の理解で要約しますが、厳密な定義は原典を参照してください。

代数的データ型とは、一般的に関数型言語にみられるデータ型で、具体的には直積型、直和型、列挙型や再帰型といったデータ型を指します。関数型言語では、これらのデータ型を使ったプログラミングが一般的であり、パターンマッチングと共にそのデータ型の表現や操作について簡潔、且つ強力に扱えます。

代数的データ型と簡潔な表現

例えば、なぜ次に学ぶ言語は関数型であるべきか - YAMAGUCHI::weblog の記事で紹介されている Boolean 式を表す型と、それらの式を評価する関数の定義が以下になります。

  • OCamlでの表現型と評価器
type 'a expr = | True 
               | False 
               | And  of  'a expr * 'a  expr 
               | Or   of  'a expr * 'a  expr 
               | Not  of  'a expr 
               | Base of  'a  
 
let  rec eval eval_base expr  = 
   let  eval' x = eval eval_base x in 
   match expr with 
   | True  -> true 
   | False -> false 
   | Base base  -> eval_base base 
   | And  (x,y) -> eval' x && eval' y  
   | Or  (x,y)  -> eval' x || eval' y 
   | Not  x     -> not (eval' x) 

これと同等のことを Python で実装してみたのが以下の記事になります。

代数的データ型とオブジェクト指向プログラミングと

代数的データ型の直和型 (後述) に相当するものは、オブジェクト指向言語においてもクラスの継承や列挙型 (Enum) で表現できます。以下は列挙型を使って表現したコードです。

# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod

from extenum import ConstantSpecificEnum

class Evaluator(metaclass=ABCMeta):
    @abstractmethod
    def evaluate(self, value): pass

class MyEvaluator(Evaluator):
    def evaluate(self, value):
        return bool(value)

class Expr(ConstantSpecificEnum):

    TRUE = 1
    FALSE = 2
    BASE = 3
    AND = 4
    OR = 5
    NOT = 6

    @overload(TRUE)
    def eval(self, evaluator, *args):
        return True

    @overload(FALSE)
    def eval(self, evaluator, *args):
        return False

    @overload(BASE)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0])

    @overload(AND)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0]) and evaluator.evaluate(args[1])

    @overload(OR)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0]) or evaluator.evaluate(args[1])

    @overload(NOT)
    def eval(self, evaluator, *args):
        return not evaluator.evaluate(args[0])

ぱっと見た直感で随分とコード量が増えて、コードの見た目 (表現) が冗長になってしまっているというのに気付くと思います。

関数型言語で代数的データ型の定義とそれを扱う処理はとても簡潔に書ける (表現できる) のに対して、代数的データ型をサポートしないオブジェクト指向言語でそういった処理を実装するのは冗長で複雑になりがちであるというのを実感する例です。

私が調べた中では、代数的データ型について語るときに言語機能としてそういったデータ型を簡潔に強力に表現できるかどうかということが論点の1つとして語られているように思います。

余談ですが、直和型を簡潔に表現するために Python 標準の enum モジュールだと機能不足だったので extenum というパッケージを作りました。extenum の機能と用途について簡単にまとめた記事が以下になります。

enum を拡張する extenum パッケージを作りました

標準の enum モジュールにはない以下の機能を提供しています。

  • 定数固定メソッド実装
  • 暗黙の列挙型メンバー
  • EnumSet

代数的データ型とパターンマッチング

代数的データ型の文脈で使われるデータ型がいくつかあります。Python におけるそれらを考察したのが以下の記事になります。

代数的データ型とパターンマッチングと

  • 直積型
  • 直和型
    • 前節の Ocaml のコードのように関数型言語では簡潔に表現できる
    • Python ではサポートしていない、継承や enum で代替できるが簡潔ではない
  • 列挙型
    • Python 3.4 から標準ライブラリとして提供されている
  • 再帰
    • データ型を定義するときに再帰的に扱う、前方参照 (forward reference) が必要

さらに代数的データ型とパターンマッチングは表裏一体な機能と言っても良さそうなので、一緒に考察することでその利点がより分かりやすくなります。

上記の記事で直積型は namedtuple に相当すると説明しています。

ここでようやく元の話に戻ってきましたが、この直積型に相当するものが Case クラスです。MacroPy では以下のように定義します。

@case
class Point(x, y): pass

MacroPy では Case クラスは以下の機能をもつと説明されています。Case クラスは @macros.decorator で実装されています。

ざっくり言うと、様々な特殊メソッド (機能) をもつクラスを自動生成してくれます。こういった何かの機能を自動生成するものをボイラープレート (boilerplate) と呼んだりするようです。詳細は README にあるサンプルコードを参照してください。

先の記事を書いた後で id:podhmoPythonのnamedtupleについて見過ごしてきたこと で namedtuple はタプルであって型ではないと言及しているのに気付きました。

これは結局、namedtupleは名前の通りtupleでしかないせいです。tupleなので型名を持っていません。したがって、同じ順序で同じ値が渡されていたものは比較でTrueになるというわけです。

先の記事で namedtuple が直積型に相当すると書いたのは厳密には間違っていて、Case クラスの __eq__ ではクラスのチェックも実装されているため、Case クラスが直積型に相当すると言った方が適切でしょう。

    def __eq__(self, other):
        try:
            return self.__class__ == other.__class__ \
                and all(getattr(self, x) == getattr(other, x) for x in self.__class__._fields)
        except AttributeError:
            return False

さて、前述した OCamlでの表現型と評価器を、MacroPy の Case クラスとパターンマッチングを使って実装してみます。

# -*- coding: utf-8 -*-
from macropy.case_classes import macros, case
from macropy.experimental.pattern import macros, ClassMatcher, _matching, switch

@case
class Expr:  # Algebraic data type
    class True_: pass
    class False_: pass
    class Base(value): pass
    class And(expr1, expr2): pass
    class Or(expr1, expr2): pass
    class Not(expr): pass

def eval_(expr):  # Pattern Matching
    with switch(expr):
        if Expr.True_():
            return True
        elif Expr.False_():
            return False
        elif Expr.Base(value):
            return bool(value)
        elif Expr.And(expr1, expr2):
            return eval_(expr1) and eval_(expr2)
        elif Expr.Or(expr1, expr2):
            return eval_(expr1) or eval_(expr2)
        elif Expr.Not(expr):
            return not eval_(expr)

def test():
    True_, False_ = Expr.True_, Expr.False_
    Base, And, Or, Not = Expr.Base, Expr.And, Expr.Or, Expr.Not
    assert eval_(Not(True_())) is False
    assert eval_(Or(And(Base(3), False_()), Not(False_()))) is True

列挙型で実装したコードよりも随分とすっきりしましたね。

Case クラスの継承 によると、@case で生成したクラスでは、継承関係をネストした内部クラスとして表現できるようです。この例では、Expr の内部クラスは Case クラスを継承することになります。

そして eval_() 関数が MacroPy の パターンマッチング のマクロで実装されています。こういった with 文と一緒に使うマクロは @macros.block で実装されています。

もはや Python の意味論ではないので何ともコメントが難しいですが、OCaml のコードによく似た表現になっていることが伺えます。そして、それでもまだ冗長であるのも否めない気はします。

感覚的なものですが、構文を変えずに意味論だけの拡張で他言語の概念を取り入れるというものの限界というのか境界というのか、そういったものが見え隠れしている気がします。

代数的データ型の補足

Python のようなオブジェクト指向言語ではほぼ馴染みがないため、最初から関数型言語で学習する方が良いとは思いますが、PyAlgebraicDataTypes というライブラリが代数的データ型とパターンマッチングの学習向けに分かりやすいと思います。以下に簡単なチュートリアルを書きました。

代数的データ型と FizzBuzz と

他にも代数的データ型そのものの概要やそれに関するデータ構造について以下の記事が参考になりました。

The Algebra of Data, and the Calculus of Mutation

代数表現とデータ型の表現の概念から始まり、直積型と直和型、再帰型の説明、後半に wikipedia:en:Zipper_(data_structure) や One-Hole Contexts といった話題も出てきます。

まとめ

MacroPy のマクロ機能と関数型言語における代数的データ型の概念について紹介しました。

  • MacroPy
    • Case クラス
    • パターンマッチング
  • 代数的データ型
    • 直積型
    • 直和型
    • 列挙型
    • 再帰

MacroPy を使って代数的データ型を表現してみました。

ボイラープレートとしての Case クラス、Case クラスを型とみなしたパターンマッチングにより、関数型言語のそれに近い表現で実装することはできました。マクロを使うことで Python の意味論を拡張できるというのを実感するには分かりやすい例でした。

Python とマクロ、インポートフックと抽象構文木

どちらがきっかけだったのか忘れてしまいましたが、wikipedia:メタプログラミングwikipedia:抽象構文木 について調べているうちに マクロ が出てきました。

私の中では、マクロと聞くと、C 言語の、プリプロセッサ (コンパイルの前処理) でコードに置き換えるものを漠然とイメージします。改めてマクロって何だったっけ?何が嬉しいのだっけ?と考えてみると、基本的なことが分かっていないことに気付いたのでマクロについて調べ直してみました。

マクロとは

wikipedia からマクロの定義を引用します。

A macro (short for "macroinstruction", from Greek μακρο- 'long') in computer science is a rule or pattern that specifies how a certain input sequence (often a sequence of characters) should be mapped to a replacement output sequence (also often a sequence of characters) according to a defined procedure. The mapping process that instantiates (transforms) a macro use into a specific sequence is known as macro expansion. A facility for writing macros may be provided as part of a software application or as a part of a programming language. In the former case, macros are used to make tasks using the application less repetitive. In the latter case, they are a tool that allows a programmer to enable code reuse or even to design domain-specific languages.

wikipedia:en:Macro_(computer_science)

ざっくり意訳すると、

コンピューターサイエンスで言うところのマクロ ("マクロ命令" の省略形) とは、定義された手続きに従い、特定の入力シーケンス (文字列のシーケンス) が出力シーケンス (文字列のシーケンス) に置き換えられる方法を指定するルールやパターンのことです。特定のシーケンス内にインスタンス化する (変換する) 対応付け処理は、マクロ展開として知られています。マクロを書くための機構 (facility) は、ソフトウェアアプリケーションの一部、もしくはプログラミング言語の一部として提供されるかもしれません。前者は簡潔な表現でそのアプリケーションを使うタスクを作るために使われます。後者はプログラマーにとってコードを再利用させたり、ドメイン特化言語を設計することさえ可能にするといったツールになります。

大雑把に要約すると、既定のコードを置き換えるルールやパターンを作ることで簡潔な表現やコードの再利用性をもたらすといったことが嬉しそうですね。

また、マクロと言えば Lisp 系の言語の特徴的な機能のように私はよく見聞きしていました。

S式は言ってみれば言語の構文木そのものです。普通の言語では、処理系のフロントエンドにある構文解析器が、「人間に優しい」文法を「機械が理解しやすい」 構文木に変換します。

Lisp:S式の理由

wikipedia:S式 という表現方法、プログラムのコードそのものをデータとして扱えるという特性により、普通の言語 *1 で必要な構文解析や抽象構文木を操作するといった処理が簡単になり、その結果としてマクロがより身近で強力なものになるのではないかと推測します。

参考までに Clojure でのマクロを使う動機付けについて書かれた記事を紹介します。


閑話休題。前置きが長くなってしまいました。先の wikipedia の続きの説明によると、Python も立派にマクロをサポートしています。

While syntactic macros are often found in Lisp-like languages, they are also available in other languages such as Prolog, Dylan, Scala, Nemerle, Rust, Haxe,[5] and Python.[6]

Syntactic macros

あれ!?そうだったっけ?と思う方もいるかもしれません。普通に Python でマクロを書いたりすることはないのでイメージできないかもしれません。

その根拠として Python でマクロを提供するライブラリとして MacroPy が紹介されています。このライブラリは様々な機能がマクロとして実装されていて、Python でマクロをどう実装するかの参照実装の1つとして良いと思います。念のため、初学者向けに断っておくと、Python における一般的なアプリ開発の用途でマクロを使う必要性は全くありません。本稿ではマクロという概念そのものを学ぶことが目的です。またマクロはその特性上、その言語におけるメタプログラミングを提供する仕組みとも密接な関係があります。そのため、マクロを学ぶことは Python におけるメタプログラミングを学ぶ上で良い題材とも言えるでしょう。

Python におけるマクロの概説

MacroPy の概要説明に分かりやすい図があるのでそこから引用します。


MacroPy は大まかに次のように動作します。

1. モジュールのインポートをフックする (インターセプトする)
2. モジュールのソースコード構文解析して AST (抽象構文木) に変換する
3. AST をトラバースして、見つけたマクロを展開する
4. 改変した AST をコンパイルして、モジュールの読み込みを再開する

(注) あるモジュール内でマクロが使われているとき、そのモジュールを直接実行することはできない (マクロが有効にならない) 。

MacroPy 30,000ft Overview

横文字がたくさん出てきました。まず用語が分からないとそれだけで嫌になってきます。それぞれの用語を1つずつ整理しながら意図している内容を噛み砕いていきましょう。

Python のモジュール

通常 モジュール は、Python のプログラム (ソースコード) を記述した xxx.py というファイルです。ここでは Python でインポートできる最小単位がモジュールであることを覚えておいてください。モジュールも Python の世界の中で扱えるオブジェクトの1つです。

歴史的に Python のインポートの API が貧弱だったことからいまの状況はやや混沌としています。私はよく知らないので簡単な紹介しかできませんが、いまは3つの方法があります。

1. __import__() 関数

import 文で呼ばれる組み込み API といったものでしょうか。インポートを制御する低レベルのインターフェースです。昔からのライブラリなどで動的にモジュールをインポートするプログラムでしばしば見かけたりします。昔は __import__ を使うしかなかったのですが、いまは importlib.import_module() を使うようにとドキュメントで推奨されています。

2. imp ライブラリ

このライブラリがいつからあるのか、どういった変遷を経たのか私はよく知りませんが、PEP 302 の仕組みを提供するライブラリの1つです。但し、ドキュメントによると 3.4 で撤廃とあるので今後は importlib へ移行されていくようです。

PEP 302 の Python バージョンが 2.3 (2002年) と明記されています。それなりに昔からある仕組みのようです。この PEP には後述するインポートフックの仕様についても記述されています。

3. importlib ライブラリ

Python 3.1 から導入されたインポートを扱う新たなライブラリです。一部 2.7 にもバックポートされています。

What’s New In Python 3.1 を眺めていて1つ気付くのは、importlib はインポート文の pure Python な参照実装だとあります。imp の C 拡張 (CPython) なところを取り除いていって、インタープリター間の移植性を高めたいといったところも狙いのようです。

と、考察した後になってから importlib の冒頭にその目的が書いてあることに気付きました。

The purpose of the importlib package is two-fold.
One is to provide the implementation of the import statement (and thus, by extension, the __import__() function) in Python source code.
(...snip...)
Two, the components to implement import are exposed in this package, making it easier for users to create their own custom objects (known generically as an importer) to participate in the import process.

importlib 31.5.1 はじめに

また後述するインポートフックのところでも出てきますが、以下の内容も頭の片隅に入れておいてください。

モジュールには、関数定義に加えて実行文を入れることができます。これらの実行文はモジュールを初期化するためのものです。これらの実行文は、インポート文の中で 最初に モジュール名が見つかったときにだけ実行されます。

6.1. モジュールについてもうすこし
インポートフック

Python のモジュールをインポートするときの処理に割り込んでごにょごにょするといったことをインポートフックと呼びます。

先ほどの PEP 302 で導入された仕組みによると、sys.meta_pathfinder オブジェクトを登録することにより、デフォルトの sys.path よりも先にその finder.find_module() が呼ばれます。そして、finder.find_module()loader オブジェクトを返し、loader.load_module() がモジュールオブジェクトを返します。

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

$ vi run.py 
# -*- coding: utf-8 -*-
def main():
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

適当なパッケージとモジュールを作り、

$ tree .
.
├── run.py
└── test
    ├── __init__.py
    ├── t1.py
    ├── t2.py
    ├── t3.py

$ head test/*.py
==> test/__init__.py <==

==> test/t1.py <==
print('I am t1')

==> test/t2.py <==
print('I am t2')

==> test/t3.py <==
print('I am t3')

インポート時に標準出力するだけのプログラムを用意します。

$ python run.py 
I am t1
I am t2
I am t3

インポートフックを実装するための finder/loader の両方の機能をもつ ImportHook クラスを定義し、インポート前に sys.meta_path に登録します。ImportHookインスタンスfind_module() が loader としての自分自身を返し、load_module() が呼ばれます。

# -*- coding: utf-8 -*-
import imp
import sys

class ImportHook:

    def find_module(self, mod_name, path=None):
        if mod_name == 'test.t2':
            print('find_module:', mod_name, path)
            return self

    def load_module(self, mod_name):
        print('load_module:', mod_name)
        path = mod_name.split('.')
        mod = imp.load_module(mod_name, *imp.find_module(path[-1], path[0:-1]))
        return mod

def main():
    sys.meta_path.insert(0, ImportHook())
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

このプログラムを実行すると以下の出力になります。

$ python3.4 run.py  # python2.7 でも実行可
I am t1
find_module: test.t2 ['/Users/t2y/work/external-repo/python/learn/import-hook/test']
load_module: test.t2
I am t2
I am t3

インポートフックが呼ばれてモジュールの検索と読み込みが行われているのが確認できました。

余談ですが、ドキュメントを見ていて sys.path_hooks というのもあるようです。この例では sys.meta_path に finder オブジェクトを登録しましたが、その finder オブジェクトを生成する呼び出し可能オブジェクトのリストを登録するようです。さらにもう1つ前の段階でもフックできるようですね。

リファレンス:

抽象構文木 (Abstract Syntax Tree)

抽象構文木構文解析構文木とデータ構造の中間的なものとして使用される。さらにコンパイラインタプリタなど(プログラミング言語処理系)でのプログラムの中間表現として使われ、コンパイラ最適化やコード生成はその上で行われる。抽象構文木のとりうる構造は抽象構文で記述されている。

wikipedia:抽象構文木

Pythonソースコード構文解析して、抽象構文木 (以下 AST) を扱うために ast モジュールという標準ライブラリがあります。ast モジュールのヘルパー関数を使うと、簡単にソースコードを AST のノードオブジェクトに変換できます。

ソースコードを見た方が分かりやすいので簡単なサンプルを紹介します。

# -*- coding: utf-8 -*-
import ast
import inspect
import sys

def func():
    for i in range(3):
        if i == 2:
            print('Value is {}'.format(i))

source = inspect.getsource(func)
tree = ast.parse(source)
print(ast.dump(tree))

実行すると以下のような AST のノードオブジェクトの dump が出力されます。手で整形するのも難しかったのでちょっと見辛いですが、どういったオブジェクト表現かという雰囲気は掴めます。後ほど、マクロ展開について考察するときのために Python の AST 表現は (知らない人には) 訳が分からない程度に覚えておいてください。

$ python3.4 ast_sample.py 
Module(
body=[FunctionDef(name='func',
           args=arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
  body=[For(target=Name(id='i', ctx=Store()),
             iter=Call(func=Name(id='range', ctx=Load()),
             args=[Num(n=3)], keywords=[], starargs=None, kwargs=None),
    body=[If(test=Compare(left=Name(id='i', ctx=Load()), ops=[Eq()], comparators=[Num(n=2)]), 
      body=[Expr(value=Call(func=Name(id='print', ctx=Load()),
                 args=[Call(func=Attribute(value=Str(s='Value is {}'), attr='format', ctx=Load()),
                   args=[Name(id='i', ctx=Load())],
                   keywords=[], starargs=None, kwargs=None)],
                 keywords=[], starargs=None, kwargs=None))],
    orelse=[])],
  orelse=[])],
decorator_list=[], returns=None)]
)

実際に ast モジュールを使ったことがなかったのと公式ドキュメントでは使い方がよく分からなかったので以下に簡単な入門記事を書きました。

ast モジュールの使いどころとしては、Python コードを処理するテンプレート、コード解析、マクロ、そういった類のツールなどが一般的です。分かりやすいサンプルとして、以下にデコレートされている関数やメソッドを調べるといったことをやってみました。

ソースコードの AST 変換とモジュール読み込み

ここまででインポートフックと AST について分かりました。

前々節のインポートフックのソースコードを修正して、あるモジュールのソースコードを読み込んで AST に変換・改変した後、その AST をコンパイルして新規にモジュールを作成するのをやってみましょう。

AST のノードオブジェクトに変更を加えるには ast モジュールの NodeTransformer を使います。ast.parse() でファイルのソースコードを AST オブジェクト (Module ノード) に変換し、NodeTransformer を継承したクラスを設けてトラバースするのが簡単です。ここでは文字列のノードオブジェクトの値を書き換えています。

class StringTransformer(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'I am StringTransformer'
        return node

...
    def handle_ast(self, file_, mod_name):
        source = file_.read()
        tree = ast.parse(source)
        transformed_tree = StringTransformer().visit(tree)
        code = compile(transformed_tree, mod_name, 'exec')
        return code

NodeTransformer は Visitor パターンで処理を実装します。Visitor パターンって何だったっけ?という方は以下の記事で復習しましょう。

そして、NodeTransformer で変更を行った AST の Module ノードオブジェクトを compile() することでコードオブジェクトを取得します。

次に置き換え用のモジュールオブジェクトを新規に生成し、先ほど変更を加えてコンパイルしておいたコードオブジェクトをそのモジュールの名前空間exec() します。これはモジュールのところでインポート時に実行して初期化されるという処理に相当します。

    def create_module(self, mod_name, file_name, code):
        mod = imp.new_module(file_name)
        sys.modules[mod_name] = mod
        exec(code, mod.__dict__)
        return mod

Python でインポートしたときに行われる処理、sys.modules への登録やコード実行などを行っています。本来は __file____package__ といった属性にも適切な値を設定すべきですが、単純なサンプルなのでここでは省きます。

前々節のインポートフックのソースコードに修正を施したものが以下になります。

# -*- coding: utf-8 -*-
import ast
import imp
import sys

class StringTransformer(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'I am StringTransformer'
        return node

class ImportHook:
    def find_module(self, mod_name, path=None):
        if mod_name == 'test.t2':
            print('find_module:', mod_name, path)
            return self

    def load_module(self, mod_name):
        print('load_module:', mod_name)
        package_name, file_name = mod_name.split('.')
        file_, pathname, desc = imp.find_module(file_name, [package_name])

        # read source and transform ast
        code = self.handle_ast(file_, mod_name)

        # create new module and exec it
        mod = self.create_module(mod_name, file_name, code)
        return mod

    def handle_ast(self, file_, mod_name):
        source = file_.read()
        tree = ast.parse(source)
        print('AST:', tree)
        transformed_tree = StringTransformer().visit(tree)
        code = compile(transformed_tree, mod_name, 'exec')
        print('compiled:', code)
        return code

    def create_module(self, mod_name, file_name, code):
        mod = imp.new_module(file_name)
        sys.modules[mod_name] = mod
        exec(code, mod.__dict__)
        return mod

def main():
    sys.meta_path.insert(0, ImportHook())
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

実行結果です。

$ python3.4 run.py 
I am t1
find_module: test.t2 ['/Users/t2y/work/external-repo/python/learn/import-hook/test']
load_module: test.t2
AST: <_ast.Module object at 0x1053c0518>
compiled: <code object <module> at 0x105313ae0, file "test.t2", line 1>
I am StringTransformer
I am t3

"I am t2" の文字列出力を AST のレイヤーで書き換えて実行することができました。

ここまでのサンプルコードではマクロ展開以外の動作、つまり MacroPy の概要説明にあるモジュール読み込みのワークフローの流れを確認しました。

マクロ展開について少し

前節のサンプルコードでは、実際にマクロを定義したわけではありませんが、StringTransformer で AST のノードオブジェクトの文字列の値を直接変更しました。冒頭で紹介した、マクロが既定コードの置き換えを目的としているといった内容を思い出してください。マクロ展開というのは、マクロというルールやパターンから AST のレイヤーで変更を行うことに相当します。つまり、マクロで実現したいことは AST で実現できるということであり、AST で実現できないことはマクロで実現できないということでもあります。

ここで Python の構文とその AST のオブジェクト表現を見比べてみます。

>>> import ast
>>> ast.dump(ast.parse("2 + 2"))
'Module(body=[Expr(value=BinOp(left=Num(n=2), op=Add(), right=Num(n=2)))])'

ある記事の言葉を引用すると、

この結果にはかなりがっかりした。Lisp なら、これと同等の構文木は (+ 2 2) である。この構文木なら誰にでも使えるが、 Python のこんな構文木をいじれるのは本当のエキスパートだけだろう。

Python にはマクロがない。

Python の構文と AST の表現が全く違うことから、マクロを書く・展開する・適用する = AST を操作するということが誰にでも実装できない、ひいては普通にマクロを書くことはないということにつながるように思います。

Karnickel

いきなり MacroPy を読み進めようとすると、複雑過ぎて挫折してしまうかもしれません。私は挫折しました。

学習向けにもっと簡単なものとして Karnickel という小さいライブラリがあります。これは Sphinx の作者としても有名な Georg Brandl 氏 *2ast モジュールとインポートフックのデモとして作ったライブラリのようです。

MacroPy の概要説明の図を使いたかったために紹介する順序があべこべになってしまいましたが、KarnickelMacroPy 同様のモジュール読み込みフローをもちます。インポートフック、NodeTransformer によるマクロ展開といった一通りのサンプルを含み、1ファイル300行ちょっとのコード量です。私は最初に Karnickel のライブラリを読み進めました。

Karnickel のマクロ

マクロ実装のデモ向けライブラリのため、マクロとして提供されている機能そのものにあまり意味はありません。

また使い方の説明が分かりにくいため (インポートフックを知っている前提)、マクロ展開のフローが分かっていないと、試しに実行するところから戸惑うかもしれません。簡単に使い方を紹介します。ちなみに KarnickelPython 3 では動きません (Python 2.6+) 。

example.macros にサンプルのマクロが定義されています。マクロは @macro デレコーターで定義するようです。

from karnickel import macro

@macro
def add(i, j):
    i + j

@macro
def assign(n, v):
    n = v

@macro
def custom_loop(i):
    for __x in range(i):
        print __x
        if __x < i-1:
            __body__

実行するときはこのマクロ定義モジュール以外に、インポートフックがある実行用ファイル (run.py) とマクロをインポートして使うファイル (example/test.py) の2ファイルが必要です (たぶん) *3

# -*- coding: utf-8 -*-
import karnickel
karnickel.install_hook()

from example.test import usage_expr, usage_block, usage_3

print 'usage_expr():', usage_expr()
print 'usage_block():', usage_block()
print 'usage_3():'
usage_3()

実行結果。

$ python run.py 
usage_expr(): 22
usage_block(): 1
usage_3():
0
loop continues...
1
loop continues...
2
loop continues...
3
loop continues...
4
loop continues...
5
loop continues...
6
loop continues...
7
loop continues...
8
loop continues...
9

詳細は追いませんが、NodeTransformer を使ってマクロ展開を実装しているのが karnickel.py で確認できます。

class Expander(ast.NodeTransformer):
    """
    AST visitor that expands macros.
    """

    def __init__(self, module, macro_definitions=None, debug=False):
        ...

まとめ

Python におけるマクロを提供する仕組み、言わばメタプログラミングについて紹介しました。

  • Python におけるマクロ
    • インポートフック
    • 抽象構文木
    • AST 変換 (マクロ展開)

インポートフックを実装してみると、インポート周りの API が分かりにくいと思うかもしれません。良く言えば癖がある、悪く言えば使いにくいといった感じでしょうか。歴史的経緯や互換性もあるでしょうし、メタプログラミングが本質的に難しいということなのかもしれません。以前からインポート周りは Python の良くないところの1つに挙げられている発表を私はいくつか見たことがあります。importlib の冒頭にも書かれているよう、もっと使いやすくなるように今後も改善されていくと思います。

次回は MacroPy で提供されているマクロの機能についても見ていきましょう。

*1:この文脈では一般的な手続き型プログラミング言語を指していると思われます

*2:基調講演者 - PyCon APAC 2013

*3:これらを1ファイルにして実行するとエラーになったのでそういう作りなんだと思います

Python と型ヒント (Type Hints)

先日、Python の静的型チェッカーとして mypy を紹介しました。

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

とか言っているうちに1ヶ月ほど経ってしまいました。

そうこうしているうちに PEP のドラフトも出てきたので区切りとしてまとめておきます。一通り調べたことを基にして書いていますが、私の誤解や勘違いもあるでしょうから怪しいところがあったら調べ直してみてください。もちろんツッコミも大歓迎です。

型ヒント (Type Hints) を導入するという提案

現時点の PEP の内容をみて簡単にまとめます (これらはまだドラフトなので今後も内容が更新される可能性があります) 。

PEP 483 では、ここで言う型ヒントは、漸進的型付け (Gradual Typing) という型システムの理論体系に基づくものであり、その型システムがどういったものかという概要を説明しています。この PEP は漸進的型付けを導入するにあたっての原則や動機付け、その参照実装として mypy の構文を使った型アノテーションの具体例を紹介しています。型システムとしての側面に着目してその背景を整理したものにみえます。

そして、PEP 484 は PEP 483 の理論を実現するための型ヒントの標準化、それらの構文を提案するもののようです。ジェネリクスや直和型など動的型付けな型システムにはなかった概念や、プラットフォーム (OS やバージョン) に特化したチェックについての提案もあったりします。この PEP は先週に提案されたばかりというのもあり、メーリングリストgithub でどういった構文や表現方法を受け入れるべきかといった議論がまさにいまも活発に行われています。

mypy から強く触発されているという冒頭の言葉もありますが、現時点の mypy では提供されていない Optional[T]Union[T, None] と見なすといった記述もあります。mypy プロジェクトのブログによると、

作者である Jukka Lehtosalo 氏もその標準化の作業にコントリビュートしていると言っています。おそらくは、型ヒントを表現するために必要なものの大半は mypy に含まれる typing モジュールに実装し、そのモジュールを Python 3.5 から標準ライブラリのような形態で提供するように推測されます。念のために補足すると、これらの PEP で提案しているのは Python の型システムそのものに大きな変更を加えるのではなく、型ヒントに必要なものをなるべく Python モジュールで実現しましょうといった取り組みです。

PEP 484 を実現するために必要な issue が mypy のリポジトリにも登録されています。

これらの PEP を読んだ方の中には、型ヒントの標準化についての提案はあるけれど、それらが型チェッカーでどう扱われるかについては何も書いていないと思われた方もいるでしょう。mypy の作者の記事によると、これらの PEP は型ヒントの標準化を推めるものであって、それをどう扱うかは依然として型チェッカー (mypy も含む) や IDEJIT コンパイラーといったサードパーティツールやプロダクトに委ねるといった方針のようです。

PEP の最後にさらっとこんなことも書いてあります。

Is type hinting Pythonic?

Type annotations provide important documentation for how a unit of code should be used. Programmers should therefore provide type hints on public APIs, namely argument and return types on functions and methods considered public. However, because types of local and global variables can be often inferred, they are rarely necessary.


The kind of information that type hints hold has always been possible to achieve by means of docstrings. In fact, a number of formalized mini-languages for describing accepted arguments have evolved. Moving this information to the function declaration makes it more visible and easier to access both at runtime and by static analysis. Adding to that the notion that “explicit is better than implicit”, type hints are indeed Pythonic .

PEP 484 - Type Hints | Python.org

翻訳するとこんな感じでしょうか。

型ヒントは Pythonic か?

アノテーションは、あるコードがどう使われるべきかという重要なドキュメントを提供します。それ故に、プログラマーはパブリックな API に型ヒントを提供すべきです。すなわちパブリックとみなす関数やメソッドの引数と返り値についてです。とはいえ、ローカル変数やグローバル変数の型は推論されるため、それらについての必要性はめったにないでしょう。


型ヒントのような類の情報は docstring で実現することも可能でしょう。実際のところ、受け取る引数を記述する形式化されたミニ言語もいくつか開発されました。この情報を関数定義へもっていくことは、実行時と静的解析時の両方においてアクセスしやすく、より見通しの良いものにします。さらに "暗黙よりも明示が良い" という考えからも、型ヒントはまさに Pythonic だと言えるわけです。

The Zen of Python からの引用はややこじつけな感もありますが、型ヒントもまた Pythonic という文化やイディオムを支えるものになっていくのかもしれません。

漸進的型付け (Gradual Typing) という型システム

PEP で参照されている漸進的型付けについてみてみましょう。Jeremy Siek 氏による 漸進的型付け の入門記事から要点をまとめます。

  • 静的型付けと動的型付けの良いとこ取りをしようといった型システムである
  • 漸進的型付けを備えた型システムでは、型を書く書かないを同一言語内で選択できる
  • アノテーションを記述すると型チェッカーにより型エラーを捕捉できる

これは前述の PEP においては以下のように説明されています。

Summary of gradual typing

We define a new relationship, is-consistent-with, which is similar to is-subclass-of, except it is not transitive when the new type Any is involved. (Neither relationship is symmetric.) Assigning x to y is OK if the type of x is consistent with the type of y. (Compare this to "... if the type of x is a subclass of the type of y," which states one of the fundamentals of OO programming.) The is-consistent-with relationship is defined by three rules:

  • A type t1 is consistent with a type t2 if t1 is a subclass of t2. (But not the other way around.)
  • Any is consistent with every type. (But Any is not a subclass of every type.)
  • Every type is a subclass of Any . (Which also makes every type consistent with Any , via rule 1.)

That's all! See Jeremy Siek's blog post What is Gradual Typing for a longer explanation and motivation. Note that rule 3 places Any at the root of the class graph. This makes it very similar to object . The difference is that object is not consistent with most types (e.g. you can't use an object() instance where an int is expected). IOW both Any and object mean "any type is allowed" when used to annotate an argument, but only Any can be passed no matter what type is expected (in essence, Any shuts up complaints from the static checker).

PEP 483 - The Theory of Type Hints | Python.org

翻訳すると、

漸進的型付けの概要

我々は is-consistent-with という新しい関係を定義します。それは新たな型 Any が適用されるときに推移的 (transitive)ではないという点を除けば、is-subclass-of の関係によく似ています。(これらの関係に対称性はありません。) もし x の型が y の型と一貫性がある (consistent) なら x を y に割り当てられます。(これを "もし x の型が y のサブクラスであるなら ..." という仮定に置き換えると、オブジェクト指向プログラミングの基礎の1つを述べています。) この is-consistent-with という関係は次の3つの規則で定義されます。

  • 型 t1 が型 t2 のサブクラスなら型 t1 は 型 t2 と一貫性がある。(但し、その逆は成り立たない)
  • Any は全ての型と一貫性がある。(但し、Any は全ての型のサブクラスではない)
  • 全ての型は Any のサブクラスである。(規則1により、全ての型は Any と一貫性があるともみなせる。)

これが全てです!詳細な説明と動機付けは Jeremy Siek 氏のブログ記事 What is Gradual Typing を参照してください。規則3はクラスグラフの根 (root) に Any を置くということに注意してください。これは object にとてもよく似ています。その違いは object がほとんどの型と一貫性がないという点のみです (例えば、int が期待されるところで object() のインスタンスは使えません) 。言い換えると、Anyobject の両方とも、ある引数をアノテートするときに使うには "任意の型を許容する" ことを意味しますが、どの型が期待されるかに関わらず引数に渡せるのは Any のみです (本質的には、Any は静的チェッカーからのメッセージを止める) 。

一貫性がある (is-consistent-with) という関係と Any という型が登場しています。この規則によると、全ての型の基底クラスであり、全ての型と一貫性のある任意の型として Any という型を定義しましょうとあります。ここで、この規則をよく見直してみると、型としての objectAny をあえて分けているのは何のためだろう?という疑問が出てきました。一見すると型としての objectAny の条件を満たせそうにもみえます。PEP には object との違いは一貫性の有無しか書いていません。

メーリングリストでのやり取りを検索してみたところ、おそらくは以下の操作に対する扱いが最も大きな違いではないかと推測します。

Also, consider the important difference between Any and object. They are both at the top of the class tree -- but object has *no* operations (well, almost none -- it has repr() and a few others), while Any supports *all* operations (in the sense of "is allowed by the type system/checker"). This places Any also at the *bottom* of the class tree, if you can call it that. (And hence it is more a graph than a tree -- but if you remove Any, what's left is a tree again.)

[Python-ideas] Type Hinting Kick-off

つまり、object 型はほとんどの操作をサポートしないけれど、Any は全ての操作をサポートするという点です。

def func(x: object) -> int:
    return x + 1  # object 型は + (__add__) という操作をサポートしない

こういったアノテーションの型チェックを行うときに具象型でもある object は型チェッカーからみたら扱いにくいのかもしれません。

>>> object() + 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'object' and 'int'

漸進的型付けの入門記事では、漸進的型チェッカーが不明な型を 動的型 として扱うときに、そういった動的型に対するアップキャスト (派生クラスから基底クラスへのキャスト) とダウンキャスト (基底クラスから派生クラスへのキャスト) の両方を許容するというのが際立った特徴であると説明しています。そのため、サブクラスの関係は 動的型 をアップキャスト・ダウンキャストすることにより同じ型にキャストされてしまい、型チェックが機能しなくなるとあります。

そこで is-consistent-with という関係をもって型チェックするために上述した3つの規則を定義しました。ある型に対するサブクラスと Any は一貫性があると定義することにより、この問題を回避するというのが狙いです。そして object 型は全ての型の基底クラスとなるので一貫性がないという規則が導かれる、ということでしょうかね。

漸進的型付けを導入している言語として wikipedia には以下が紹介されています。ActionScript がこのカテゴリーに含まれるんだというのを知るとそう目新しいものでもないんだなと自身の勉強不足を実感しました。

Examples of gradually typed languages include ActionScript, Dart, Dylan, Hack, Perl 6, Typed Racket, Typed Clojure, TypeScript, and mypy (a static type checker for Python).

wikipedia:en:Gradual_typing

ちょうど Dylan という言語コミュニティでも漸進的型付けについての記事が投稿されていました。

Python の型システムに対する懸念

Flask などの開発者として知られる Armin Ronacher 氏の記事から紹介します。

彼はフレームワークやライブラリの開発経験から API の設計において型システムがどういったものであることを望むかという視点を述べています。いまの動向から未来のプログラミング言語は、強力な型システムを備えていて、なお柔軟性や生産性を保持するといったものになると考察しています。Python という言語は、そのインタープリターの実装である CPython の最適化や歴史的経緯により課題を抱えています。具体的には C 言語側か Python 側かのどちらで実装されるかで型意味論が異なるという状況になっており、そのことは PyPy といった他処理系の実装や API 設計に悪影響を及ぼすと懸念を抱いています。

過去または現在も抱えている CPython の型システムの不明瞭さから、Python に型アノテーションを導入することよりも型システムをより強力なものに改善すべきだというのが彼の想いのようです。とはいえ、これから Python の型システムを改善しようというのは多大な時間と労力がかかることから現実的には無理だろうというのも理解しています。mypy や漸進的型付けへの言及はあまりないことから、型アノテーションの導入には興味がないといったようにみえます。

以前の mypy を紹介した記事においても PyPy のコア開発者である Alex Gaynor 氏も同様のことを示唆していました。

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
mypy で静的型付け Python プログラミング - forest book

Python の作者である Guido van Rossum 氏からのメーリングリストでのやり取りをみる限り、互換性を崩さずに型アノテーションを導入していこうといった姿勢が伺えました。現在の Python ほどの規模のユーザーコミュニティになると、Armin 氏や Alex 氏が指摘するような型システムの改善という、影響範囲の大きそうな改善を期待するのは難しいのかもしれません。その視点からも mypy で実装された型アノテーションが既存の Python 3 の構文としてそのまま実行可能であるというのは特筆すべきことなのだと思います。

アノテーションと Composability の考察

Andrew Montalenti 氏による記事を紹介します。

彼は、関数アノテーションが導入された当時からアノテーションに対する Composability *1 が欠けていると指摘しています。関数アノテーションには階層化の機構がなかったため、フレームワークによって記述方法が異なったり、そのことが可読性を落とすものになり得ると考えているようです。

例えば、以下のような関数アノテーションの定義方法を比較した場合、

def foo(
    *args: {"doc": "arguments", "type": list}, 
    **kwargs: {"doc": "keyword arguments", "type": dict}): \
    -> {"doc": "a bar instance", "type": Bar}

アノテーションデコレーター を使う Pyanno: Python Annotations の例を紹介しながら

from geometry import Point

@returnType(float)
@parameterTypes(Point, Point)
def getDistance(p1, p2):
    """
    getDistance() calculates the distance between two points.
    """
    ...

一目瞭然であると説明しています。

彼の提案としては、アノテーションを階層化する仕組みと基本的な慣習を提供するというものであったようです。(アノテーションの) 構文を変えずにその上に composition 層を設けるというのであれば、それはコードの明瞭さにはつながらない。そして、いまはアノテーションの用途をドキュメント利用に限定した方が良いのではないかと指摘しながら、mypy の構文は簡潔で表現力のあるものだという点も認めていて、議論の開始点としては良さそうだとも述べています。

最後に

But let’s remember: simple is better than complex — and practicality beats purity!

» Python annotations and type-checking

という The Zen of Python からの言葉で締めくくっています。

Composability という概念

Andrew 氏の記事に対する lawrence 氏のトラックバックも読んでみました。彼は Composability が指すものはこういうものではないかと考察しています。

Python に対する mypy は、実行時にプログラムの動作に影響を与えないため、Clojure に対する Typed Clojure とよく似ています。動的型付き言語に型チェックを追加することは、形式的ではなくユニットテストに必要なもののを多くをアサートできるのに加え、ドキュメントとして役立つので、一定の間違いを防げるというのが彼の経験談のようです。さらに composable とはどういうものだろう?という問いに Shen という実験的言語の機能から、真に composable な型システムは、ある型の規則が別の新たな型定義の一部に使えるものだと説明しています。

最後に Andrew 氏の意味する composable を誰にでも分かるように言い換えると、Clojure ではデータ型の定義とドキュメントとしてのアノテーションの定義は別になっており、その両方を兼ねようとしている Python の型アノテーションは悪い考えだと締めくくっています。

型システムにまつわる用語

型システムについて調べているときに用語が分からなくて苦労したので少し整理しておきます。

選択的型付け (Optional Typing, Optional type systems)

漸進的型付けと関連して似たような意図を表すのに使われるそうですが、厳密には違う定義のようです。基本的には wikipedia の受け売りです。

大きな違いは漸進的型付けは同一言語内で型の有無を選択しようという意図に対して、選択的型付けは型システムの選択と言語の選択を独立させて、必要に応じて言語内にモジュールであるかのように型システムを組み込むことを示唆しています。但し、現実的には型がその言語の動作に影響しないという要件を実現するのは難しく、例えば、クラスベースの継承はできないことになってしまうとあります。

型ヒント (Type Hinting, Type Hints)

私がググった限りでは、厳密な定義をみつけることはできませんでした。おそらくは言葉通りの用語の意味でしかないと思います。

Clojure が型ヒントと選択的型付け (または漸進的型付け) という2つの用語を使い分けています。

Clojure には言語機能として型ヒントの仕組みがあります。これは実行時のリフレクションを避けることでプログラムの最適化を行うことを目的としていて、型エラーを捕捉するといった用途には使えません。

(defn len [x]
  (.length x))
 
(defn len2 [^String x]
  (.length x))
 
user=> (time (reduce + (map len (repeat 1000000 "asdf"))))
"Elapsed time: 3007.198 msecs"
4000000
user=> (time (reduce + (map len2 (repeat 1000000 "asdf"))))
"Elapsed time: 308.045 msecs"
4000000

Python の PEP でも型ヒントという用語を使っていますが、いまのところ、最適化には使われません。型アノテーションをドキュメントもしくは型チェッカーのための用途だと明言して型ヒントと呼んでいるため、Clojure で言う型ヒントと Python で言う型ヒントは異なる用途を指す用語となっています。

そして、選択的型システム (または漸進的型付け *2 ) を導入する仕組みとして Typed Clojure (Python でいう mypy) があります。ann というマクロで関数アノテーションを指定できるようです。

(ann add [Number -> Number])
(defn add [x]
  (+ x 1))

リファレンス:

まとめ

まとめられないですね ... (´・ω・`)

今回扱った話題の中には、型システム、構文、コミュニティなどプログラミング言語に関するいろんな知見を含んでいました。いくつか記事を読んだだけでは自分の中に明確なモデルを構築できなかったのと、自分の解釈が正しいかどうかを検証するのが難しいというのも分かりました。それでも良かったことは、他のプログラミング言語について知るきっかけになったことです。

Python へ型ヒントのための構文や仕組みを導入すること自体は、私が調べた限りでは、一定数の支持は得ていて積極的に進められているようにみえます。Python の型システムに懸念があるというのは、おそらくはその通りなのでしょうけれど、型ヒントの導入とは直接関係するものではないので分けて考えた方が良いように思いました。

*1:一般的には式や関数を組み合わせてプログラムを作るという特性を指すようです。

*2:README には Gradual typing in Clojure, as a library. とあるので厳密な定義を意図しているわけではなさそうです。

アリエル・ネットワークを退職しました

今月末をもって2年4ヶ月働いたアリエル・ネットワークを退職することにしました。

あるとき同僚とお昼ご飯を食べていて、私より後に入社した2人の同僚が私のブログを読んで入社を決意したという話を聞きました。

なんか書いた責任みたいなのも感じたため、辞めましたというのを書いておこうと思った次第です。

アリエルは良い会社だと思います。社員数が100人に満たない会社なので個人の裁量は大きいですし、周りの人や身近な部署が何をやっているのかが見通せて、組織間の横串も通しやすいです。開発者の視点からみたとき、一緒に働いている同僚に嫌な人がいなくて、同僚の技術スキルが高いというのが魅力的です。

Be the Worst

cLabs

という言葉があります。私にとってのアリエルの開発部はそんな場所でした。

いずれマネージャーをやってほしいという展望で採用されていましたが、結局は開発ばかりやっていました。お仕事なので良いことも悪いこともありますが、ある種の閉塞感があって、モチベーションの低い状態で働くよりは潔く辞めることにしました。

なんとなくいま思うのは、良いマネジメントというのはその世界を広げることなんじゃないかという気がします。

やったこと

開発したもの

プロダクトのコンポーネントのうち、5つほどコンポーネントオーナーとして開発に携わりました。

以下の3つのコンポーネントは、私がプロダクトである開発プラットフォーム上にスクラッチから開発したコンポーネントです (外部からみえるもののみ紹介) 。ようやくお客さんに使われ始めてこれからユーザーフィードバックがくるのにとか、あの機能も作りたかったなと心残りはあってすごく残念な思いがあります。またそれを引き継ぐ同僚へは迷惑をかけることになるので申し訳ない限りです。

1. 動画ストリーミング機能

Red5リポジトリgoogle code から github へ移行された後、Windows インストーラーの生成スクリプトRPM パッケージの生成スクリプトコントリビュートしていました 。それが認められて、数日前に Red5-Installer リポジトリへのコミット権が付与されました。

せっかくの申し出なので快諾しました。今後もできる範囲で何かしら貢献していこうと思います。

2. 外部システムとのデータレプリケーション機能

  • アプリレベルの、完全内製で作ったもののため、外部に出せるものがなかった ... (/ _ ; )

3. システム監視ツール

インターンシップ生のメンター

毎年1ヶ月半ほど、wikipedia:インド工科大学 という大学から3回生のインターンシップ生がやってくるのでその面倒をみました。親会社が面接やらライブコーディングやらで選抜した学生のみを招待しているのもあり、とても優秀な学生たちでした。もっとうまく指導してあげられたら、という反省が多い経験でした。

私はほとんど英語を話せませんが、1年目のときは自分の英語力が低いからうまくいかないと自己嫌悪になりつつ考え込んでしまっていました。2年目はその失敗を反省して、周りにも協力を仰いだり、日々彼らの進捗にあわせてカリキュラムを作ったりしていました。そのうちに英語とか関係なく、そもそも就業経験のない21歳の学生に対して、会社での業務開発にいきなり入っていくという高いハードルをいかに手引きするかということの方がずっと大事だと気付きました。

英語力が低いことでより高次の会話ができないのは事実だけれど、だからと言ってコミュニケーションが取れないというのはちょっと違うんじゃないか。日本語でやり取りしていても、開発プロジェクトで認識違いが普通にあります。インターンシップ生とやり取りも本質的にはそれと同じだと気付いて、それからはずいぶん自分の心持ちが楽になったことがありました。

Trac のインフラ管理

もともとは社内に Pythonista が少ないというのもあり、私が管理した方が良いだろうと自主的に始めました。普段の業務での開発は JavaJavaScript がほとんどで、たまに PythonRuby といったスクリプト言語ツールを作るといった感じでした。業務でそこそこの規模のサーバーサイド開発をしていると、たまに息抜きしたくなるんです。

そこで Python ですよ!

Trac という、Python が書ける対象があったのでストレス解消的な位置付けでプラグインを作ったりしていました。Trac の良さの1つにプラグラインが簡単に開発できるという利点があります。気分転換にちょっと作って、社内で運用しているからユーザーフィードバックもすぐに来て、自分も便利になって、なんかすごい楽しい!みたいなっ ... 業務のイテレーション開発の隙間に気分転換でやっていたのがうまくまわっていたように思います。

良かったこと

働いていて一番何をしたかと思い返すと、とにかくソースコードを読んだことだろうと思います。

コードを読んで読んでも分からないことがあったり、見当違いのところを無駄にデバッグして遠回りしたり、すごい人たちはどうやってソース読むんだろうと先輩の背中から見てたり、とにかくコードを読むという体験が私にとって一番良かったなぁと振り返って思うことです。

忘れる体験

どこかの雑誌の記事で"米国の平均的なプログラマーが把握できるソースコードの量は1万行だ"というのを読んだことがあります。

ひらメソッド - LinuxKernelHackJapan

プロダクトのコードベースの規模が大きくなると本当に忘れるんです。ひどい場合は、例えば1年前に自分が修正していたという事実すら忘れていることもありました。そうすると、当たり前の話ですが、忘れていてもコードを読み直してその意図を理解できるかという可読性の重要さに気付けたわけです。

頭で理解しているのと実際の体験から得られるものの差異みたいなのがちょっと分かって良い経験でした。

直せない体験

他の人の書いたコードを自分のコードと同じくらいよく理解することはできない。どんなに熱心に読んだとしても、それは単に読んだというに過ぎず、書いたわけではないのだ。だからコードのある部分が複数の人によって書かれていたとすると、それは誰によっても一人の人に書かれたコードほど深く理解されることはない。


そして他の人もいじっているコードは、安全に再デザインすることができない。

頭の中にプログラムを入れる

他人が開発したコンポーネントを引き継いで1年半ぐらい保守開発やサポートもしました。

そうしていると、そのコンポーネントの全体像や振る舞いは把握できるようになって、この仕組みは潜在的に不具合になりやすいとか、ここは汎用的な仕組みに置き換えるべきとか、いくつか抜本的にリファクタリングした方が良いというところは分かってきます。

けれど、これがまた再デザインというレベルでは結局手をつけられませんでした。

自分が作ったものでないと、(前任者がいない状態で) コードからいまに至る変遷 (歴史的経緯) とその意図を完全に把握するのは困難です。チケットやコミットログをちゃんと書いてないのも論外です。

いわゆる「動いているコードは触るな」ですね。

具体的には以下のようなものを初期実装者がやっておかないと、他人が正しく再デザインしようというモチベーションになりにくいと実感できました。

  • テストを書く
  • 仕様を書く (ドキュメント)
  • 依存関係を小さくする
  • 意図を明確にする (可読性)
  • がんばって作り込み過ぎない

読めない体験

code is read much more often than it is written.

PEP 8 - Style Guide for Python Code | Python.org

Python 2 では文字列を str と unicode という2つのデータ型で扱いますが、文字コードを扱う処理のベストプラクティスとして、外部との境界で文字コードエンコード/デコードを行うというものがあります。コード内では全て unicode 文字列を扱い、(ファイルやネットワークなど) 入出力のときに適切な文字コードに変換します。このプラクティスを無視して、コード内のあちこちで文字コードエンコード/デコードをやっているツールがありました。

そういうツール文字コード周りの問い合わせがくると、コード上のある時点で str と unicode のどちらのデータ型なのかの判別が難しく、あちこちに型チェックのコードが散らばり、結果的にデータ型や文字コードに依存した処理も所々に実装され、上述したベストプラクティスに従うにはツール全体をリファクタリングしないといけないという、どうしてこうなった状態が出来上がってしまっていたわけです。

Python みたいな言語だと、そんなコードを読むより実行してデバッグした方が確実だと読まなくなりました。毎回コードを読み始める度に「またこれか、、、」みたいな気持ちになって辛いわけですね。

これは顕著な例ですが、たぶん可読性の悪さと設計の良し悪しはそれなりに因果関係があるのでしょう (そういう理論があれば教えてください) 。もちろん最初から良いものを作れというわけではなく、可読性が悪かったらなにかが間違っているんだと直感的に信じて、どんどん直していく姿勢をもつ方が結果的に良いものができるような、何となくそう思うようになりました。

まとめ

そんな感じでお仕事ばっかりやってたのでしばらくは息抜きしようと思います。

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

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 言語で普及した訳語で型システムでは直和型という訳語の方が一般的な表現のようです