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

Python3 プログラミング勉強会の補足

先日、Python3 プログラミング勉強会 を開催しました。

以下に資料の中で引用したサンプルコードを置いておきました。

ちなみにこういったサンプルコードをアップする前に pep8 と flakes と doctest のテストを実行しておけば typo してツッコミを受けることもありません。例えば pytest 使えば、以下のように簡単にできます。

$ py.test --pep8 --flakes --doctest-module --clear . 
============================= test session starts ==============================
platform linux -- Python 3.4.0 -- py-1.4.20 -- pytest-2.5.2
plugins: flakes, pep8, cache
collected 39 items 

algorithm_set1.py ....
generator_sample1.py ...
generator_sample2.py ......
generator_sample3.py .......
lru_cache_sample1.py ....
singledispatch_sample1.py ....
super_sample1.py ....
test.txt .
with_sample1.py ......
========================== 39 passed in 0.15 seconds ===========================

ページ14: バージョニングのお話

勉強会ではスライドを飛ばしたけど、バージョニングのスキームがすごいことになってます。

[N:]N(.N)*[{a|b|c|rc}N][.postN][.devN]

RPM 以外で Epoch をバージョニングのスキームに組み込んでいるのを私は初めてみました。PEP440 の提案者が Nick Coghlan (@) で、プロフィールをみると Red Hat で働いている開発者だったので元ネタは RPM なのかもしれません。あと、Python の3番目のバージョンをマイクロと呼ぶのも知りませんでした。

ページ16: setuptools の原罪と贖罪

このスライド1枚作るのに何時間もかけて情が移ってしまったために、このスライドを力説し過ぎて時間を浪費してしまいました。この資料の中で一番価値のあるスライドだと思っているいるので、何かに困ったときに見返してください。

ページ17: パッケージングと pbr

あー、pbr の話ももっとしたかったです。これだけで勉強会しても良いぐらいです。OpenStack プロジェクトで何が起きているか、みたいなのは今後の Python の未来に大きな影響を与えるような気がします。

簡単に紹介すると、setup.py にパッケージ情報を書かずに setup.cfg に Metadata2.0 のフォーマットでパッケージ情報を記述できます。

$ git clone https://github.com/openstack-dev/pbr
$ cd pbr/
$ vi setup.py
import setuptools
from pbr import util

setuptools.setup(
    **util.cfg_to_args())
$ vi setup.cfg 
[metadata]
name = pbr
author = OpenStack
author-email = openstack-dev@lists.openstack.org
summary = Python Build Reasonableness
...

git の履歴から AUTHORS や ChangeLog も自動生成してくれます。git のコミットログから ChangeLog を作ってくれるのは結構便利かもしれませんね。コミットログの最初の行を ChangeLog のサマリとして扱ってるようにみえます。マージのコミットログは無視してたりして賢いです。

$ workon pbr3  # pbr や関連ツールをインストールしておいた仮想環境を使う
(pbr3)$ python setup.py sdist
(pbr3)$ head AUTHORS 
Alex Gaynor <alex.gaynor@gmail.com>
Andrew Bogott <abogott@wikimedia.org>
Angus Salkeld <asalkeld@redhat.com>
Anthony Young <sleepsonthefloor@gmail.com>
Attila Fazekas <afazekas@redhat.com>
Ben Nemec <bnemec@redhat.com>
Bhuvan Arumugam <bhuvan@apache.org>
Brian Waldon <bcwaldon@gmail.com>
Chang Bo Guo <guochbo@cn.ibm.com>
ChangBo Guo(gcb) <eric.guo@easystack.cn>
...

(pbr3)$ cat ChangeLog 
CHANGES                                                                        
=======                                                                        
                                                                               
* Allow examining parsing exceptions                                           
* Update integration script for Apache 2.4                                     
* Restore Monkeypatched Distribution Instance                                  
* Register testr as a distutil entry point                                     
* Check for git before querying it for a version                               
* Allow _run_cmd to run commands in any directory                              
* Make setUp fail if sdist fails                                               
* Permit pre-release versions with git metadata                                
* Un-nest some sections of code                                                
                                                                               
0.8.2                                                                          
-----                                                                          
                                                                               
* Remove --use-mailmap as it's not needed                                      
* Fix typos in docs                                                            
                                                                               
0.8.1                                                                          
-----                                                                          
                                                                               
* pbr/testr_command.py: Add logging                                            
* Documentation updates                                                        
* Fixed a typo in the documentation                                            
* Make source configurable when using --coverage                               
* README.rst: tweaks                                                           
* Format autoindex.rst file properly                                           
* make pbr use hacking directly   
...

(pbr3)$ git log
commit e1b98f578a0f94a791210dd48530e2fed43fbe61
Merge: fa17f42 6541911
Author: Jenkins <jenkins@review.openstack.org>
Date:   Mon Jun 30 17:34:07 2014 +0000

    Merge "Check for git before querying it for a version"

commit fa17f42d7d195dab77c042ac4881ac63bfb638f3
Author: Robert Collins <rbtcollins@hp.com>
Date:   Mon Mar 17 09:46:58 2014 +1300

    Allow examining parsing exceptions.
    
    Having to put breakpoints in pbr to diagnose issues is bad for dealing
    with reports from users.
    
    Change-Id: Ifecf4c4e4bb5955e0e5feb4bf5b5b85150b08ebe

commit d815366577a7dbb7a8dab66ab105e2803af4c007
Merge: ec1009c bdb0191
Author: Jenkins <jenkins@review.openstack.org>
Date:   Fri Jun 27 02:29:12 2014 +0000

    Merge "Un-nest some sections of code"

commit ec1009cf197ed555b29b6f226adb300914ea5bd0
Author: Sean Dague <sean@dague.net>
Date:   Thu Jun 26 08:22:47 2014 -0400

    Update integration script for Apache 2.4
    
    Apache 2.4 requires site configuration files to have a ".conf"
    extension, and Apache 2.2 does not want the extension. Add logic to
    figure out the right name for the file so we can run the tests against
    both versions of Apache.
    
    Fixes bug: #1334326
