Lisp プログラマのための Python 入門

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 を望んでいた。 その理由は:

  1. 他のクラスでもっともなじんでいた言語だったから。
  2. グラフィカルなアプリケーションが欲しかったらしい。
  3. 少数派ではあったが、ブラウザ上のアプレットを望んだ人もいた。
  4. 何人かはただ単に Lisp の文法に慣れることができなかった。 彼らが授業にささげられる、限りある時間のうちには。
しかし、Java のコードでこれら走らせるという 試み は あまり成功したとはいえなかった。 そのうちわたしは PythonJython を発見し、 こっちのほうが Java よりいいかもしれないと思い始めたのだ。

わたしの結論

Python は、わたしが意図する使い方ではすぐれた言語である。 この言語は簡単に (コンパイル-リンク-ロード-実行というプロセスなしで) 使うことができる。 このことはわたしにとって教育上とても大切だ。Python は J-A-V-A と綴られるものの 最低必要条件を満たしはしないが、Jython はそれに近い。 Python と Lisp どちらの言語も経験がない人にとっては、 Python のほうが Lisp よりも読みやすいようだ。 わたしの書いた Python コード のほうが、同じ Lisp コード に比べて ずっと (これとは独立に書かれた) 擬似コード に近い。 これは重要なことである。なぜなら本の中の擬似コードがオンラインの Lisp コードとどう対応しているのか (Lisp プログラマにとっては明白でも) わかりにくい、という不満が何人かの生徒から出ていたからだ。

