魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

例の Python 難読クイズが難しかった

例の問題

これです。

list(map(list, list(map(map, map(lambda map: list, map := "map"), map))))

以下、ネタバレです。

0. 答え

実行するとこうなります。

これが答えです。

[[['m']], [['a']], [['p']]]

当然私はわからなかったので、REPLに打ち込んでどうなるか試してみたのですが、少し奇妙なことが起きます。

>>> list(map(list, list(map(map, map(lambda map: list, map := "map"), map))))
[[['m']], [['a']], [['p']]]
>>> list(map(list, list(map(map, map(lambda map: list, map := "map"), map))))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable

2回実行すると、エラーになります。 難読ゆえ最初は写し間違えかと思いましたが、そうじゃないのです。

では何が起こっているのか、このコードを読み解いてみましょう。

1. 一番内側

一番深い場所にいるらしい箇所から順に考えていくことにします。

map(lambda map: list, map := "map")

まだややこしいので前半と後半に分けてみましょう。

1-1. 一番内側 1

lambda map: list

lambda 式です。挙動を見てみましょう。

>>> (lambda x: list)("foo")
<class 'list'>
>>> (lambda x: list)(1)
<class 'list'>

なんでもいいけど一つだけ引数を渡すと、型コンストラクlist が返却されますことが分かります。 よくみると lambda 式に渡された引数の中身は全然見ていません。

型コンストラクタは、まぁ特定の型を作るためのコンストラクタですね。

>>> list("foo")
['f', 'o', 'o']

これは「list という関数でリスト型にキャストしている」とも解釈できますし「list というコンストラクタに str 型変数 "foo" を渡すことでリスト型の変数を作った」とも解釈できるわけです。

先ほどの例1ではこのコンストラクタが返されていたわけですから、for文を実行できる適当な変数を追加で渡すことができます。

>>> (lambda x: list)("foo")("hoge")
['h', 'o', 'g', 'e']

まとめるとlambda map: list は『list に変換してくれる関数(型コンストラクタ)を返す』です。

また、ここに含まれる map は単なる変数名ということが分かります。 なんでそんなことをしているかというと、ズバリ読みにくくするためですね。

1-2. 一番内側 2

次に、map := "map" に注目します。 := がまず見慣れない点かと思います。 これはセイウチ演算子とも言われる記法で、Python3.8 から導入された比較的新しい記号です。

docs.python.org

一般には、if 文の条件式の中で変数を定義する時に使われます。

>>> a = [1, 2, 3]
>>> if (foo := len(a)) > 2:
...     print(foo)
... 
3

カスみたいなサンプルコードですが、こう書いたり

>>> a = [1, 2, 3]
>>> foo = len(a)
>>> if foo > 2:
...     print(foo)
... 
3

こう書いたりしなくてもよいわけです。

>>> a = [1, 2, 3]
>>> if len(a) > 2:
...     print(len(a))
... 
3

もう一つ大事なこととして、この := で代入された変数はローカルじゃないということです。 つまり、if 文の外からも参照できます。

>>> a = [1, 2, 3]
>>> if (foo := len(a)) > 2:
...     print(foo)
... 
3
>>> foo
3

それを踏まえて map := "map" に戻ると、 map という変数名に文字列 map を代入していることになります。 こうすると、同じスコープ内では map が map 関数ではなく文字列リテラルになってしまいます。 (組み込み関数の定義を書き換えてしまうことになるので、普通はやらない方がいい)

ちなみにこれこそが2回目実行した時にエラーになる原因です。 要するに map 関数を使おうとしても文字列型リテラル"map" になってるから「TypeError: 'str' object is not callable(文字列は呼び出し可能ではない)」というエラーが出るわけです。

1-3. 一番内側 3

ようやくここに戻ります。

map(lambda map: list, map := "map")

さっきの結果を踏まえるとこうです。

map({1-1.の結果}, {1-2.の結果})

みなさん大好き map 関数です。 よくある使い方だとこうです。

# 例2
>>> a = [1, 2, 3]
>>> list(map(lambda x: x + 1, a))
[2, 3, 4]

第一引数は関数、第二引数がイテラブル(簡単にいうとfor文で回せるオブジェクト)です。 第一引数に指定した関数を第二引数のイテラブルに順次適用していきます。 結果としては、「順次適用した結果を1個ずつ返す準備ができた」オブジェクトを返します。(ジェネレータと言われるやつです)

上の例2では最後に list に入れることで、「準備ができたオブジェクト」から「要素を一つずつ最後まで取り出してリストの要素にセットする」操作を行い、結果 [2, 3, 4] が得られているわけです。

そういうわけでまとめると、

map(lambda map: list, map := "map")

こいつは「第一引数『list クラスの型コンストラクタを返す関数』に第二引数『文字列 "map" が代入された変数 map 』を順次適用する準備ができたオブジェクト」を返すことが分かります。

「準備ができたオブジェクト」だと分かりにくいので、この結果を list に渡した結果を見てみましょう。

>>> list(map(lambda map: list, map := "map"))
[<class 'list'>, <class 'list'>, <class 'list'>]

こうなります。『list クラスの型コンストラクタを返す関数』が "map" の文字数分だけループされているので、『list クラスの型コンストラクタ』が三つになっています。

