Effective Python の発売日だった

自宅に Effective Python がやってきた。233 ページしか無いのに 3,200 円もするという技術本。 まぁ技術本なんてこのような値段設定は珍しくないが、やっぱり高い。

オライリーの書籍は O'Reilly Ebook で電子書籍としても購入することができるが、こちらは PDF フォーマットな上に若干出版にタイムラグがある。いつもだったら Ebook を待つのだがちょっと今回はすぐ読んでみたいのとたまには物理的な紙の媒体で読んでみたいというので前もって楽天で予約しておいたというわけだ。

折角高いお金を出して買ったので、ちょっと一人読書会でもやってみよう、というお話。 Blog や Qiita などで不特定多数に向けて何かを書くことを心掛けると、いい加減なことを書けないのでちゃんと理解しようという縛りを自分に設けることができる。

Effective Python の目次は O'Reilly の書籍紹介ページにある。 各項目に対して自分なりの考察若しくは感想文を書こうというわけだ。というわけで、以下1 章 Python 流思考 (Pythonic Thinking) に関する一人読書会を実行する。

項目 1: 使っている Python のバージョンを知っておく

Python には 2.x 系と 3.x 系があり双方には互換性が無い。処理系にも CPython, Jython, IronPython, PyPy などあるよ、という話。 OS X や CentOS 等に最初からインストールされている Python は未だ 2.x 系である。Ubuntu は 16.04 LTS から Python 3.5 がデフォルトになるようだ。

これから新規 Python プロジェクトを立ち上げる場合は特に理由がない場合は 3.x 系を使用すること。 とはいえ、仕事としてやっているとどうしても 2.x の方を相手せざるを得ないのも事実。

項目 2: PEP 8 スタイルガイドに従う

Python プログラマの従うべき最も有名なコーディング規約として PEP 8 がある。 PyCharm などの IDE であれば最初から PEP 8 のチェックが入るし Vim や Emacs のようなエディタでもチェックする方法がある。

書籍には PEP 8 の中でも特筆すべき項目について列挙されている。筆者が気になったものを以下に引用する。

各行は、長さが 79 文字かそれ以下とする。

PEP 8 のこれはかなり有名なのだが、何故 79 文字以下なのだろうか。80 文字では駄目だったのだろうか。

またコードが短めの Python ならまだこれも守れないこともないが Java で 80 文字制限などしたら悲惨なことになるし、 今のディスプレイは高精細なので割と横に多く表示できるので 120 文字くらいでもいい気はしないでもないが、まぁこういう規約なので守っておく。

長さを使って空値かどうかをチェックしない。空値が暗黙に False と評価されることを使う。

暗黙型変換を使うのは危ないのでは?長さを使ったほうが安全では?と思ってしまった。 特に PHP では if ($hoge) と書くと if ($hoge == true) の意味 (緩やかな比較) となり PHP では割と変な値まで true, false になってしまうというのがあるので strlen($hoge) と書くのは結構よくやるので Python もアリではないかと思っていたが、暗黙型変換を用いた方が構文がシンプルになって良いということだろうか。

# 冗長な書き方
if len(somelist) == 0:
    ...

# 好ましい書き方
if not some list:

項目 3: bytes, str, unicode の違いを知っておく

Python 2 では文字列は str で Unicode 文字列を扱うのに unicode を使わなければならなかった。 英語圏の人は全く困らない仕様だが、我々の使用しているような非 ASCII 文字を使用している言語の場合 u'日本語' などと頭に u を付けて Unicode 文字列であることを明示しなければならなかった。

しかし Python 3 ではこれば str に一本化され単純に '日本語' と表現できるようになった。つまり Python 2 の unicode が Python 3 の str になったという話。

一時期 Unicode と UTF-8 がごっちゃになっていた時があったのだが UTF-8 はあくまで Unicode の効率的なエンコード方式であり別物である。 Unicode についてがすごく分かりやすかった。

Unicode 文字をバイナリ (生の 8 ビット値) で表すには多くの手法があります。一番多いのは UTF-8 符号化です。 重要なのは Python 3 の str インスタンスと Python 2 の unicode インスタンスがバイナリ符号化を伴っていないことです。 Unicode 文字をバイナリデータに変換するには、メソッド encode を使わなければなりません。