...

テストランナーに tox と testr を使います。setuptools の拡張コマンドに testr オプションが追加されています。デフォルトでテストを並列実行 (CPU の数かな?) してくれるようです。

(pbr3)$ python setup.py testr
running testr
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ . --list 
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpe9wwqlo3
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpbwa1_yy8
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpqv7w0s4v
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpqdbg34vv
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpkd169eki
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmpch2tkra0
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmp6h4ef3mr
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  --load-list /tmp/tmp6407izna
Ran 57 tests in 2.529s (-0.726s)
PASSED (id=9, skips=8)

(pbr3)$ python setup.py testr --testr-args --concurrency=1
running testr
running=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ .  
Ran 57 tests in 9.140s (-0.065s)
PASSED (id=7, skips=8)

ページ21: Pythonic なコード

エキスパート Python プログラミングから引用しました。

Pythonic とは、小さなコードパターンに最も効率的なイディオムを使ったコーディングや構文

最近、他人の Python コードを読んでいて「何かおかしい」と違和感を感じるときは、この Pythonic の概念を大事にしてコードを書いているかどうかの違いによるもののような気がしてきました。OSSフレームワークやライブラリを使うときにも、その開発者の意図に共感できると親しみがもてたりするものです。

Pythonista というと、一貫性にこだわる怖い人たちなんでしょう?のイメージがあります。もちろんそういった場面にもよく遭遇しますが、

Although practicality beats purity. ( とはいえ、純粋さよりも実用的であること )

  • PEP8 の一節

A Foolish Consistency is the Hobgoblin of Little Minds

とか言っていて、一貫性にこだわり過ぎるのもよくないという戒めもあります。こだわりは強くても極論はよくないといった感じですかね。

ページ23: functools.singledispatch

汎用関数が気持ちよく書ける、デコレーターの応用例の紹介をしました。
勉強会でも singledispatch をコードリーディングしたのですが、ソースもシンプルなのでデコレーターの実用例としておもしろいと思います。

ページ33: Python 2.4 との互換性を気にする

これを気にしなくなって Python3 に感激したのも事実の1つです。

標準モジュールがなかったり、機能のバージョン毎の差異を吸収するように実装したものがいくつかありました (ここにサンプルコードを置きました) 。2.4 対応を考慮しながらコードを書くと、常にこういったことに悩まされながらコードを書いているという例です。あくまで自分の用途にあわせた実装なのでサンプルコードは参考程度にみてください。

  • json の読み込み
  • itemgetter の複数引数の対応
  • datetime.strptime の互換関数

ページ34: 集合型 set の応用例

これもスライドを飛ばしたけど、集合型をこんな用途にも使えるんだなぁと感心したスニペットです。アルゴリズムを勉強したくなる気を起こさせるコードでした。

def has_invalid_fields_loop(fields):                                             
    """                                                                          
    >>> has_invalid_fields_loop(['foo', 'bar', 'foo', 'test'])                      
    True                                                                            
    >>> has_invalid_fields_loop(['foo', 'bar'])                                     
    False                                                                           
    """                                                                             
    for field in fields:                                                            
        if field not in ['foo', 'bar']:                                             
            return True                                                             
    return False                                                                    
                                                                                    
                                                                                    
def has_invalid_fields_set(fields):                                                 
    """                                                                             
    >>> has_invalid_fields_set(['foo', 'bar', 'foo', 'test'])                       
    True                                                                            
    >>> has_invalid_fields_set(['foo', 'bar'])                                      
    False                                                                           
    """                                                                             
    return bool(set(fields) - set(['foo', 'bar']))

ページ35: super はスーパーな話

簡単な分かりやすいサンプルコードを提示したつもりだったのに、絶賛大ブーイングを受けたサンプルコードがこれでした。こんな複雑なことをする意図が分からない的な。

メソッド解決順序(MRO) がとても分かりやすいけれど、全てのクラスが object を継承するようになったため、どんな多重継承を行っても必ずダイヤモンド継承になる。多重継承が本当に必要かどうかの議論はもちろんあって良いですが、mixin 的に機能を扱いたいときとか、普通にその MRO や移譲先を意識して設計するでしょうし、サブクラスで拡張することを意識してコア機能作ったりするんじゃないかなぁとも思います。フレームワークプラグインのような、それぞれの開発者が異なるケースでもたくさん応用例がありそうに思いますが、ちゃんと調べないと分からないぐらいにはややこしいです。

後半で時間がなかったからちゃんと背景を説明をしなかったせいでもあるけれど、もっと私も勉強しないといけないです。

ページ42: The Hacker's Guide to Python

Python WeeklyThe Hacker's Guide to Python が紹介されていて、機会があったら読もうと思っていました。ちょうど勉強会するからと読み始めたらおもしろくて、この本を読んだために勉強会の内容もかなり変わってしまったぐらい影響を受けました。本当は AST やパフォーマンスの章も紹介したかったけれど、準備が全然間に合わなくて断念しました。

まとめ

当初は社内の Python ツールリファクタリング的な話をしながら Python3 楽しいよという話をしようと構想していました。いろいろ調べたり、The Hacker's Guide to Python を読んだりしているうちにどんどん変わっちゃってこんな感じになってしまいました。せっかく資料をまとめたので何かの役に立ったら嬉しいです。

Ubuntu 14.04 で vim パッケージをリビルドする

Ubuntu 14.04 で vim をインストールすると python3 インターフェースが無効になっています。python3 インターフェースを有効にするために deb パッケージをリビルドしてみます。vim拡張機能をチェックするには以下のようにバージョンを表示します。