2. 一つ外側

一つ外の階層に進みましょう。

map(map, map(lambda map: list, map := "map"), map)

先ほどの説明を踏まえると、こうです。

map(map, {1-3.の結果}, map)

つまりこうです。

map(map, {listの型コンストラクタを最大三つ返せる何か}, map)

よくみると、map 関数に三つ目の引数があります。 これが、二つ目のポイントです。

実は、map 関数は第一引数の関数の引数が複数ある場合、第二引数以降にその分だけ指定する必要があります。

つまりこういうことです。

>>> list(map(lambda x, y: x * y, [1,2,3], [10,20,30]))
[10, 40, 90]

第一引数の lambda 式に引数が x, y と二つあるので、適用対象のイテレータも二つ([1,2,3][10,20,30])渡す必要があります。

もちろん、lambda 式の引数が3つだと、イテレータは3つ渡す必要があり、第四引数まであるように見えます。

>>> list(map(lambda x, y, z: x * y + z, [1,2,3], [10,20,30], [100, 200, 300]))
[110, 240, 390]

では問題に戻りましょう。

map(map, map(lambda map: list, map := "map"), map)

第一引数は map 関数です。map 関数は少なくとも引数を二つ取りますから、上の式で一番外側にある map の引数は3つ以上必要だと分かります。(ややこしい!)

ここで、一番外側にある map の第三引数 map はどう考えたらよいでしょうか?

これが3つ目のポイントです。

1-2. 一番内側 2 で説明した通り、map は文字列リテラル "map" なのです。

つまり、

map(map, map(lambda map: list, map := "map"), map)

こいつは「第一引数『map 関数』に第二引数『list クラスの型コンストラクタを最大3つ返す準備ができたオブジェクト』と第三引数『文字列 "map" が代入された変数 map 』のペアを順次適用する準備ができたオブジェクト」を返すことが分かります。

これらを実際にループするところを想像すると、第二引数をループしたときに出てくる要素『list クラスの型コンストラクタ』が第三引数 "map" をループした時に出てくる要素 "m""a""p"に順に適用されることになりますから、結果として ["m"]["a"]["p"] を順に返すことが分かります。

言葉で言うとややこしいので、実際にみてみましょう。

>>> for a in map(map, map(lambda map: list, map := "map"), map):
...     print('----')
...     for i in a:
...             print(i)
... 
----
['m']
----
['a']
----
['p']

ここで当然の疑問として、第三引数の map が文字列リテラルなら、第一引数も map もそうじゃないの?としてその外側にある map もそうじゃないの?と思い浮かびます。

ですが、書かれている位置が map := "map" よりも後ろなのが第三引数の map だけだということをに気付けば、他の箇所が文字列扱いされないことは何も不思議ではありません。

実際に2回目の実行からは全部の map が文字列リテラルだと認識されたことによってエラーになっています。

3. もう一つ外側

だいぶ近づいてきました。

list(map(map, map(lambda map: list, map := "map"), map))

今までを踏まえるとこうですね。

list({2.の結果})

シンプルに list へ渡していますので、その通りやってみましょう。

>>> list(map(map, map(lambda map: list, map := "map"), map))
[<map object at 0x1022abb50>, <map object at 0x10228f370>, <map object at 0x1022bc040>]

直前にfor文で確かめた例と見比べると、まぁその通りだろうと言う感じがします。 一つ目の map オブジェクトは ["m"] を、二つ目の map オブジェクトは ["a"] を、三つ目の map オブジェクトは ["p"] を返す準備ができています。

4. さらにもう一つ外側

後もう少しです。

map(list, list(map(map, map(lambda map: list, map := "map"), map)))

つまりこうです。

map(list, {3.の結果})

3. でわかった内容を代入してみましょう。

map(list, [<map object at 0x1022abb50>, <map object at 0x10228f370>, <map object at 0x1022bc040>])

3. でみた通り、map の中身のイメージはこうなっています。

map(list, [ループしたら["m"]を返す, ループしたら["a"]を返す, ループしたら["p"]を返す])

これを実行すると、第一引数の list が順次適用されます。 つまりループした結果の出力を順番に並べてリストで囲ったものが得られます。

例えば一つ目の「 ループしたら ["m"] を返せる 」は [["m"]] になります。

「ループしたら ["a"] を返す」,「 ループしたら ["p"] を返す」についても同じですので、結果として

map(list, list(map(map, map(lambda map: list, map := "map"), map)))

ループしたら [["m"]], [["a"]], [["p"]] を順に返せるオブジェクト

を意味することになります。

5. 一番外側

最後です。

list(map(list, list(map(map, map(lambda map: list, map := "map"), map))))

つまりこうですね。

list({4.の結果})

4. の最後に書いたものを list クラスの型コンストラクタに渡します。

list を適用すると、引数を全部順番にループした結果の出力をまた同じ順番に並べて最後に [] で囲ったものが得られます。

したがって、結果が得られます。

[[["m"]], [["a"]], [["p"]]]

まとめ

頑張って流れをまとめてみた。 関数適用のタイミングとかで盛大に勘違いをしていそうな気もするが、多分こんな感じだと思います。

多分こう。

参考