凡例:
n 階乗にする数
fn n!の値
fact 階乗を返す関数
■ループを使った方法
特長:直観的、シンプル
難点:グローバル変数の再代入
⇒ 関数でラッピングする
<forループを使用した例>
fn = 1
for x in range(1, n+1):
fn *= x
コメント:Guido師の勧める方法。直観的かつシンプルで分かりやすい(Py禅的)。
一時カウンタとしてxを用いているが、一文字(小文字)変数をグローバ
ルで引っ張るようなプログラムを書いていたなら、それはそれで問題な
ので、ラッピングしなくてもほぼ問題無いだろう。
<forループの改良例>
fn = 1
for x in range(1, n+1):
fn *= x
else:
del x
コメント:Pythonではループがbreakや例外などで中断されない限り、ループ終
了後else節が実行される。nとfnの使い道はともかく、カウンタに使用
する変数を最後に消去しておけば、もし他所でこの変数を利用している
場合に参照エラーになる。別の値が入っているより、参照エラーが出る
ほうがバグを検知しやすい。
<whileループを使用した例>
fn = 1
while n:
fn *= n
n -= 1
コメント:whileを使ったループ。構造が単純であり、かつ基本的な条件分岐な
ので、私が初心者に教えるのなら、これを勧める。ただし、一時カウン
タの代わりにnの値が変化してしまうので注意が必要。実際にコードの
中で使用するのであれば、nの値を何かに代入するか、関数でラッピン
グした方がいいのかもしれない。
<whileループの改良例>
fn = n, 1
while fn[0]:
fn = fn[0] - 1, fn[1] * fn[0]
else:
fn = fn[1]
コメント:タプルを用いて、fnの中に情報を詰め込んだ例。nは変更されていな
い。タプルによるパッキングはPythonでは通常のテクニックだが、思い
つくにはやや慣れが必要。裏ワザというほどのモノではないが、Python
に慣れていないと、やや読みづらいコードかもしれない。
■functools.reduceを使った方法
特長:
慣れれば直観的
グローバル変数の汚染が無い
組み込み関数なので早い
難点:
λを用いるので、中〜上級者向け
Guido師はあまりお好みではない
<functools>
from functools import reduce
fn = reduce(lambda x, y: x * y, range(1, n + 1))
コメント:Pythonistaにとってはお馴染みの方法だが、λ異端審問(Pythonista
は異端審問が大好き?)によって、標準関数から『組み込みモジュール
関数』に格下げになったreduceを使う以上、Pythonの正道から外れると
の謗りからは逃れられない?(トビト書を説教で使う神父みたいなモン
かな?)改良の余地は、多分全く無い。
■単純再帰を使った方法
特長:
再帰関数を用いる
難点:
再帰によるスタックオーバーフローの危険
<再帰関数を使った例>
def fact(x):
if x < 2:
return x
else:
return fact(x-1) * x
fn = fact(n)
コメント:再帰関数を使う例の中でも、素直な例。再帰の勉強に最適。もっとも、
メモリ効率が悪い上、再帰につきもののスタックオーバーフローは避け
られない。再帰関数の勉強専用の例とも言える。なお、日本語版の
Wikipediaに載っているPythonのサンプルコードはコレである(若干終
了条件などが違うが)
<lambdaで実装した再帰関数の例>
【その1】
fact = lambda x: x if x < 2 else fact(x - 1) * x
fn = fact(n)
【その2】
fact = lambda x: x < 2 and x or fact(x - 1) * x
fn = fact(n)
コメント:関数をlambdaで定義しただけの例。【その1】は条件付評価式
(if-else式)を使用した例。【その2】はand/orを利用したクラッシッ
クな書き方。and/orの場合は、andの後が真であるように設定しないと
誤作動する。これもlambda式などの勉強用には使えるが、実用には何の
メリットもない(lambdaを導入しても、名前を付けているのであまり意
味は無い)やはり勉強専用コードだろう。
■末尾再帰の再帰関数を使った方法
特長:
末尾再帰関数を用いる
難点:
末尾最適化しないPythonではあまり意味が無い
スタックオーバーフロー
<末尾再帰をとりいれた例>
【その1】
def fact(x, e=1):
if x < 2:
return e
else:
return fact(x-1, e*x)
fn = fact(n)
【その2】
fact = lambda x, e=1: e if x < 2 else fact(x-1, e*x)
fn = fact(n)
コメント:末尾再帰版。末尾再帰が最適化されるLispやSchemeならともかく、
Pythonではほぼ意味なし。Lispの勉強の役には立つかもしれないが、
Pythonコードとしてのメリットは無い。
■Yコンビネータを使った再帰関数の方法
特長:
グローバル一時変数や関数名を使用せずに再帰が書ける
難点:
読みづらさクライマックス
<Yコンビネータを使用した例>
【その1】
Y = lambda f:(lambda x: lambda m: f(x(x))(m))(lambda x: lambda m: f(x(x))(m))
fn = Y(lambda f: lambda x: x if x < 2 else f(x -1) * x)(n)
【その2】
Y = lambda f:(lambda x: lambda m: f(x(x))(m))(lambda x: lambda m: f(x(x))(m))
fn = Y(lambda f: lambda x: lambda t: t if x < 2 else f(x-1)(t * x))(n)(1)
【その3】
fn = (
lambda f:(
lambda x: lambda m: f(x(x))(m)
)(lambda x: lambda m: f(x(x))(m))
)(
lambda f: lambda x: lambda t: t if x < 2 else f(x-1)(t * x))(
n
)(1)
コメント:Yコンビネータ(不動点演算子)を取り入れた例。【その1】はこれ
を単純再帰版に、【その2】は末尾再帰版に適用した例。【その3】は
その末尾再帰版を、Yの名前無しに書いた。グローバル変数には一切変
更は無いが、複雑怪奇で実用性は皆無。勉強、というか頭の体操やパズ
ル用、あるいは数学のλ式の演習くらいにしか利用法は無い。ただし、
Yコンビネータ自身は定義しておいて使うと意外に便利。
■リスト内包を使った方法
特長:
グローバル一時変数は無い
スタックオーバーフローしない
一行で書けないこともない
難点:
見づらい、解りづらい
再帰以外の裏技のオンパレード
メソッドフックへのアクセス
条件付評価式の悪用(不使用値)
ループしないforをカウンタに使用
最後以外使用しないリスト
メモリの浪費
<リスト内包を使った(悪用した)例>
fn = [0 if p.__setitem__(0, p[0]*x)
else p[0]
for p in [[1]]
for x in range(1, n+1)
][-1]
コメント:『これでもまだλよりマシだと言うのか?』と言わんばかりのえげつ
ない裏技コード。一時変数導入目的で入れたループしないpの内部を
__satitem__を用いて裏側から書き換え、ターゲットの数値になるまで、
順々に足していく。もともとリスト内包はリストを生成する目的なので、
刻々と変化する状態を正直に記録していく。しかし結果で必要なのは最
後の数値なので、最後にばっさり切られる。基本構造はforループなの
でスタックは消費しないが、メモリの浪費がはなはだしい。
<リストではなく一時変数を無名クラスオブジェクトに置き換えた例>
fn = [0 if setattr(p,'t', p.t * x)
else p.t
for p in [type('',(),{'t':1})()]
for x in range(1, n+1)
][-1]
コメント:とりあえず__setitem__ではなくsetattrにして、少しだけ見栄えを改
良したもの。一行目はさっきのものよりは若干マシか。しかし『
type('',(),{})()』でPythonで無名クラスオブジェクトを作るTipsなど、
黒魔術の領域だ。決して若いPythonistaの手本にはならない。
<ジェネレータを使用してメモリを節約した例>
for fn in (0 if setattr(p,'t', p.t * x)
else p.t
for p in [type('',(),{'t':1})()]
for x in range(1, n+1)
):pass
コメント:無駄なリストを生成するのは無駄が多いので、とりあえずジェネレー
タにしてforループでfnに次々代入することにより、無駄を省いたもの。
メモリはともかく、最早、何の言語かすら判別不可能。
、