$ vim --version  # vim.gtk を使ってます
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Jan  2 2014 19:39:59)
適用済パッチ: 1-52
Modified by pkg-vim-maintainers@lists.alioth.debian.org
Compiled by buildd@
Huge 版 with GTK2 GUI.  機能の一覧 有効(+)/無効(-)
+acl             +farsi           +mouse_netterm   +syntax
+arabic          +file_in_path    +mouse_sgr       +tag_binary
+autocmd         +find_in_path    -mouse_sysmouse  +tag_old_static
+balloon_eval    +float           +mouse_urxvt     -tag_any_white
+browse          +folding         +mouse_xterm     +tcl
++builtin_terms  -footer          +multi_byte      +terminfo
+byte_offset     +fork()          +multi_lang      +termresponse
+cindent         +gettext         -mzscheme        +textobjects
+clientserver    -hangul_input    +netbeans_intg   +title
+clipboard       +iconv           +path_extra      +toolbar
+cmdline_compl   +insert_expand   +perl            +user_commands
+cmdline_hist    +jumplist        +persistent_undo +vertsplit
+cmdline_info    +keymap          +postscript      +virtualedit
+comments        +langmap         +printer         +visual
+conceal         +libcall         +profile         +visualextra
+cryptv          +linebreak       +python          +viminfo
+cscope          +lispindent      -python3         +vreplace
...

自分で vim のソースをダウンロードしてきてビルドする方法もありますが、なるべくディストリビューションのもの (と同じ設定) を使いたいと思う人もいるでしょう。ここでは deb パッケージをリビルドすることで python3 インターフェースを有効にしてみます。

前準備

まずビルドに必要な依存パッケージをまとめてインストールします。私の環境では以下のパッケージを追加するとビルドできましたが、これはそれぞれの環境によって違うかもしれません。

$ sudo apt-get build-dep vim  # vim のビルドに必要な依存パッケージをインストール
$ sudo apt-get install python3-dev ruby-dev tcl8.5-dev  # vim の拡張機能のビルドに必要な依存パッケージをインストール
$ sudo apt-get install devscripts  # deb パッケージのビルドツールをインストール

次にソースをダウンロードします。

$ mkdir /tmp/vim-build
$ cd /tmp/vim-build/
$ apt-get source vim
$ ls
vim-7.4.052  vim_7.4.052-1ubuntu3.debian.tar.gz  vim_7.4.052-1ubuntu3.dsc  vim_7.4.052.orig.tar.gz
$ cd vim-7.4.052
  • vim の configure オプションを調べる
$ ./configure --help
ビルド

バージョン番号もここで指定したものに置き換えられるようです。ディストリビューション名やメールアドレスなど、適切な内容を changelog に書かないとビルド時にエラーになります。

$ debchange -i
vim (2:7.4.052-1ubuntu4) trusty; urgency=medium
                                                                               
  * enabled python3 interface.
                                                                               
 -- t2y <email>  Sat, 26 Apr 2014 18:43:45 +0900
  • rules ファイルの編集

python3 インターフェースを有効にするための configure オプションを追加します。また vim-nox のテスト中にエラーになったのでここではビルド後にテストを実行しないように編集しています。

$ vim debian/rules
--- orig-vim-7.4.052/debian/rules	2014-04-26 15:42:19.264743944 +0900
+++ vim-7.4.052/debian/rules	2014-04-26 16:42:47.818878711 +0900
@@ -27,11 +27,11 @@
 BUILDER := $(shell echo $${DEBEMAIL:-$${EMAIL:-$$(echo $$LOGNAME@$$(cat /etc/mailname 2> /dev/null))}})
 
 MAKETEST := no
-ifeq ($(DEB_HOST_GNU_TYPE),$(DEB_BUILD_GNU_TYPE))
-  ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
-    MAKETEST := yes
-  endif
-endif
+#ifeq ($(DEB_HOST_GNU_TYPE),$(DEB_BUILD_GNU_TYPE))
+#  ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+#    MAKETEST := yes
+#  endif
+#endif
 
 ifneq (,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
     NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
@@ -108,7 +108,7 @@
 ALLINTERPFLAGS+=--disable-mzschemeinterp
 ALLINTERPFLAGS+=--enable-perlinterp
 ALLINTERPFLAGS+=--enable-pythoninterp --with-python-config-dir=$(shell python-config --configdir)
-ALLINTERPFLAGS+=--disable-python3interp
+ALLINTERPFLAGS+=--enable-python3interp --with-python3-config-dir=$(shell python3-config --configdir)
 ALLINTERPFLAGS+=--enable-rubyinterp
 ALLINTERPFLAGS+=--enable-tclinterp
  • ビルド
$ debuild -us -uc
$ ls ../  # 親ディレクトリにパッケージが作成される

インストール

必要なパッケージをインストールします。私は vim-gtk を使っています。

$ cd ../
$ sudo dpkg -i vim_7.4.052-1ubuntu4_amd64.deb \
               vim-common_7.4.052-1ubuntu4_amd64.deb \
               vim-doc_7.4.052-1ubuntu4_all.deb \
               vim-runtime_7.4.052-1ubuntu4_all.deb \
               vim-tiny_7.4.052-1ubuntu4_amd64.deb \
               vim-gui-common_7.4.052-1ubuntu4_all.deb \
               vim-gtk_7.4.052-1ubuntu4_amd64.deb 

$ dpkg -l | grep vim
ii  vim               2:7.4.052-1ubuntu4     amd64     Vi IMproved - enhanced vi editor
ii  vim-common        2:7.4.052-1ubuntu4     amd64     Vi IMproved - Common files
ii  vim-doc           2:7.4.052-1ubuntu4     all       Vi IMproved - HTML documentation
ii  vim-gtk           2:7.4.052-1ubuntu4     amd64     Vi IMproved - enhanced vi editor - with GTK2 GUI
ii  vim-gui-common    2:7.4.052-1ubuntu4     all       Vi IMproved - Common GUI files
ii  vim-runtime       2:7.4.052-1ubuntu4     all       Vi IMproved - Runtime files
ii  vim-tiny          2:7.4.052-1ubuntu4     amd64     Vi IMproved - enhanced vi editor - compact version

オリジナルの vim --version の出力と比べると python3 インターフェースが有効になっているのが分かります。

$ diff -u orig-vim.gtk-version.txt <(vim --version)
--- orig-vim.gtk-version.txt	2014-04-26 15:48:29.234466537 +0900
+++ /dev/fd/63	2014-04-26 17:12:13.006045318 +0900
@@ -1,7 +1,7 @@
-VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Jan  2 2014 19:39:59)
+VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Apr 26 2014 16:45:41)
 適用済パッチ: 1-52
 Modified by pkg-vim-maintainers@lists.alioth.debian.org