つまりファイルから読み込んだ場合などで bytes 型になっている時はエンコードされている状態 (多くは UTF-8) なのでそれを decode しなければならない。

項目 4: 複雑な式の代わりにヘルパー関数を書く

複雑な式を 1 行に詰め込むなとか部分的に共通化できるならヘルパー関数を書けという話。 Python は気軽に関数内関数が書けるので、このあたりは積極的に使っていきたいところ。

項目 5: シーケンスをどのようにスライスするか知っておく

Python は文字列もシーケンス型なので配列のようなスライスが簡単に使えるのが便利で美しい。

リストの先頭からスライスするときには、添字のゼロは省いて、見た目をスッキリさせましょう。

assert a[:5] == a[0:5]

0 ... つけてしまっていたかもしれない。

末尾までスライスするときには、末尾の添字は冗長なので省きましょう。

assert a[5:] == a[5:len(a)]

これはちゃんとできていた。Java 等の substring が第二引数を付けないと末尾までスライスするという意味なので類推しやすかったように思う。

一箇所、パット見よくわからなかった箇所が以下:

添字 start も end もないスライスに代入を行うと、(新しいリストが作成されるのではなくて) リストの内容全体が右辺のリストが参照している要素に置き換わります。

a = []
b = a
print('Before', a)  # []
a[:] = [101, 102, 103]
assert a is b  # True
print('After ', a)  # [101, 102, 103]

なるほど単純に a = [101, 102, 103] とやってしまうと a is not b になってしまう。リストの参照を変えずにリストの内容全体を書き換えたい時に使うわけだ。

項目 6: 1 つのスライスでは start, end, stride を使わない

start, end, stride とは somelist[start:end:stride] みたいなものの事で stride でリストの取得間隔を指定できるが、 これが読みにくいのでなるべく避けましょうという事だった。 それ以前にあまり使うことが無いわけだが……。

項目 7: map や filter の代わりにリスト内包表記を使う

これはもうその通りとしか言いようが無い。リスト内包表記は便利すぎる。他の言語にも欲しいくらいだ。

項目 8: リスト内包表記には 3 つ以上の式を避ける

リスト内包表記は for 文をネストできるが、当然だがやり過ぎると読みにくいので普通に for 文を使うほうが良い。

項目 9: 大きな内包表記にはジェネレータ式を考える

これもその通りとしか言いようがない。Python はリスト内包表記を少し書き換えるだけでジェネレータ式になるので便利だ。

項目 10: range よりは enumerate にする

恥ずかしながら私も range() で書きがちだったのでこれは肝に命じることにする。

for i in range(len(flavor_list)):
    flavor = flavor_list[i]
    ...

よりも

for i, flavor in enumerate(flavor_list):
    ...

の方がずっと簡潔だという話。PHP の foreach, Java の拡張 for 文にあたるものは Python では enumerate として用意されていると覚える。

項目 11: イテレータを並列に処理するには zip を使う

これも Python の便利なところで、他の言語でも zip() が欲しいと思い自分で実装してしまったケースもあるくらいだ。

項目 12: for と while ループの後の else ブロックは使うのを避ける

恥ずかしながら私はこの else が通る場合の条件を理解していなかった:

for i in range(3):
    ...
else:
    ...  # for 分が break されなかった場合に呼ばれる (!!)

Python での else, except, finally のすべての用法から、初めてのプログラマは for/else の else 部分は「ループが完了しなかったらこれをしなさい」という意味だと思い込むものです。

for 文が実行されなかった時 (対象のリストが 0 件だった時) だと思っていた。全然違った。 というわけで、確かに混乱の元なので使わないほうがいいだろう。わかりやすさを好む Python でこんな分かりにくいパーツがあるのが驚いた。

項目 13: try/except/else/finally の各ブロックを活用する

こちらでは前章と違い else も活用せよ と書いてある。しかし、こちらも誤解を呼ぶ (正直パット見分からない) から使わないほうが良いのではないかと思うが……。