いかなる副作用もなく計算することができます
タイトルはPythonドキュメントからの引用です。
ふぅ。
やっと戻ってきました。
副作用についてごちゃごちゃと書きましたが、そもそもはこの「リスト内包表記の副作用」からでした。
副作用についてはこちら。
くどいようですが、Pythonドキュメントの「リスト内包表記」には次のように記載されています。
「いかなる副作用もなく計算することができます」
では、リスト内包表記の副作用がないとはどういうことでしょう。
リスト内包表記
リスト内包表記は次のようなものです。
a = [i for i in range(0, 4)]
これは次の表記と同じ意味です。
a = [0, 1, 2, 3]
リスト内包表記を使わないとこうなる
例えば、リスト内包表記を使わずに書くとこうなります。
def test(n):
a = []
for i in range(0, n):
a += [i]
return a
もしも・・・。
この関数に変更が必要だとします。
それでちょっと人に頼みました。
「関数の先頭で“a”を print で表示しておいて」
頼まれた人は次のように変更しました。
def test(n):
a = []
print(a)
for i in range(0, n):
a += [i]
return a
ん。
んんん?
「print」のタイミング、ここ?
でも、関数の先頭であることは間違いないですよね。ここで“a”を表示して意味があるのかという疑問はあるけれど、指示されたとおりであるように見えます。
副作用については既にさんざん述べました。
「a += [i]」⇐これはまさに副作用です。
「a」は、「a = []」によって既に一旦空のリストとして生成されています。「a += [i]」はそれを上書きしました。これはとりもなおさず「a」の状態を変更することであり副作用を意味します。
変数の実体と初期化って、離れすぎ
上記は簡単なサンプルなので
「まさかこんなとこに print 入れるヤツなんかおらんやろぉ」
と思うかもしれませんが、実体の定義と初期化って意外に分離しています。特にC言語などはまさにそうで、こともあろうにスコープの先頭でしか変数を宣言できません。
変数の宣言をズラズラ並べてからやっと
変数の初期化ができるようになります。
長い関数になると、
どこで宣言して、
どこで初期化して、
どこで参照しているのやら、
と、こんな事態にもなりかねません。
まぁ、そこまでいくと関数の設計そのものに問題があるようにも思えますが。
以上がオート変数の事情で、グローバル変数になると実体宣言と初期化の距離はさらに遠い。。。 grep で検索してようやく発見できるという有り様です。
C言語からの派生である C++ もあまり状況は変わらなかったのですが、最近はクラスのメンバ変数を
int m_member = 2;
という感じで初期化できるようになったようです。
それでも、やはり、配列のような複数データを持つ変数を宣言時に初期化するのは面倒です(何十個も羅列する?)。
そこで、リスト内包表記!
先程の例をリスト内包表記で書くとこうなります。
def test_com(n):
a = [i for i in range(0, n)]
return a
ここでもう一度、件の方に改修をお願いしてみましょう。
「関数の先頭で“a”を print で表示しておいて」
def test_com(n):
a = [i for i in range(0, n)]
print(a)
return a
うん。
これより前にはもって行けませんよね。
さらに手前に書くとシンタックスエラーになってしまいます。
宣言=初期化だから、この間が分離されることはありません。分離できない。
素晴らしい!
「a」には2つも3つも状態はありません。
すなわち「いかなる副作用もない」のです。
さて次回は・・・
Pythonのチュートリアルから
「18 lines: 8-Queens Problem (recursion)」
を紹介します。
実は、リスト内包表記、再帰呼出、副作用についてはこのチュートリアルから出発している。
長い道のりでした(笑)。