-Compiled by buildd@
+Compiled by t2y@...
 Huge 版 with GTK2 GUI.  機能の一覧 有効(+)/無効(-)
 +acl             +farsi           +mouse_netterm   +syntax
 +arabic          +file_in_path    +mouse_sgr       +tag_binary
@@ -18,8 +18,8 @@
 +cmdline_info    +keymap          +postscript      +virtualedit
 +comments        +langmap         +printer         +visual
 +conceal         +libcall         +profile         +visualextra
-+cryptv          +linebreak       +python          +viminfo
-+cscope          +lispindent      -python3         +vreplace
++cryptv          +linebreak       +python/dyn      +viminfo
++cscope          +lispindent      +python3/dyn     +vreplace
 +cursorbind      +listcmds        +quickfix        +wildignore
 +cursorshape     +localmap        +reltime         +wildmenu
 +dialog_con_gui  +lua             +rightleft       +windows

python/dyn, python3/dyn に変わってしまいました。vim のドキュメントによると、Windows だと DLL を探すように書いてあります。Linux だと関係ないのかな?これでも Python 拡張が動いたので気にしないことにします。

$ vim
:help python-dynamic
----
9. Dynamic loading                                      python-dynamic         
                                                                               
On MS-Windows the Python library can be loaded dynamically.  The :version      
output then includes +python/dyn.                                              
                                                                               
This means that Vim will search for the Python DLL file only when needed.      
When you don't use the Python interface you don't need it, thus you can use    
Vim without this DLL file.

考える生き方

極東ブログ の著者の著書を読んだ。

それまでブログの存在は知っていたし、twitter 経由でリンクから記事を読んだことも何度かあった。著者がアルファブロガーの1人だということも知っていた。著者のブログである記事の内容を覚えている。いま見返すと、2年前の記事だった。2年前の記事を何となく読んだだけで未だに記憶に留めているというのは、よくよく考えてみるとすごいことだ。

引用が長くなるので結論だけ紹介するが、この結びが強く印象に残っている。

そういう無名の社会人が困難な状況のなかで「公」を支えてくれるおかげで、私たちは生きていけるわけです。みなさんの、誰かもそうなります。

 だから、いつも思うのです、ありがとう。

4月から新社会人になる皆さんへ: 極東ブログ

本書は何か

たぶん何でもないの只の自伝だと著者なら言うかもしれない。著者は自分の生き方や過去を「からっぽの人生」だったと振り返っているが、本書を読む限り、そんな自虐するようなものでもないように思えた。普通の人の、普通の人生も、端からみると全然普通じゃないというのを端的に表しているのかもしれない。

著者が「からっぽ」だというのは、世の中で理想とされるようなレールに則った人生ではなかったということを指しているのかもしれない。それは社会的に承認されることのない生き方だったのかもしれない。著者の年齢が55歳とあるので、いまよりもずっとそういったレールから外れた生き方に理解がなかったり、厳しかった時代だと思う。

著者のブログのファンならば興味深く読めると思う。私は逆だけど、本書を読んで著者の人となりを知って rss reader に追加した方だ。ブログでもそうだけど、読みやすく、ユーモアを交え、押し付けがましくない著者の文体に好感がもてる。著者は否定しても、アルファブロガーというのはやはりちょっと普通じゃないと思うので、普通の人が普通の生きるための参考になるかは正直分からない。でも、誰彼もその人の人生であって、それが普通か普通でないかは、さして意味はないというのを考えさせられる気もする。

テーマが多い

著者の半生を綴ったものだからテーマが多い。仕事、家族、恋愛、難病、学問、社会、文化、人生、、、色んなことが書いてある。その分、著者の主張のようなものがあるわけではない。ブログの延長と言えばそうかもしれないが、本書は誰かのための、何かのためのものという目的では書かれていない。読者が読んで何かを考えるきっかけになるようなものだと思う。

私の中では、最もおもしろかったのは、第5章の「勉強して考えたこと」でした。著者の勉強に対する考え方や向き合ってきた姿勢が読み取れて興味深かった。

勉強する意味

著者の勉強する意味とはこうらしい。

私は、楽しいからだと思う。もちろん、私がそういうタイプの人だからというのはある。
(中略)
知るということ、それ自体が楽しい。子どものころは、ほとんどの人がそうだった。それをいくつになっても維持していくようにするのが勉強ではないかと思う。

著者と自分の共通する点が1つある。それは職を転々と変えてきたところだ。一般的に職を変えることはあまり良いことではないだろう。ただ、考えるという点において環境を変えるというのは大きな影響力をもつように思う。

考える習慣を持たない人に共通する問題点として 「無知の知」 ならぬ 「無知の無知」 があります。 自分が分かってないことを分かってない、 自分の浅慮では到底及ばない領域があることが全く想像できない、 いわば 「井の中の蛙」 状態ですね。

仙石浩明の日記: 「無知の無知」への 3ステップ

また別のブログの引用だけど、またしても新社会人向けに書かれた記事だw、なんだ?そんなことも分かってないのか自分、、、という気分だ。

長く同じ環境にいると人間は慣れる。慣れると、考えずに行動できるようになる。それ自体が悪いわけではないけど、人間は楽をする生きものだから無意識にものごとを深く考えないようになってしまうことがあるように思う。そのときにこの「無知の無知」が起こる気がする。私の場合、転職を繰り返すうちに、前の職場で通用した知識やスキルが次の職場では全く通用しないことが起こる。そんなときに強く実感する。自分は何も分かってなかったんだなぁと。

