Python のカッコ無いところを紹介してみる

Haskellのカッコいいところを紹介してみる をみて、 Python と比較してみようと思います。 以下、 heading は上記記事の heading の引用で、 Python のことではなく Haskell の特徴です。

数学や英語の知識で「読める」表現が多い

一応、 instanceof など多くの2引数関数が、 infix で書いたら左に来るものが第一引数というルールを守っているので、頭の中ではそれで引数の順序を補完して、 if instanceof(x, int) は "if x is instance of int" と読んでいます。引数の順序がどっちだっけ?と迷うことはほとんど無いです。

しかし残念ながら Python は中置記法はありません。構文をシンプルに保つ方を取っているんでしょうね。

import Data.List
import Data.Function
xs = sortBy (compare `on` snd) [(4,4),(1,2),(3,1)]

これは Python で書くと

xs = sorted([(4,4), (1,2), (3,1)], key=lambda x: x[1])

となります。ソート順を比較するのに比較関数ではなくて比較キーを取り出す関数を指定するので、 compare on のようには書けませんが十分読みやすいと思います。

(比較関数が推奨されないのは、動的インタプリタ言語では関数の呼び出しコストが高かったりインライン展開等の最適化ができないという事情があります。比較は O(n log n) 回行われますが、キー関数が組み込み型を返せばユーザー定義関数の呼び出し回数は O(n) だけにして、比較関数は高速な組み込み型の比較を使えます。「読みやすさと効率を両立する書き方を Pythonic と呼び推奨する」の良い例です)

文字列がリストで実現されている

Python はリストと文字列は別の型です。 Haskell でも組み込みの文字列型は効率が悪くて結局別の文字列型が推奨されていると思いますが、 Python の場合はリストは mutable, 文字列は immutable という違いなどもあります。

ですが、シーケンス型、というまとまりで同じ操作ができるようにインタフェースが提供されているので、 (array と str でインタフェースが異なるphpPerlなどに比べて) 1つの操作を覚えるだけでどちらにも応用できる事が多いです。

x = reverse [1,2,3]
-- x == [3,2,1]
y = reverse "123"
-- y == "321"

Python の組み込みの reversed 関数はイテレータを返す関数なので文字列の逆転はできませんが、シーケンスの共通操作としてスライスの第一、第二項 (始点と終点) を省略して第三項 (ステップ) に -1 を指定することで逆転ができます。

>>> [1, 2, 3][::-1]
[3, 2, 1]
>>> "123"[::-1]
'321'

スライスは他にも奇数番目の要素を [1::2] で取得できたりと、シンプルなのに超強力です。

リスト内包表記が簡潔

xs = [ x^2 | x <- [1..10]]
-- xs == [1,4,9,16,25,36,49,64,81,100]
>>> [x**2 for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Haskell と比べると、 Python は ∈ を in で表しているなど、記号よりもその記号に意味的に近い英語のキーワードを好むことが多いです。 数学のバックグラウンドがない場合内包表記はとっつきにくいですが、チュートリアルをひと通り読んだ程度の人がコードを読むときは記号よりもキーワードの方が意味を思い出しやすい気がします。

また、 1..10 ではなく range(1, 11) と書かないといけないのも、リッチな構文よりも、できるだけ構文を増やさず普通の関数を使う方を選ぶあたりは Python らしさと言えます。

1つの例だけを見ると構文を追加したほうが読みやすいですが、それが積み重なっていくと構文がどんどん複雑になって、覚える・思い出すコストが高くなっていって、可読性が逆転することになるので、バランスが重要です。Pythonは可読性の差がもっと大きくないと構文の追加が却下されることが多いです。

ラムダ式が簡潔

xs = map (\x -> x^2) [1..10]
-- xs == [1,4,9,16,25,36,49,64,81,100]

Python だと次のようになるんですが、内包が推奨されます。

>>> map(lambda x: x**2, range(1,11))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

言語によってはlambdaとかfunctionとか書く必要があるわけですが、あれって面倒だと思うんですよね。

Python は記号よりもキーワードを好み、(簡潔さを含む)可読性は気にするがタイプ数はあまり気にしないですね。

また、 Python の場合は関数型言語でよくある「小さい操作をパズル的に組み合わせる」スタイルよりも、できるだけ内包やループを使うスタイルを推奨しています。Python高階関数はよく使いますが、関数型言語脳を形成しなくても手軽に使える、関数型言語の美味しいところだけを頂戴した手続き言語です。

少し脱線しますが、 Python の lambda で文(Python では代入も文)が書けないという制限を嫌がる人も多いですが、 Python は1つの文では1つのことをするのを推奨していて、 JavaScript でよくある関数の引数として渡す複雑な関数を in-place で書くスタイルをあえて書きにくくしています。諦めて def を使いましょう。

無限リストを作りやすい

fibs = 1 : 1 : zipWith (+) fibs (tail fibs)

Python ではジェネレータを使います。

def fibs():
    a = b = 1
    while True:
        yield a
        b, a = a, a+b

Python の方が長いですが、とっつきやすさは Python の方がいいと思います。

型システムが強力

PythonHaskell に比べると型クラスよりも敷居が低い duck typing を採用しているので、学習は容易だと思います。

また、型の扱いは動的言語の中ではかなり厳密な方で、実行時かコンパイル時かという違いはあるものの、 php などに比べると暗黙の型変換によるエラーの見逃しが少なく型がエラーを見つけてくれる事が多いという点は Haskell と共通しています。

まとめ

import 地獄やインデントが構文になっているところなど、見た目として HaskellPython で似ている点もありますが、 PythonHaskell に比べると「記号よりもキーワード、構文よりも関数、小さいパーツのパズルより高級な手続きの記述」となっており、(特にメインでは他の手続き言語を使っているユーザーにとって)ある程度使えるようになるまでの勉強や暗記が少なくてとっつきやすい言語だと思います。

一方、関数型言語脳を形成することで頭を良くする(関数型が手続き型より良いという意味ではなく、複数のパラダイムを並列して理解する事により柔軟な考え方ができるようになるという意味)とか、数学的な定義をそのまま書き下せるのが Haskell の言語としての魅力だと思います。

また、ランタイムとしてもGHCの方がCPythonよりも高速だったり軽量スレッドが組み込みだったりと魅力的です。その点では、高速で軽量スレッドを持っていてPythonのようにとっつきやすい言語として Go も「今年勉強したい言語」にどうでしょう?

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。