by Peter Norvig
これは Lisp プログラマのための簡単な Python 概説である。 (はからずも、Python プログラマにとってもこのページは Lisp を学ぶのによいそうだ。) 基本的には、Python は「伝統的な」構文 (Lisp 使いが "infix (中置記法)" とか "m-lisp" とか呼ぶもの) をもつ Lisp の方言と思われている。Comp.lang.python のあるメッセージによれば 「自分は Python で遊ぶようになるまで、なぜ LISP がそんなにいいアイデアなのか わからなかった」とのことで、Python は マクロ をのぞく Lisp の主要な機能をすべてサポートしている。それでも eval や演算子オーバーロードの 機能も持っているからマクロが完全になくなったというわけではない。この方法を使えば 自分の独自言語を作ることができる。
わたしが Python に目を向けたのは、 Russell & Norvig の AI 教科書 にある Lisp コード を Lisp から Java に移植しようと考えていたからだった。何人かの教師と学生が Java を望んでいた。 その理由は:
わたしの見地では Python の欠点は 2つある。それは (1) コンパイル時のエラー解析やタイプ宣言がほとんど、Lisp よりさえも 少ないことと、(2) 実行時間が Lisp よりずっと、多くの場合数十倍のスケールで 遅い、ということだ (100倍遅いときもあるかもしれないし、1倍のときもあるが)。 定性的にみれば、Python はインタプリタ型の Lisp と同じくらいの速度に見える。 だが Lisp をコンパイルしたものと比べると明らかに遅い。この理由から、 わたしは Python をたくさん計算が必要な (あるいは、回数を重ねると結局たくさんの計算が必要になる) アプリケーションに使うことはおすすめしない。 しかしわたしの興味は実用ではなく教育のほうにあるから、 その意味ではこれはとりたてて問題ではない。
Python は Scheme の (よいライブラリをもった) 実用的なバージョンか、 ($@ や % がない) きれいなバージョンの Perl ととらえることができる。 Perl の哲学が TIMTOWTDI (there's more than one way to do it - あることをするのにいくつものやり方がある) なのに対して、Python は 人々が同じやり方で使うような最小のサブセットを提供することを 目標としている (たぶんこれは TOOWTDI - あることをするのに ひとつしかやり方がない - ということかもしれないが、もちろん がんばれば常にいくつものやり方はあるだろう)。論争の起こりやすい Python の 機能のひとつは begin/end や中カッコのかわりにインデントを使うというものだが、 これはこの哲学に由来している。つまり、中カッコがないということは、 どこに中カッコを起くべきかという書き方論争がなくなるということだ。 おもしろいことに Lisp もこの点においてはまったく同様の哲学をもっている。 誰もが自分のコードをインデントするのに Emacs を用いるので、 かれらはインデントについて議論するということがない。Lisp プログラムを とってきて正しくインデントし、制御構造用のカッコをとり除けば、 最終的に Python プログラムにかなり似たものができあがる。
Python は簡単なことをとても簡単にし、むずかしいことが 多すぎることのないようにするという、賢明な妥協の哲学をもっている。 わたしの意見では、これは非常にうまくいっている。簡単なことは簡単にでき、 むずかしいことは段階的にむずかしくなっている。その非一貫性は気にならないことが多い。 Lisp はより妥協の少ない哲学をもっていて、非常に強力でよく統制のとれた 言語の核を提供している。このことは Lisp を学ぶのをむずかしくしている。 なぜなら最初から高水準の抽象化を扱わければならず、見た目や感覚に 頼らずに自分がいま何をしているかを理解しなければならないからだ。 しかしこのことは Lisp で抽象化や複雑さの階層をふやすのを簡単している。 Lisp では非常にむずかしいことを、そんなにむずかしくないレベルにまで 落とすことができる。
これはわたしが Python.org にある宣伝文句を 持ってきて、2つのバージョンをつくったものだ。ひとつは ブルーで書かれた Python 用 で、 もうひとつは グリーンの強調文字で書かれた Lisp 用 である。 その他ののこりの部分は両方の言語に共通するもので、黒で書かれている。
Python/Lisp はインタプリタ型で、 コンパイルでき、オブジェクト指向や動的意味論をそなえた 高水準プログラミング言語です。これらの高水準の組み込みデータ構造は、 動的タイプづけや動的束縛と組み合わせることで、「迅速なアプリケーション開発 (Rapid Application Development)」にとって魅力的なものになっています。また スクリプティングや既存のコンポーネントをまとめるための“糊づけ言語”としても使えます。 Python/Lisp の構文はシンプルかつ 簡単に覚えることができ、これはプログラムの可読性を増し、それによってプログラムの維持にかかる費用を 抑えます。Python/Lisp は モジュールや パッケージをサポートしており、これによって プログラムのモジュール性とコードの再利用性を高めることができます。 Python/Lisp インタプリタと豊富な標準ライブラリは、ソースおよびバイナリの形式で、 すべてのプラットフォームにおいて修正することなく使え、かつフリーで配布することができます。 しばしばプログラマ達は Python/Lisp の もたらす高い生産性に酔いしれることでしょう。別過程の コンパイルという 作業がないために、修正-テスト-デバッグ作業は信じられないほど速くなりますし、 Python/Lisp をデバッグするのは 簡単なのです。まずい入力やバグが Segmentation Fault を起こすことは決してありません。 かわりに、インタプリタはエラーを発見し、例外を発生させます。プログラムが例外を キャッチしないときには、インタプリタはスタックトレースを表示します。 ソースレベルデバッガによって、ローカル変数やグローバル変数を検査したり、 任意の式を評価させたり、ブレイクポイントを設定したり一行ずつコードを ステップ実行できたり、といったことができます。デバッガは Python/Lisp 自身で 書かれていて、これは Python/Lisp の 自己記述力の高さを証明しています。いっぽうで、プログラムをデバッグする もっとも手軽な方法は何行かの print 文をソースに埋めこむことです。 すばやい修正-テスト-デバッグ作業のくり返しは、この単純なアプローチを より効果的なものにします。さらにわたしのつけたしとして:
はじめはその ブロック構造のインデント/カッコ に抵抗を示す人もいますが、ほとんどの人はやがてそれらを 好む/深く愛する ようになります。
経験ゆたかなプログラマが Python についてさらに学ぶならば、わたしは Python.org の ダウンロードページ から ドキュメンテーション パッケージをとってくることをおすすめする。 そして Python リファレンス マニュアルと、Python ライブラリ リファレンス に目を通せばよい。 あらゆる類のチュートリアルや書籍がそろっているが、 これらのリファレンスは絶対に必要だ。
以下の表は、Lisp/Python の翻訳ガイドになっている。 赤字 で書かれている箇所は、わたしからみて 一方の言語がとくに劣っていると感じる部分である。 強調 で書かれている箇所は、2つの言語が非常に異なってはいるものの、 とくにどちらが断然よいというわけではない部分である。 標準的なフォントの部分は 2つの言語が互いに似ている部分を示している。 構文はすこし違うものの、概念はまったく、あるいはほとんど同じ部分である。 この表のあとには 要点 と いくつかの Python用 サンプルプログラム が続く。
基本的な特徴 | Lisp における機能 | Python における機能 |
すべてはオブジェクトである | はい | はい |
型はオブジェクトが保有し、 変数が保有するのではない |
はい | はい |
リストにいろんなものを入れられる | はい (連結リスト と配列) | はい (配列) |
複数のパラダイムをサポートしている | はい: 関数型、手続き的、オブジェクト指向、Generic | はい: 関数型、手続き的、オブジェクト指向 |
記憶管理 | 自動ガーベジコレクション | 自動ガーベジコレクション |
パッケージ/モジュール | 使いづらい | 使いやすい |
オブジェクトやクラスの自己記述 | 強力 | 強力 |
メタプログラミングのためのマクロ | パワフルなマクロ完備 | マクロなし |
対話的な「入力-評価-表示 (Read-eval-print)」ループ |
> (string-append "hello" " " "world")
|
>>> ' '.join(['hello', 'world'])
|
簡潔かつ表現力ある言語 |
(defun transpose (m)
|
def transpose (m):
|
プラットフォーム間での移植性 | Windows, Mac, Unix, Linux | Windows, Mac, Unix, Linux |
実装の数 | たくさん | ひとつ |
開発モデル | 商用およびオープンソース | オープンソース |
効率 | C++ より 1~2倍遅い。 | C++ より 2~100倍遅い。 |
GUI, Web などのライブラリ | 標準なし | GUI, Web ライブラリは標準 |
メソッド メソッド実行方法 | 動的, 書式は (meth obj arg)
実行時型付け, 複数メソッド | 動的, 書式は obj.meth(arg)
実行時型付け, クラスごと |
データ型 | Lisp におけるデータ型 | Python におけるデータ型 |
Integer (整数)
Bignum (大きな数) Float (浮動小数点数) Complex (複素数) String (文字列) Symbol (シンボル) Hashtable/Dictionary (ハッシュ/辞書) Function (関数) Class (クラス) Instance (インスタンス) Stream (ストリーム) Boolean (真理値) Empty Sequence (空リスト) 値のない場合 Lisp List (連結リスト) Python List (調整可能な配列) その他 |
42
たくさん (言語の中に) |
42
たくさん (ライブラリ中に) |
制御構造 | Lisp における制御構造 | Python における制御構造 |
式とステートメント | すべては式である | 式とステートメントを分ける |
偽をあらわす値 |
nil が唯一の値である |
None, 0, '', [ ], {} はどれも偽をあらわす |
関数呼び出し |
(func x y z) |
func(x,y,z) |
条件判断 |
(if x y z) |
if x: y
|
While ループ |
(loop while (test) do (f)) |
while test(): f() |
その他のループ |
(dotimes (i n) (f i))
|
for i in range(n): f(i)
|
代入 |
(setq x y)
|
x = y
|
例外 |
(assert (/= denom 0))
|
assert denom != 0, "denom != 0"
|
その他の制御構造 | case, etypecase, cond, with-open-file など | これ以外に制御構造はない |
字句構造 | Lisp における字句構造 | Python における字句構造 |
コメント | ;; セミコロンから行末まで | ## ハッシュマークから行末まで |
区切り文字 |
カッコで式を区切る:
|
インデントで式を区切る:
|
高階関数 | Lisp における高階関数 | Python における高階関数 |
関数の適用
式の評価 ステートメントの実行 ファイルのロード |
(apply fn args)
|
apply(fn, args) または fn(*args)
|
シーケンス用の関数 |
(mapcar length '("one" (2 3))) ⇒ (3 2)
|
map(len, ["one", [2, 3]]) ⇒ [3, 2]
|
その他の高階関数 |
some, every, count-if など
:test, :key などのキーワード |
組込みではこれ以外の高階関数はない
map/reduce/filter にキーワードはない |
読み込み専用変数の閉包 (closure) 書き込み可能な変数の閉包 (closure) | (lambda (x) (f x y))
| lambda x: f(x, y)
不可能、オブジェクトを使うべし |
引数渡し | Lisp における引数渡し | Python における引数渡し |
省略可能な引数
可変個の引数 不特定なキーワードによる引数 呼び出しのきまり |
(defun f (&optional (arg val) ...)
キーワードを使えるのは宣言されたときのみ:
|
def f (arg=val): ...
あらゆる関数にキーワードで引数を渡せる:
|
実行効率 | Lisp における実行効率 | Python における実行効率 |
コンパイル
関数の参照解決 宣言 |
ネイティブコードにコンパイル可能
ほどんどの関数/メソッドの参照が高速 効率を上げるための宣言が可能 |
コンパイルはバイトコードのみ
ほどんどの関数/メソッドの参照は遅い 宣言なし |
その他の機能 | Lisp における機能 | Python における機能 |
クォート (quote) |
リスト構造全体をクォートする:
|
個々の文字列をクォートする、あるいは .split():
|
自分自身を説明する機能 |
(defun f (x)
|
def f(x):
|
リストへのアクセス |
関数経由でアクセスする:
|
構文でアクセスする:
|
ハッシュテーブルへのアクセス |
関数経由でアクセスする:
(let ((h (make-hash-table)))
|
構文でアクセスする:
|
リストの操作 | (cons x y)
| [x] + y でもこれはやらないように
|
配列の操作 | (make-array 10 :initial-element 42)
サイズが変わらない場合
| 10 * [42]
|
多くの人々に重要な部分は Python と Lisp の速度に関するところだろう。 あなたの 利用法に適したベンチマークをおこなうのはむずかしいが、 次の例は役に立つかもしれない。
テスト | Lisp | Java | Python | Perl | C++ | ||
---|---|---|---|---|---|---|---|
ハッシュへのアクセス | 1.06 | 3.23 | 4.01 | 1.85 | 1.00 | ||
例外処理 | 0.01 | 0.90 | 1.54 | 1.73 | 1.00 | 凡例 | |
ファイル中の数値を総計 | 7.54 | 2.63 | 8.34 | 2.49 | 1.00 | C++ の 100倍以上 | |
行を反転させる | 1.61 | 1.22 | 1.38 | 1.25 | 1.00 | C++ の 50~100倍 | |
行列の乗算 | 3.30 | 8.90 | 278.00 | 226.00 | 1.00 | C++ の 10~50倍 | |
ヒープソート | 1.67 | 7.00 | 84.42 | 75.67 | 1.00 | C++ の 5~10倍 | |
配列へのアクセス | 1.75 | 6.83 | 141.08 | 127.25 | 1.00 | C++ の 1~5倍 | |
リスト処理 | 0.93 | 20.47 | 20.33 | 11.27 | 1.00 | C++ の 0~1倍 | |
オブジェクト作成 | 1.32 | 2.39 | 49.11 | 89.21 | 1.00 | ||
単語を数える | 0.73 | 4.61 | 2.57 | 1.64 | 1.00 | ||
平均 | 1.67 | 4.61 | 20.33 | 11.27 | 1.00 | ||
25% から 75% | 0.93 から 1.67 | 2.63 から 7.00 | 2.57 から 84.42 | 1.73 から 89.21 | 1.00 から 1.00 | ||
範囲 | 0.01 から 7.54 | 0.90 から 2.47 | 1.38 から 278 | 1.25 から 226 | 1.00 から 1.00 |
速度は C++ 用の g++ コンパイラが 1.00 になるよう正規化してある。 だから 2.00 はそれより 2倍遅いことを表す。0.01 は 100倍速いということだ。 Lisp は CMUCL コンパイラを使っている。 背景の色は右の凡例にしたがって変えてある。最後の行は各言語からトップ2 と ワースト2 を除いて 25% から 75% のみの結果を出したものだ。 (トップ2 とワースト2 を除いて Lisp と Python を比較した結果、 Python のほうが Lisp よりも 3~85倍 遅いことがわかった -- これは Perl とほぼ同じで、 Java や Lisp よりずっと遅い。Lisp は Java の約 2倍の速さである。)
def f(list, len): return list((len, len(list))) ## まずい Python (define (f list length) (list length (length list))) ;; まずい Scheme (defun f (list length) (list length (length list))) ;; 正しい Common Lispこのことはフィールドとメソッドにもあてはまる。 メソッドと同じ名前のフィールドを使ってデータ抽象をおこなうことはできない:
class C: def f(self): return self.f ## まずい Python ...
def sum(items): total = [0.0] def f(x): total[0] = total[0] + x map(f, items) return total[0] >>> sum([1.1, 2.2, 3.3]) 6.6ここでは lambda を使うことはできないということに注意してほしい。 なぜなら lambda 関数の本体部分は単一の式でなければならず、ステートメントは 使えないからだ。これ以外の選択肢は関数のかわりにオブジェクトを使うことだが、 これはいささか冗長すぎる。しかし冗長さというのはそれを見る者の内にあるものだ。 Lisp プログラマなら (lambda (x) (* k x)) がまっとうなやり方だと思うだろうが、 Smalltalk のプログラマはこれでも多すぎると考える。彼らは [:x | x * k] を 使うのだ。これにひきかえ Java プログラマは、次のようにバカバカしいくらい 冗長な inner class 表記を持ちだしてくるだろう。
new Callable() { public Object call(Object x) { return x.times(k) } }
>>> parse("2 + 2") ['eval_input', ['testlist', ['test', ['and_test', ['not_test', ['comparison', ['expr', ['xor_expr', ['and_expr', ['shift_expr', ['arith_expr', ['term', ['factor', ['power', ['atom', [2, '2']]]]], [14, '+'], ['term', ['factor', ['power', ['atom', [2, '2']]]]]]]]]]]]]]], [4, ''], [0, '']]この結果にはかなりがっかりした。Lisp なら、これと同等の構文木は (+ 2 2) である。この構文木なら誰にでも使えるが、 Python のこんな構文木をいじれるのは本当のエキスパートだけだろう。 文字列を連結してマクロに似たようなものは作れるかもしれない。 それでもその機能は言語のその他の部分と統合されていないので、 実際には使えない。Lisp では、マクロにはおもに 2つの目的があった: 新しい制御構造をつくることと、ある問題に特化したカスタム言語を つくることである。前者は、単純に Python では実現できない。 後者については、問題に特化した形式の データ を Python で つくることによって実現できる。下の例で、わたしは辞書用の組み込み文法と、 文字列を解析してデータ構造に変換する前処理をつかって、文脈自由文法を定義している。 この組み合わせはほとんど Lisp のマクロと同じくらいに便利だが、 たとえば論理プログラミング言語のコンパイラを書くなどの より複雑なタスクは、Lisp ではやさしいが Python ではむずかしい。
Lisp プログラム simple.lisp | Python プログラム simple.py |
(defparameter *grammar* '((sentence -> (noun-phrase verb-phrase)) (noun-phrase -> (Article Noun)) (verb-phrase -> (Verb noun-phrase)) (Article -> the a) (Noun -> man ball woman table) (Verb -> hit took saw liked)) "よくある英語の文法のサブセット。") (defun generate (phrase) "ランダムな文あるいは句を生成する。" (cond ((listp phrase) (mappend #'generate phrase)) ((rewrites phrase) (generate (random-elt (rewrites phrase)))) (t (list phrase)))) (defun generate-tree (phrase) "ランダムな文あるいは句を、完全な構文木つきで生成する。" (cond ((listp phrase) (mapcar #'generate-tree phrase)) ((rewrites phrase) (cons phrase (generate-tree (random-elt (rewrites phrase))))) (t (list phrase)))) (defun mappend (fn list) "リストの各要素に関数 fn を適用させた結果を append する。 mapcon のようなものだが、nconc のかわりに append を使っている。" (apply #'append (mapcar fn list))) (defun rule-rhs (rule) "生成規則の右辺。" (rest (rest rule))) (defun rewrites (category) "このカテゴリに対する可能な書き換えのリストを返す。" (rule-rhs (assoc category *grammar*))) |
from whrandom import choice def Dict(**args): return args grammar = Dict( S = [['NP','VP']], NP = [['Art', 'N']], VP = [['V', 'NP']], Art = ['the', 'a'], N = ['man', 'ball', 'woman', 'table'], V = ['hit', 'took', 'saw', 'liked'] ) def generate(phrase): "ランダムな文あるいは句を生成する。" if is_list(phrase): return mappend(generate, phrase) elif phrase in grammar: return generate(choice(grammar[phrase])) else: return [phrase] def generate_tree(phrase): "ランダムな文あるいは句を、完全な構文木つきで生成する。" if is_list(phrase): return map(generate_tree, phrase) elif phrase in grammar: return [phrase] + generate_tree(choice(grammar[phrase])) else: return [phrase] def mappend(fn, list): "リストの各要素に関数 fn を適用させた結果を append する。" return reduce(lambda x,y: x+y, map(fn, list)) def is_list(x): return type(x) is type([]) |
この Lisp プログラムを走らせる | この Python プログラムを走らせる |
> (generate 'S) (the man saw the table) |
>>> generate('S') ['the', 'man', 'saw', 'the', 'table'] >>> string.join(generate('S')) 'the man saw the table' |
Python のコードに含まれている文法が Lisp のそれより汚なくなっているのが いやだったので、Python で構文解析器を書く (あとでこれにはフリーで 使用可能なものがすでにいくつかあることを知った) か、組み込み演算子を オーバーロードすることを考えた。後者のアプローチは、わたしが 論理式を表現・操作するためにつくった Expr クラス などで実現可能である。しかし今回のアプリケーションではよくある その場かぎりのアドホックな生成規則パーサでいいだろう。つまり 生成規則は '|' で区切られた候補のリストからなり、各候補は ' ' で区切られた単語のリストになっている、というように。そして Lisp でできたプログラムを翻訳するよりも Python の慣習にしたがった 文法プログラムにしたら、次のようなものができあがった:
Python プログラム simple.py (慣習バージョン) |
"""与えられた文法からランダムな文を生成するモジュール。文法は S = 'NP VP | S and S' のような形式のエントリで構成される。各エントリは {'S': [['NP', 'VP'], ['S', 'and', 'S']]} のような形式に変換され、 これはそれぞれの外側の大きなリスト (top-level lists) からどれか ひとつの書き換え候補がランダムに選ばれることを示す。そして、選ばれた リストの内側にある各要素 (each element of the second-level list) が 書き換えられる。その要素が文法中になければ、各要素はそれ自身に書き 換えられる。関数 rewrite および rewrite_tree はそれぞれ単語のリストと、 結果が追加されるアキュムレータ (空リスト) を引数としてとるようになっている。 関数 generate および generate_tree は、rewrite と rewrite_tree への 簡便なインタフェイスを提供しており、これは文字列 (デフォルトは 'S') を 入力として受けとる。""" import random def make_grammar(**grammar): "シンボルから各書き換え候補へのマッピングをおこなう辞書を作成する。" for k in grammar.keys(): grammar[k] = [alt.strip().split() for alt in grammar[k].split('|')] return grammar grammar = make_grammar( S = 'NP VP', NP = 'Art N', VP = 'V NP', Art = 'the | a', N = 'man | ball | woman | table', V = 'hit | took | saw | liked' ) def rewrite(words, into): "リスト中の各単語を文法中のランダムなエントリで (再帰的に) 置き換える。" for word in words: if word in grammar: rewrite(random.choice(grammar[word]), into) else: into.append(word) return into def rewrite_tree(words, into): "単語のリストを、文法から選ばれたランダムな木で置き換える。" for word in words: if word in grammar: into.append({word: rewrite_tree(random.choice(grammar[word]), [])}) else: into.append(word) return into def generate(str='S'): "str に含まれている各単語を文法中のランダムなエントリで (再帰的に) 置き換える。" return ' '.join(rewrite(str.split(), [])) def generate_tree(cat='S'): "文法を使ってカテゴリ cat を書き換える。" return rewrite_tree([cat], []) |
Japanese Translation Changes:
公開。(2002/10/13)
深町 和哉さん, Shiro Kawai さん, 木村 栄伸さん から指摘をいただき修正しました。(2002/10/27)
Yusuke Shinyama <yusuke at cs . nyu . edu>