私にとっての、勉強する意味は2つある。

一つは、勉強してないと、この先生き残れないんじゃないかという不安がある。周りの人たちも当たり前のように勉強しているのを見てきたのでそこに疑問はないし、勉強せずに活躍している人もやはり見たことがない。もう一つは、分からないと悔しい。気持ち悪い。恥ずかしい。そういった負の感情へのコンプレックスがある。

著者の知りたいという動機とは純粋度が違う。違う分、業務の勉強なんかには向くけど、自然科学とか、興味がないものの勉強には食指が動かない。

国際性の中のエピソード

著者の大学時代の、留学生とのやり取りのエピソードでおもしろいものがあった。

「NO」と言えるのは、人間が対等なときだ。人間が人間であるなら、違う考えをもって表明するときは、「NO」という。

気さくな友だちには NO と言えても、そうじゃない知り合いとか、同僚とか、そういう人たちに NO と言いにくかったりするのは、そういった背景もあるんだなぁと省みてしまった。ここで言う対等というのは立場的なものじゃなくて人間関係なようにも思う。

リベラル・アーツという概念

この節がすごくおもしろかった。この節を読むだけで純粋に勉強する楽しみというのはこういうことなんだなと分かった気になる。

メカニカル・アーツの対立語としての wikipedia:リベラル・アーツ について述べている。この wikipedia の説明もおそらくは正しいのだろうけど、本書と比べて、その時代の背景や文化的な側面があまり書かれていない。辞書を引けばその定義が書いてある。そういうものだけど、何だかしっくりこない。

本書では、まず「リベラル」とは自由市民を指していて、自由市民とは、奴隷とは、枝芸とは、そういったあれを知るためにはこれを知る必要があって、背景が分かった上で最終的にはこうなんですと展開している。それはほんの上辺でしかないけど、体系的に学ぶというのはそういうことかとも思う。そして、それがおもしろいから勉強するんだなということも分かる。決して wikipedia を作るために勉強するわけではないんだ。

リベラル・アーツが何ものかが分かったところでその必要性を説く。きっと著者の講義を受けられるとしたらこんな感じで勉強する楽しさを教えてくれるように思う。

また余談だけど、art という単語から想像する日本語は「芸術」だろう。実際、その訳語が第一義だから正しいのだけど、辞書を引くと art という単語には「技術」や「技能」といった訳語も出てくる。ここで言う、リベラル・アーツの art というのは、徒弟訓練で学ぶ技能の体系のことを指すらしい。

"The Art of Computer Programming", "The Art of UNIX Programming" など、Art of と銘を打つコンピューター関係の書籍はちらほらある。それらのイメージを単純に私は「芸術」と捉えていたのだけど、なんだ技術体系の書籍だという見方もあるのかと再発見した次第だ。

終わりに

そして、やはり結びの言葉も良い。

こう言ってもいいのかもしれない。人生の敗北者であっても、貧しい生活でも、学ぶことで人生は豊かになる。

希望はある。

考える生き方

考える生き方

パーフェクトPython

Python3 に特化した専門書という位置付けですが、(Python3 に関した) 言語仕様やその変更、ライブラリの詳細の違いなどを除けば、Python2 でも活用できる知識が大半です。まだ Python2 しか使っていないという方でも十分に役立つ内容だと思います。

ただ、本書を読み進める上で1つだけ忘れてはいけないことがあります。Python 全般について丁寧に解説されていますが

著者名が Python サポーターズとなっていますが、ついったーなんかでは Python モヒカンズなんて言われています。

パーフェクト Python の執筆に参加しました — プログラマのネタ帳 二冊目

と何だかこわい人たちが書いた本だということです。まずはそのことを念頭におくことで本書を楽しんで読み進められる心の準備が整った、言い換えると、覚悟はできたと言えます。そうすれば、途中でこわくなってきても勇気をもって立ち向かえますからね。

気になったところや興味深かったところを付箋付けしていったらこんなことになってしまいました。こわいことがたくさんです。

目次は以下の通りです。

1章 Pythonの概要

  • Part2 言語仕様

2章 Pythonの基本
3章 型とリテラル
4章 制御構文
5章 関数
6章 クラス
7章 モジュールとパッケージ
8章 拡張モジュールと組み込み
9章 標準ライブラリ

  • Part3 実践的な開発

10章 コマンドラインユーティリティ
11章 チャットアプリケーション
12章 アプリケーション/ライブラリの配布
13章 テスト
14章 Webプログラミング

  • Part4 適用範囲

15章 学術/分析系ライブラリ
16章 マルチメディア
17章 ネットワーク
18章 データストア
19章 運用/監視

Appendix
A 環境構築
B 標準ライブラリ

章ごとにこわくなったところをみていきます。

追記:
内輪的なものが気になるという意見も頂きました。
悪い意図はなかったので気になる方は以下のように置き換えて読んでください。

こわい = (個人的に) 興味がある、気になる、分からなかった、おもしろかった、不思議に思った

1章 Pythonの概要

Python の禅

Python の禅を全て紹介していた書籍は、私が読んだ中ではなかったように思います。こういった思想的なものは、最初のうちは言葉通りでしか理解できませんが、Python でプログラムを書くのに慣れていったり、開発コミュニティでやり取りしていくうちに、議論の根幹の部分の、共感できる要素になっていったりする気もします。

最初に思想的なところから啓蒙してくる辺り、Python サポーターズに洗脳されるんじゃないかと警戒したくなります。まだ初めの方なのでこわがらずに Python サポーターズの価値観に共感して読み進めましょう。

  • Beautiful is better than ugly.

最後の方に PerlRuby正規表現Python との違いをさらっと紹介しています。Python は言語の機能として正規表現をサポートせず、re という標準モジュールにより提供していることの背景を期待して読みましたが、、、書かれていませんでした。