わたしの見地では Python の欠点は 2つある。それは (1) コンパイル時のエラー解析やタイプ宣言がほとんど、Lisp よりさえも 少ないことと、(2) 実行時間が Lisp よりずっと、多くの場合数十倍のスケールで 遅い、ということだ (100倍遅いときもあるかもしれないし、1倍のときもあるが)。 定性的にみれば、Python はインタプリタ型の Lisp と同じくらいの速度に見える。 だが Lisp をコンパイルしたものと比べると明らかに遅い。この理由から、 わたしは Python をたくさん計算が必要な (あるいは、回数を重ねると結局たくさんの計算が必要になる) アプリケーションに使うことはおすすめしない。 しかしわたしの興味は実用ではなく教育のほうにあるから、 その意味ではこれはとりたてて問題ではない。

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")
"hello world"
>>> ' '.join(['hello', 'world'])
'hello world'
簡潔かつ表現力ある言語 (defun transpose (m)
  (apply #'mapcar #'list m))
> (transpose '((1 2 3) (4 5 6)))
((1 4) (2 5) (3 6))
def transpose (m):
  zip(*m)
>>> transpose([[1,2,3], [4,5,6]])
[(1, 4), (2, 5), (3, 6)]
プラットフォーム間での移植性 Windows, Mac, Unix, LinuxWindows, 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
100000000000000000
12.34
#C(1, 2)
"hello"
hello
(make-hash-table)
(lambda (x) (+ x x))
(defclass stack ...)
(make 'stack)
(open "file")
t, nil
(), #() linked list, array
nil
(1 2.0 "three")
(make-arrary 3 :adjustable t
  :initial-contents '(1 2 3))

たくさん (言語の中に)
42
100000000000000000

12.34
1 + 2J
"hello" または'hello' ## 変更不可

'hello'
{}
lambda x: x + x
class Stack: ...
Stack()
open("file")
1, 0
(), [] tuple, array
None
(1, (2.0, ("three", None)))
[1, 2.0, "three"]

たくさん (ライブラリ中に)
制御構造 Lisp における制御構造 Python における制御構造
式とステートメント すべては式である 式とステートメントを分ける
偽をあらわす値 nil が唯一の値である None, 0, '', [ ], {} はどれも偽をあらわす
関数呼び出し (func x y z) func(x,y,z)
条件判断 (if x y z) if x: y
else: z
While ループ (loop while (test) do (f)) while test(): f()
その他のループ (dotimes (i n) (f i))
(loop for x in s do (f x))
(loop for (name addr salary) in db do ...)
for i in range(n): f(i)
for x in s: f(x) ## どんなシーケンスにも可能
for (name, addr, salary) in db: ...
代入 (setq x y)
(psetq x 1 y 2)
(rotatef x y)
(setf (slot x) y)
(values 1 2 3) スタック上
(multiple-value-setq (x y) (values 1 2))
x = y
x, y = 1, 2
x, y = y, x
x.slot = y
(1, 2, 3) ヒープ中のメモリを使う
x, y = 1, 2
例外 (assert (/= denom 0))
(unwind-protect (attempt) (recovery))
��N
(catch 'ball ... (throw 'ball))
assert denom != 0, "denom != 0"
try: attempt()
finally: recovery()
try: ...; raise 'ball'
except 'ball': ...
その他の制御構造 case, etypecase, cond, with-open-file など これ以外に制御構造はない
字句構造 Lisp における字句構造 Python における字句構造
コメント ;; セミコロンから行末まで ## ハッシュマークから行末まで
区切り文字 カッコで式を区切る:
(defun fact (n)
  (if (<= n 1) 1
      (* n (fact (- n 1)))))
インデントで式を区切る:
def fact (n):
  if n <= 1: return 1
  else: return n * fact(n — 1)
高階関数 Lisp における高階関数 Python における高階関数
関数の適用
式の評価
ステートメントの実行
ファイルのロード
(apply fn args)
(eval '(+ 2 2)) => 4
(eval '(dolist (x list) (f x)))
(load "file.lisp")
apply(fn, args) または fn(*args)
eval("2+2") => 4
exec("for x in list: f(x)")
execfile("file.py") または import file
シーケンス用の関数 (mapcar length '("one" (2 3))) ⇒ (3 2)

(reduce #'+ numbers)
(remove-if-not #'evenp numbers)

(reduce #'min numbers)
map(len, ["one", [2, 3]]) ⇒ [3, 2]
または [len(x) for x in ["one", [2, 3]]]
reduce(operator.add, numbers)
filter(lambda x: x%2 == 0, numbers)
または [x for x in numbers if x%2 == 0]
min(numbers)
その他の高階関数 some, every, count-if など
:test, :key などのキーワード
組込みではこれ以外の高階関数はない
map/reduce/filter にキーワードはない
読み込み専用変数の閉包 (closure)
書き込み可能な変数の閉包 (closure)
(lambda (x) (f x y))
(lambda (x) (incf y x))
lambda x: f(x, y)
不可能、オブジェクトを使うべし
引数渡し Lisp における引数渡し Python における引数渡し
省略可能な引数
可変個の引数
不特定なキーワードによる引数
呼び出しのきまり
(defun f (&optional (arg val) ...)
(defun f (&rest arg) ...)
(defun f (&allow-other-keys &rest arg) ...)
キーワードを使えるのは宣言されたときのみ:
(defun f (&key x y) ...)
(f :y 1 :x 2)
def f (arg=val): ...
def f (*arg): ...
def f (**arg): ...

あらゆる関数にキーワードで引数を渡せる:
def f (x,y): ...
f(y=1, x=2)
実行効率 Lisp における実行効率 Python における実行効率
コンパイル
関数の参照解決
宣言
ネイティブコードにコンパイル可能
ほどんどの関数/メソッドの参照が高速
効率を上げるための宣言が可能
コンパイルはバイトコードのみ
ほどんどの関数/メソッドの参照は遅い
宣言なし
その他の機能 Lisp における機能 Python における機能
クォート (quote) リスト構造全体をクォートする:
'hello
'(this is a test)
'(hello world (+ 2 2))
個々の文字列をクォートする、あるいは .split():
'hello'
'this is a test'.split()
['hello', 'world', [2, "+", 2]]
自分自身を説明する機能 (defun f (x)
  "compute f value"
  ...)
> (documentation 'f 'function)
"compute f value"
def f(x):
  "compute f value"
  ...
>>> f.__doc__
"compute f value"
リストへのアクセス 関数経由でアクセスする:
(first list)
(setf (elt list n) val)
(first (last list))
(subseq list start end)
(subseq list start)
構文でアクセスする:
list[0]
list[n] = val
list[-1]
list[start:end]
list[start:]
ハッシュテーブルへのアクセス 関数経由でアクセスする:
(setq h (make-hash-table))
(setf (gethash "one" h) 1.0)
(gethash "one" h)

(let ((h (make-hash-table)))
  (setf (gethash "one" h) 1)
  (setf (gethash "two" h) 2)
  h)
構文でアクセスする:
h = {}
h["one"] = 1.0
h["one"] または h.get("one")
h = {"one": 1, "two": 2}
リストの操作 (cons x y)
(car x)
(cdr x)
(equal x y)
(eq x y)
nil
(length seq)
(vector 1 2 3)
[x] + y でもこれはやらないように
x[0]
x[1:] でもこれはやらないように
x == y
x is y
None または () または [ ] または 0
len(seq)
(1, 2, 3)
配列の操作 (make-array 10 :initial-element 42)
(aref x i)
(incf (aref x i))
(setf (aref x i) 0)
(length x)
#(10 20 30)
サイズが変わらない場合
10 * [42]
x[i]
x[i] += 1
x[i] = 0
len(x)
[10, 20, 30]

多くの人々に重要な部分は Python と Lisp の速度に関するところだろう。 あなたの 利用法に適したベンチマークをおこなうのはむずかしいが、 次の例は役に立つかもしれない。

5 つの言語における 10個のベンチマークの相対的な速度比較。 The Great Computer Language Shootout より。
テスト Lisp JavaPythonPerl C++
ハッシュへのアクセス1.063.234.011.851.00
例外処理0.010.901.541.731.00 凡例
ファイル中の数値を総計 7.54 2.63 8.34 2.49 1.00 C++ の 100倍以上
行を反転させる 1.61 1.221.38 1.25 1.00 C++ の 50~100倍
行列の乗算 3.30 8.90278.00 226.00 1.00 C++ の 10~50倍
ヒープソート 1.67 7.0084.42 75.67 1.00 C++ の 5~10倍
配列へのアクセス 1.75 6.83141.08 127.25 1.00 C++ の 1~5倍
リスト処理 0.93 20.4720.33 11.27 1.00 C++ の 0~1倍
オブジェクト作成 1.32 2.3949.11 89.21 1.00
単語を数える 0.73 4.612.57 1.64 1.00
平均 1.67 4.6120.33 11.27 1.00
25% から 75% 0.93 から 1.67 2.63 から 7.002.57 から 84.42 1.73 から 89.21 1.00 から 1.00
範囲 0.01 から 7.54 0.90 から 2.471.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倍の速さである。)

Lisp プログラマが Python を学ぶときの要点

ここに Lisp プログラマであるわたしが Python を学んだときにひっかかった箇所をまとめてみた:
  1. リストは Cons ではない。 Python のリストは実際には Lisp における adjustable array や、Java における Vector に似ている。つまりリストへの アクセスは O(1) ですむが、cons と cdr をつかった操作に相当するものは O(n) の 新しい記憶領域を消費してしまう。car/cdr を使った再帰よりは mapfor e in x: を使ったほうがいい。あと、空のリストがただひとつではなく 複数であることには注意しておく必要がある。これは Lisp でよくあるバグ -- ユーザが (nconc old new) とやって old が変更されると思いこみ、 実際には oldnil のときには何も起こらない -- を防ぐ。 Python では old.extend(new) はつねにうまく動作する。しかしこれは同時に、 空リスト [] に対して検査を行うときにはつねに == を使わなければ ならないということを意味する。is ではだめなのだ。また、デフォルト引数を [] にした場合は、値を修正しないほうがいいということも覚えておこう。
  2. Python は Lisp ほど関数的でない。 リストが cons でないせいもあるが、Python のほうが Lisp よりも破壊的な関数を 使うことが多い。そしてこれらの関数はその操作が破壊的であることを強調するため、 多くの場合、None を返すようになっている。for x in list.reverse() は できそうに思えるかもしれないが、実際には Python の reverseNone を返す nreverse のようなものだ。これは いくつかのステートメントに分けておこなうか、自分の reverse を 書く必要がある。reverse 以外にも、removesort に ついても同じことがいえる。
  3. Python のクラスは Lisp より関数的である。 Lisp (CLOS) では、あるクラス C を再定義すると C を表すオブジェクトも 変更された。したがって、それまでに存在した C のインスタンスやサブクラスは 新しいクラスにつなぎ換えられた。ときにこれは問題をひき起こすが、 対話的にデバッグしているときは、これは望まれる動作だろう。Python では、 新しいクラスを定義して新しいクラスオブジェクトを手にしても、それまでの 古いインスタンスやサブクラスは依然として古いクラスを見ている。つまり クラスを再定義するたびに、ほとんどの場合はサブクラスを再読み込みしたり データ構造を再構築しなければならない。このことを忘れると混乱のもとになる。
  4. Python は Lisp より動的で、エラーチェックが少ない。 Python では未定義の関数や未定義のフィールド、関数に渡す引数の 個数間違いなど、読み込みのさいにはどれもほとんど警告されない。 これらの警告には実行時まで待たねばならない。商用の Lisp 実装はこれらの警告を たくさん出すことができる。clisp のような簡単な実装では出ない。 Python のほうが明らかに危険であることを示す例として、 self.field = 0 とするつもりで self.feild = 0 と やったときなどがあげられる。後者の場合、新しいフィールドが動的に 作られてしまう。Lisp では同じことをしても (setf (feild self) 0) は エラーを返すだろう。いっぽう未定義のフィールドに対するアクセスは どちらの言語もエラーになる。
  5. self を忘れるな。 これは Lisp プログラマ向けというよりはむしろ Java プログラマ向けの注意である。 メソッドの中では、かならず self.field のように書き、field のようには 書かないこと。暗黙のスコープというものはない。ほとんどの場合これは ランタイムエラーになる。いらいらするが、しばらくすれば慣れると思う。
  6. return を忘れるな。 def twice(x): x+x のような書き方は格好いいし警告や例外も出さないのだが、 たぶんあなたのやりたかったことは直前に return をつけることだろう。 これは特にやっかいだ。なぜなら、lambda の中では return をつける ことは禁止されているからだ。にもかかわらず、意味的には lambdareturn を含んでいる。
  7. ひとつだけのタプルに気をつけろ。 タプルとはただの変更不能なリストであり、角カッコ [ ] ではなく丸カッコ ( ) によって表現される。 () は空のタプルであり、(1, 2) は 2つの要素をもった タプルなのだが、(1) はただの 1 なのだ。かわりに (1,) を使わなければならない。うげー。Damian Morton はわたしに、 タプルというものは丸カッコとともに表示されはするものの、実は カンマによって構成されていると考えれば意味が通ると教えてくれた。 カッコはただグループの曖昧さをなくすためにあるのだと。 この解釈によれば、1, 2 は 2つの要素をもったタプルで、 1, は 1つの要素をもったタプルで、カッコはタプルが 現れる場所によってそのときどきで必要だ、ということになる。 たとえば 2, + 2, は正しい表記だが (2,) + (2,) または (2, 2) を使ったほうがよりクリアーになるだろう。
  8. ある種の式に気をつけろ。 dict[key]key が存在しないときは KeyError 例外を 発生させることに注意。Lisp のハッシュテーブルを使っていたユーザなら nil を期待するところだ。この例外を捕捉するか、dict.has_key(key)key in dict、あるいは dict.get(key) を使うべし。
  9. Python は Lisp-1 である。 これが意味するところは、 Python は Scheme のように ひとつの名前空間を関数と変数に使っているということだ。 Common Lisp のように別々にはしていない。たとえば:
    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
        ...
    
  10. Python 2.1 以前は字句的スコープ (lexical scope) をサポートしていない。 バージョン 2.1 以前の Python では、各モジュールには 2つの 変数スコープしかなかった。グローバルなスコープと、関数におけるスコープである。 2001年 4月にリリースされた Python 2.1 では、 "from __future__ import nested_scopes" を実行することにより 第 3 のスコープである、ブロック中での入れ子になった変数スコープを使うことができる。 Python 2.2 では、この動作はデフォルトになっている。これは望むところだろう。 これで、外側の変数を参照する閉包をつくることができる。外側の変数の内容を 変更したい場合、選択肢はすこししかなく、どれも少々手がかかる。 変数を 1要素のリストでくるんでおくのはひとつの方法だ。
    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)
        }
    }
    
  11. Python の文字列は Lisp のシンボルとは全然ちがう。 Python は、モジュールやクラス中に存在するハッシュテーブルに 文字列を intern させることでシンボルの検索をおこなっている。 つまり obj.slot と書くと、Python は実行時に クラス obj のハッシュテーブルで文字列 "slot" を 検索する。また、Python はユーザのコード中にある x = "str" などの ようないくつかの文字列も intern する。だが変数らしく見えない文字列は intern されない。たとえば x = "a str" は intern されない (このことを指摘してくれた Brian Spilsbury に感謝)。
  12. Python にはマクロがない。 Python はプログラムの抽象的な構文木にアクセスする機能を もっているが、これはなにもびっくりするようなことではない。 このモジュールの長所は理解するのが簡単だということで、 5行ほどのコードを使って、5分かそこらで次のようなものを手にすることができた:
    >>> 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 プログラムと Python プログラムの比較

最初のサンプルプログラムは Paradigms of Artificial Intelligence Programming からとってきた、簡単なランダム文生成器 を Python に翻訳したものだ。結論: 簡潔さという点では似たようなものである。 grammar[phrase](rule-rhs (assoc phrase *grammar*)) より 簡単だという点では Python のほうがまさっているが、['NP', 'VP']'(NP VP) と書ける点では 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], [])

Peter Norvig

Japanese Translation Changes:
公開。(2002/10/13)
深町 和哉さん, Shiro Kawai さん, 木村 栄伸さん から指摘をいただき修正しました。(2002/10/27)
Yusuke Shinyama <yusuke at cs . nyu . edu>