なまじ説明がないのは「分かれよ」というメッセージです。Python正規表現リテラルをサポートしないのは、記号の多用を避けることを意図したものだったような気がしますが、情報ソースがどこだったのか見つけられませんでした。私の解釈があっているのか、ちょっとこわいです。

  • Flat is better than nested.

Java のパッケージの名前空間は長くて冗長だというものに対する否定やユーモアからの禅です。厳密に言うと、Java のパッケージは、ファイルシステムの階層構造と一致させる必要はありませんが、慣習的に (分かりやすさのために) そうなっているようです。Python のパッケージは __init__.py を置く決まりになっているのでファイルシステムと一致させる必要があります。データ構造やプログラミングの階層構造のことだと思わせておいて Java を dis ってるのがこわいです。

column: 名前の由来

本書では、spam や egg という変数名を使ったサンプルコードがたくさん紹介されていて、モンティ・パイソンから引用したものだとコラムにあります。

プログラムの挙動を説明するときなど、短いスニペットに使うときもありますが、意味のない名前を付けるのはなるべく避ける方が良いです。というのは、プログラミングの重要な要素の1つに「名前を付ける」ということがあります。ここで言うのは以下のような意味です。

  • 適切な名前を付ける
  • 分かりやすい名前を付ける
  • 簡潔な名前を付ける

この名前付けというのは、簡単なようでプログラムの規模が大きくになるにつれ難しくなります。変数名に限らず、小さい関数に分割しても関数名で悩み、クラス名で悩み、モジュール名で悩みます。その処理そのもの、責務、意図といった多くの概念を簡潔に表そうと考えるほど、名前を付けるのは難しいものです。

spam や egg というのは、ただの名前の過ぎないので、プログラマーにとって重要な「名前を付ける」という素養を軽視するのは、あまり良くない習慣だと言えます。こんなことを書いて大丈夫か、自分がこわいです。

IPython のオートコンプリート機能

IPython の機能の1つとしてコード補完が紹介されています。

私も普段は IPython を使っていますが、サーバーでのデバッグなど、IPython がインストールされていない環境でコード補完を行いたいときもあるでしょう。readline モジュールがインストールされている必要がある点は同じですが、IPython をインストールしなくても Python の標準ライブラリでコード補完できます。

>>> import readline, rlcompleter
>>> readline.parse_and_bind('tab:complete')

覚えておくと、ちょっとしたときに便利です。

たぶん Python サポーターズは、コード補完なんて軟弱な機能を使っていません。こわいです。

3章 型とリテラル

ucs2 と ucs4

PyCon JP 2012 で Dan Kogai 氏が指摘されていたのが印象に残っています。

Python 3.2 まではビルドオプションで ucs2 と ucs4 のどちらかを指定していました。この違いにより、16bit で表せるコードポイントを超えた部分に定義された文字列長が違ってしまうという問題がありました。Python 3.3 (PEP 393) からその問題が解消され、正しい文字列長が返されるようです。Python後方互換性を重視するため、マイナーバージョンアップにより以前書いたコードが動かなくなることはありませんが、あるバージョンから内部の仕組みが変わることはあります。

多くの人はディストリビューションが提供するバイナリを使うため、どちらのビルドオプションか意識せずに使っているでしょう。いまが過渡期であることの、問題の1つですね。

Python サポーターズは自分でビルドしているそうです。こわいです。

sys.intern

私自身、こういった最適化を行いたい状況に遭遇したことはありませんが、http://d.hatena.ne.jp/atsuoishimoto/20110418/1303094478 で知り、そんな仕組みがあるんだと印象に残っている機能です。

>>> import sys
>>> 'spam'.lower() is 'spam'.lower()
False
>>> sys.intern('spam'.lower()) is sys.intern('spam'.lower())
True

これはこわくありません。

タプルの構文

要素が1つの場合には tuple である事が解らなくならないように、(1,) のように末尾にカンマを記述します。

これは少し誤解を招く表現です。というのは、分からなくならないようにこのような記述をするのではなく、tuple の構文はカンマで区切るものであり、カッコは人間が分かりやすいように付けているものだからです。

>>> type((1))
<class 'int'>
>>> type((1,))
<class 'tuple'>
>>> 1,
(1,)

要素1のリストはカンマが必要でないので混同しがちなところかもしれません。

>>> [1]
[1]
>>> type([1])
<class 'list'>

カンマを忘れて tuple のつもりが int だったりしたらこわいです。

追記: 補足もらった、ありがとう

4章 制御構文

column: 内包表記のメリット

シンプルなループは積極的に内包表記を使った方がパフォーマンス面でメリットがあります。

>>> r = [i for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> type(r)
<class 'list'>

私は map の方がコーディング的には好みですが、Python3 からはリストが返されるのではなく map オブジェクトが返されるようになりました。

>>> r = map(lambda x: x, range(10))
>>> type(r)
<class 'map'>
>>> r[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'map' object is not subscriptable

map オブジェクトは、ジェネレーターとしては判別されないみたいですね。

>>> import types
>>> isinstance(r, types.GeneratorType)
False

結果をリストで扱いたいときは組み込み関数の list で変換できますが、そういった用途なら内包表記を使う方が簡潔で良いですね。

>>> r = list(map(lambda x: x, range(10)))
>>> r[1]

従来からのリスト内包表記に加えて、セット内包表記、辞書内包表記も使えるようになりました。

>>> {x for x in [1, 2, 3, 2, 1, 5]}
{1, 2, 3, 5}
>>> {key: value for key, value in zip(['a', 'b'], [1, 2])}
{'b': 2, 'a': 1}

(慣れの問題で) ちょっと違和感も感じますが、コーディング的におもしろいです。こわくはありません。

例外オブジェクトの特殊メソッド

前後に __ (私はアンダーアンダーと呼んでます) で囲まれたメソッドを、Python では特殊メソッドと呼びます。ある人は、特殊メソッドは「Python の黒魔術だ」と表現していましたが、どんな特殊メソッドがあるのか知っておくというのは Python のプログラミングで重要なことです。

例外オブジェクトで3つの特殊メソッドが紹介されていました。

  • __traceback__: 例外発生時のトレースバックが格納される
  • __context__: 例外発生中に例外が発生したとき、元の例外オブジェクトが格納される
  • __cause__: "raise Error from err" としたとき、元の例外オブジェクトが格納される

この手のものは、時間とともにどんどん増えていきそうな雰囲気を感じます。便利なのは便利なのでしょうが、バージョンにより使える特殊メソッドがあるというのは意識しておく必要がありそうです。

Python サポーターズは、特殊メソッド を全て暗記しているそうです。こわいです。

5章 関数

サブジェネレーターへの委譲

Python 3.3 の目玉機能の1つです。

def generator():
    yield from sub_generator()

この処理をちゃんと実装しようとすると、かなり煩雑になるというのが PEP 380 Formal Semantics にあります。PEP 380 は粗訳したまま放ったらかしです、こわいというかごめんなさい。

6章 クラス

super とメソッドの検索順序 (mro)

Python3 から super を呼び出す引数を省略できるようになったのが簡潔で良いですね。

super と mro は、Python で委譲を実装する仕組みの1つです。super のメリットが分かる簡単なサンプルを紹介します (Pythonのsuper()ができること から)

  • 継承元の基底クラスを変更するときにコードを書き換える必要がない
  • サブクラス側でスーパークラスのデフォルトの委譲先を変更できる
>>> class MyDict(dict): pass
... 
>>> MyDict.__mro__
(<class '__main__.MyDict'>, <class 'dict'>, <class 'object'>)
>>> class YourDict(dict): pass
... 
>>> class OurDict(MyDict, YourDict): pass
... 
>>> OurDict.__mro__
(<class '__main__.OurDict'>, <class '__main__.MyDict'>, <class '__main__.YourDict'>, <class 'dict'>, <class 'object'>)

MyDict の mro は、MyDict->dict->object の順番だったのが、YourDict を定義して、OurDict で MyDictとYourDict を多重継承した場合、MyDict の次の mro が YourDict に変更されていることが分かります。例えば、MyDict の __init__ でスーパークラスの __init__ の呼び出しを super で行った場合、MyDict を修正することなく dict.__init__ から YourDict.__init__ へ変更できるというわけです。

>>> class MyDict(dict):
...   def __init__(self):
...     super().__init__()  # mro により、dict か YourDict かを解決してくれる

本書のサンプルにあった、多重継承の「もう少し複雑なケース」はよく分からなかったので読み飛ばしました。初心者に易しくないのがこわいです。

7章 モジュールとパッケージ

相対インポートと名前空間パッケージ

サードパーティのライブラリをインポートしたり、拡張プラグインを開発するときなど、ちょくちょくはまった経験があります。相対インポートと名前空間パッケージについて簡潔に説明されていて分かりやすいです。PEP 420 で Python3.3 からは名前空間パッケージが言語機能として提供されるようになったようです。

従来の pkg_resources によるおまじないは不要になるのですね。

__import__('pkg_resources').declare_namespace(__name__)

Python のパッケージングの仕組みは、一体どのライブラリの機能なのかよく分からなくなるのがこわいです。

9章 標準ライブラリ

組み込み型関数の memoryview

バッファプロトコルをサポートしたデータを扱い、コピーを作る事なく操作できるクラスです。

こんな組み込み型関数があったんだと初めて知りました。

>>> m = memoryview(b'abc')
>>> type(m)
<class 'memoryview'>

Python2.7 でも (バックポートされて?) 使えるみたいですね。

IO 周りのどんな処理や用途に使うと良さそうなのか、実例を見てみたいです。私はこわくて声をかけられませんが、誰か Python サポーターズに聞いてみてほしいです。

map の特殊な使い方

Python2 では map の第1引数に None を渡すと、足りない要素数を None で補う zip のような挙動をしていました (ライブラリリファレンスの map() の説明) 。

>>> map(None, [1,2,3], [4,5], [7,8,9])
[(1, 4, 7), (2, 5, 8), (3, None, 9)]
>>> zip([1,2,3], [4,5], [7,8,9])
[(1, 4, 7), (2, 5, 8)]

Python3 からはこのように扱ってくれないようです。

>>> list(map(None, [1,2,3], [4,5], [7,8,9]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
>>> list(zip([1,2,3], [4,5], [7,8,9]))
[(1, 4, 7), (2, 5, 8)]

変わりに itertools.zip_longest を使うようです。

>>> from itertools import zip_longest
>>> list(zip_longest([1,2,3], [4,5], [7,8,9]))
[(1, 4, 7), (2, 5, 8), (3, None, 9)]

2to3 では変換できなかったので移行のちょっとした tips ですね。

$ 2to3 map-none.py 
--- map-none.py (original)
+++ map-none.py (refactored)
@@ -1 +1 @@
-map(None, [1,2,3], [4,5], [7,8,9])
+list(map(None, [1,2,3], [4,5], [7,8,9]))

Python サポーターズは 2to3 を使わずに自分でコードを書き直すようです。こわいです。

pdb コマンド

インタラクティブデバッガも標準で付属していて必要十分なのが嬉しいですね。ツールが揃っていると、デバッグも楽しいものです。

  • pretty print

複雑なオブジェクトを調べるには from pprint import pprint してたのですが、pp でできることを知りました (> <)

(Pdb) list
  1     d = {'a': 1, 'b': 2, 'l': range(10), 't': tuple(range(10)), 'd': dict(x=9)}
  2  -> import pdb; pdb.set_trace()
[EOF]
(Pdb) p d
{'t': (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 'l': range(0, 10), 'a': 1, 'd': {'x': 9}, 'b': 2}
(Pdb) pp d
{'a': 1,
 'b': 2,
 'd': {'x': 9},
 'l': range(0, 10),
 't': (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)}
  • 条件付きブレイク

なぜか私の中では pdb で 条件付きブレイクはできないと思い込んでいました。これも Python2 の頃からできたんですね (- -#)

> /Users/t2y/work/book/review/perfect-python/pdb/pdb_sample2.py(3)<module>()
-> def f(x):
(Pdb) longlist
  1     import pdb; pdb.set_trace()
  2     
  3  -> def f(x):
  4         # do something
  5         return x
  6     
  7     for i in range(5):
  8         print(f(i))
(Pdb) break 5, x==3
Breakpoint 1 at /Users/t2y/work/book/review/perfect-python/pdb/pdb_sample2.py:5
(Pdb) c
0
1
2
> /Users/t2y/work/book/review/perfect-python/pdb/pdb_sample2.py(5)f()
-> return x
(Pdb) x
3
(Pdb) c
3
4
$ 

Python サポーターズの書くプログラムにはバグがないのでデバッガの使う機会はないんだと推測します。こわいです。

column: IDE

PythonIDE でメジャーなものを紹介していますが、あれ!?と思ったのが漏れていたので紹介します。

きっと Python サポーターズは IDE を使っていません。こわいです。

10章 コマンドラインユーティリティ

10-3-5 データを表示する

集計したデータを表示するところで、さらっと operator モジュールが使われていました。

def print_results(results):                                                         
    for key, value in sorted(results.items(), operator.itemgetter(1)):              
        print(key, value)    

operator モジュールの用途の1つとして、要素のゲッターとして使うとコードが簡潔に記述できるので、私は好んでよく使います。

>>> import operator
>>> d = {"x": 1, "y": 2}
>>> operator.itemgetter("x", "y")(d)
(1, 2)

>>> f = open('test.txt', mode='w', encoding='utf-8')
>>> operator.attrgetter('mode', 'encoding')(f)
('w', 'utf-8')

同処理をする Python の関数や lambda を使うよりパフォーマンス面でもメリットがあると、以前はドキュメントに記述されていた気がします。operator, functools, itertools, contextlib, collections といった標準ライブラリを使うようになってきたら段々こわくなる気がします。

12章 アプリケーション/ライブラリの配布

distribute の注意点

distribute には、プロジェクト内のパッケージを自動で発見する find_packages という関数があり、よく利用されています。しかし、Python 3.3 以降の名前空間パッケージが __init__.py を含まなくなったため、注意が必要です。

これもパッケージ配布の機能が標準で提供されることによる過渡期の課題ですね。覚えておくと、移行の tips になりそうです。移行というキーワードで話題を出したのに「昔からそうだよ」と返されるとこわいです。

13章 テスト

doctest の便利な記法

空行を意図する が紹介されています。

def safe_str(s):
    """
    >>> print(safe_str(None))
    <BLANKLINE>
    >>> print(safe_str("test"))
    test
    """
    if s is None:
        return ""
    else:
        return s

その他にも便利な機能がサポートされています。doctest は、コーディングしながら一緒にテストも書ける楽しさがありますね。

  • "..." でマッチングを省略する ELLIPSIS オプション
def myrange(x):                                                                     
    """                                                                             
    >>> list(myrange(10))  # doctest: +ELLIPSIS                                     
    [0, 1, ... 8, 9]                                                                
    """                                                                             
    return range(x)   
  • 空白文字類を無視する NORMALIZE_WHITESPACE オプション
def get_double_range(x):                                                         
    """                                                                          
    >>> get_double_range(10)  # doctest: +NORMALIZE_WHITESPACE                   
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],                                             
     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])                                             
    """                                                                          
    r = list(range(x))                                                           
    return r, r       

doctest で ELLIPSIS や NORMALIZE_WHITESPACE オプションを使うと、PEP8 の 80 文字制限も遵守できそうでこわいです。

後半の章

後半は主にアプリ開発やツール・ライブラリの紹介です。

  • 14章 Webプログラミング
  • 15章 学術/分析系ライブラリ
  • 16章 マルチメディア
  • 17章 ネットワーク
  • 18章 データストア
  • 19章 運用/監視

本当はいろいろ使ったり、開発したりする上でのコメントも入れたかったのですが、余裕がなくてそこまで至りませんでした。こうやって体系的にみると、いろんな分野で Python が使えるのが分かります。取りあえずで Python を始めてみても、後からあるドメインの処理のために別言語やツールを使わないといけない、といったケースは少ないかもしれません。

2012-04 に Hacker News で好きな言語として Python が選ばれていました。

書くことないから他の記事を引用しているというのが Python サポーターズにばれてないか、こわいです。

正誤表

最後に私が見つけた誤植等をまとめておきました。誰でも編集できるので皆さんも利用して共有してください。Python サポーターズへもこのサイトを連絡済みなので次刷で修正されると思います。

くれぐれも誤植を見つけても悪いのはネコなので文句を言ってはいけません。どんなこわいことがあるか分かりませんからね。

誤植があったらネコのせいです

emerge cat / bengal: 本を書いたらネコがクンスカした

まとめ

無理に Python3 を使う必要はなく、頑に Python2 を使う必要もないのがいまの状況な気がします。新しく開発するものでサードパーティのライブラリがあるなら Python3 で開発を始める時期に差し掛かっていると言えます。

2008年の12月にリリースしたのが Python 3.0 です。
(中略)
リリースから大凡4年がたち、2012年9月にはバージョン 3.3 になっています。

実は前バージョンの 2.0 がリリースされた後、実際に誰もが当然 2 系を使うようになるまで 5 年程度かかっています。各種サードパーティのライブラリが出そろうだけでも多くの時間を要します。Python の原作者である Guido van Rossam 氏は、今回の 3 系に関しても5年かかるだろう、と予測しています。

「2013年 Pythonの本格的な浸透」といった記事も見ましたが、着実に Python3 への移行は進んでいます。Ubuntu の次の LTS では Python3 しか提供しないという噂も聞いたりします。

本書を読んで Python3 プログラミングを始める人が増えて、日本でも Python コミュニティやその開発が活発になると嬉しいです。

Python サポーターズは、自分はこわくないですとみんな言いますが、本当はこわいと思います。