魂の生命の領域

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"]]]

まとめ

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

多分こう。

参考

現代思想なるものに少しだけ触れた

なぜか自分はライトな現象学オタクみたいな感じなのですが、直後に上げる千葉雅也氏の本を読んだところから、少しだけ現代思想にも興味が湧いて、ちょっとだけ本を読みました。

現代思想入門 (講談社現代新書) / 千葉 雅也

bookmeter.com

まぁ読んでおいて損はないだろう、という感じで読みました。

軽妙な文体で少し慣れが必要でしたが、逆にこの砕けているがごまかしはない感じの文章はすごいぞと思いながらば〜〜〜っと完読しました。

デリダはすごく嫌味ったらしい文章を書くらしいことを覚えました。

ですが、この後に続く読書体験により、デリダはもうちょっと深掘りしてみようかなと思うようになりました。

構造と力-記号論を超えて (中公文庫 あ 51-2) / 浅田 彰

bookmeter.com

ちょうど2023年の年末ぐらいに出版から40年が経ってついに文庫化される!しかも巻末の解説は千葉雅也氏!みたいなのを偶然知ったので、これも読んでおいて損はなかろうという感じで読みました。

この辺りで現代思想って…う〜ん…という感じになってきます。

著者は当時26歳だったというのを知ってビックリしました。確かに衒学的なところというか、「周知のように」とか「〜〜であることはいうまでもない」とか「〜〜に他ならない」みたいな表現を多用するのは若気の至りという感じがしますね。いや知らんてwっていう。

個人的には、第1章が一番面白いと思いました。

総集編って感じで楽しいです。(今、書きながら「ニコニコ動画流星群」を聴いてる感覚に近い、という例えを思いつきました。共感してくれる人がいると嬉しい)

第6章はちょっと…ね。

私はぼんやりこれをポストモダン思想の解説書だと思って買って読み始めたのですが、実際にはポストモダンポスト構造主義に至るまでの構造主義の解説がほとんどです。(と言ってもジャンル厨になるのはアレなのでこの辺の区別はあまりしっかりと把握してないですが)

最後の第6章がポストモダンについての実質的な導入みたいな章です。
ドゥルーズフーコーの思想をベースにした、管理社会からの逸脱によってもっと楽しく自分らしく生きようぜ!的なおそらくバブル期とも相まったであろうこれからの社会への展望が謎の歌詞を大量引用するスタイルで語られるぐらいです。

なにこれ?

とはいえ、たまに思い出して所々拾い読みしたくなる(拾い読みできる)感じがあります。1冊は家に置いておきたい感じ。

「知」の欺瞞――ポストモダン思想における科学の濫用 (岩波現代文庫) / アラン・ソーカル,ジャン・ブリクモン

bookmeter.com

『構造と力』を読み終えたあたりでだいぶ自分の中に溜まっていたモヤモヤがすでに本としてまとまっていることを知りました。

それがこれです。

(月並みすぎる説明ですが)あの有名なソーカル事件の背景について書かれた本です。

ja.wikipedia.org

私は一応、学生時代は物理学を専攻しており(留年した上に論文も学会発表もないカス of カスの存在しないに等しいものの)修士号を持っておりやして、ものの考え方自体はこの著者のアラン・ソーカル氏と同じ系統です。

あと訳者の一人にあの田崎先生がいてビックリしました。統計力学 I と II にはお世話になりました。

本書はポストモダン思想とそれに影響を与えた何人かの有名な哲学者・思想家の論文のなかに現れるハチャメチャな理解に基づいた数学や自然科学のアナロジー(もしくは本人曰く『厳密な等価性』)を引用してその不正確さを指摘するものです。

特に、ある種のテクストが難解なのはきわめて深遠な内容を扱っているからだという評判を「脱構築」したいのである。多くの例において、テクストが理解不能に見えるのは、他でもない、中身がないという見事な理由のためだということを見ていきたい。

アラン・ソーカル,ジャン・ブリクモン p. 8

ジャック・ラカンがやたらとトポロジーに入れ込んでいて、最終的には結び目理論についても言及し始めるところなどは、正直まぁ私も似たようなものだと思ってちょっと親近感を覚えました(私は大学院でいわゆるトポロジカル物性をやっていたので…)。

完全に意味不明なものと、明らかに間違っているのがわかるものに2種類あるように感じました。

私は量子力学にちょっと詳しい程度でそれ以外の分野はほぼ雑学程度の知識しかないですが、それでもパッと見で解釈がおかしいのが分かるものがあるのは驚きです。

あるいは、言及されている数学用語の正しい解釈は知らないけど、少なくともそのアナロジーに意味はないでしょってなるところもありますよね。

これは『構造と力』でもそれに近いものが散見されましたが、ただ言葉が似ているだけで急に関係ない分野の結論だけ借りてくる系のやつです。しかも借りてくる先は仮にも厳密な数学や(今のところ正しいとされている)物理学の結果ですから、その『正しさ』だけをいきなり移植してくるのが非常に厄介だと思います。

いくつか引用してみます。

一つ目。

しかし、われわれの見る例では、(自然科学における)確立した理論と、(ラカン精神分析学のような)あまりに漠然としていて経験的に検証しようがないような理論のあいだのアナロジーが議論されていると考えられる。このようなアナロジーの役割は漠然とした理論の薄弱さを隠すことなのではないかと疑わざるをえない。

同 p. 16

もう一つ。

不明瞭なものが全て深遠なわけではない。 (略)ある種の難解な言説は、それを理解するために読者に思考の質的な跳躍や天啓のような体験を要求しているという印象を与える。またしても、裸の王様の物語を思い出さずにはいられない。

同 p. 276

最後にもう一つ。

科学は「テクスト」ではない。 自然科学というのは、人間科学ですぐに使うことのできるメタファーを集めた倉庫ではない。

同 p. 277

本当はこれ以外にも論点はたくさんあるんですが、一番自分的に理解しやすかったところを引用してみました。

本書は、ただのトンデモ事例のコレクションといったものではなく、「これらの間違った数学や自然科学の濫用がいかに危険か」ということが主題となっています。

例えば、自然科学は人々の信念の体系にすぎず、社会的なものである、という相対主義の極端なスタンスの危険性について論じています。

ちなみに、巻末には問題となったソーカルのパロディ論文の和訳といくつかの種明かし的な解説が付されています。

本書を読む前にこのパロディ論文を読むと、完全に意味不明な文章の羅列に見えてしまうかもしれません。

しかし、本書を読んだ後で読むとそのパロディ論文が何を皮肉っているのかがよくわかります。

単に意味不明な科学用語、数学用語を羅列して「ほら、論文っぽいでしょ?」というものでは決してないのです。

要するに、 パロディ論文の大半は本書で紹介された怪しい主張の引用を肯定的に取り上げたものに過ぎない わけです。

おそらくここら辺を理解せずに「ソーカル事件」を考えようとすると、話がおかしくなります。

最後のエピローグでは、これらの政治的な側面としての問題点が語られます。

科学の客観性を疑わない合理主義に対してその特権性を突き崩す新しい勢力、みたいな感じで一部の左派が実質的に反知性的な性格を帯びてしまっているということです。

例えば従来の自然科学は男性中心の社会の中で作られてきたから、女性的な側面のある(?!)流体力学は解けない問題として置き去りにされていた、とかそいういうのです。

これによって一番被害を受けるのは、自然科学ではなくむしろ社会科学の方だということが述べられています。

要するにガバガバな理解に基づいた自然科学の引用と、自然科学の正しさも社会的な構築物に過ぎないとかいう何でもあり論によって、社会科学自体が実際に何でもありの状態になってしまいます。

そんな学問全然信用できないじゃん、となれば社会科学はおしまいです。

中身のある議論ができるようになるといいですね。

nヶ月間保管されるデータの月額保存料金から当月の増加分に想いを馳せる 2

kesumita.hatenablog.com

オタクはすぐ何でも一般化する

前回の記事では、前月と当月のストレージ増分の変化(差や比)が一定の値になるパターンを考えました。

より一般的に考えるとどうなるでしょうか。

システムの運用開始月のストレージ量を s_1、その翌月が s_2、... というようにすると、 データの保持期間を n ヶ月とした場合の運用開始から i ヶ月目のストレージ量 \displaystyle{S^ n_ i}

\begin{align}
S^n_i = \sum_{j=1}^n s_{i-j+1} = s_i + s_{i-1} + \cdots + s_{i-n+1}
\end{align}

となるでしょう。

前回の記事の前提に則ると、知ることができるのはこの n ヶ月分の合計だけになるので、ここから話を取る前の \displaystyle{s_ i} を求めましょうという問題になります。

s_i を構成する要素を考える

このストレージ量 \displaystyle{s_ i} に主に寄与する要因は何かを考えます。

例えば適当なECサイトを考えると、アクティブユーザー数や商品数などが挙げられると思います。

まぁ EC サイトエアプ勢なので解像度の低い理解なのは何卒ご容赦ください…。

ともあれ、\displaystyle{s_ i} が m 個の主要なファクター \displaystyle{a _ {i1}, a _ {i2}, ..., a _ {im}} でほぼ表せたとしましょう。そして、これらのファクターは測定可能だとします。

\begin{align}
s_i = s_i(a_ {i1}, a_ {i2}, ..., a_ {im})
\end{align}

…このままだと何もできないので、ここから一般化して広げた風呂敷を畳みにいきます。

それぞれ一次の寄与が支配的だとしましょう。要するに \displaystyle{a _ {i1}, a _ {i2}, ..., a _ {im}} の線型結合で表せるとしましょう。

\begin{align}
s_i = \sum_{k=1}^m c_{ik}a_{ik}
\end{align}

そうすると、\displaystyle{S^ n_ i} は、

\begin{align}
S^n_i & = \sum_{j=1}^n \sum_{k=1}^m c_{i-j+1, k}a_{i-j+1, k} \\
& = \sum_{k=1}^m c_{ik}a_{ik} + \sum_{k=1}^m c_{i-1, k}a_{i-1, k} + \cdots + \sum_{k=1}^m c_{i-n+1, k}a_{i-n+1, k}
\end{align}

です。

もっと単純化して、一つの支配的なファクターとその係数だけで十分近似できるとしてみます。 要するにこういうことです。添字 \displaystyle{c _ {im}} の m 依存性はなくしました。

\begin{align}
s_i = c_{i}a_{i}
\end{align}

そうすると、\displaystyle{S^ n_ i} は、

\begin{align}
S^n_i & = \sum_{j=1}^n c_{i-j+1}a_{i-j+1} \\
& = c_{i}a_{i} + c_{i-1}a_{i-1} + \cdots + c_{i-n+1}a_{i-n+1}
\end{align}

ですよね。

で、この式は何を意味するのでしょうか?

\displaystyle{a _ {1}, a _ {2}, ..., a _ {n}} は測定できる値だと仮定したので、ストレージ量に一番作用しそうだと決めたファクター(アクティブユーザー数、商品数、とか)の値を n ヶ月分集めることが必要です。

そうやって集めた値を代入して、未知数 \displaystyle{c _ {1}, c _ {2}, ..., c _ {n}} を求めることになります。

なので、保存期間の分だけ未知数がある方程式を解かないといけないことになります。

やっぱりこれ以上は何もできないので、当月と前月の総ストレージ量の差分を取ります。

\begin{align}
S^n_i - S^n_{i-1} = c_{i}a_{i} - c_{i-n}a_{i-n}
\end{align}

これは前回の記事の冒頭で、次のように書いていた箇所のことを言っているだけですね。

  • 当月:10月・9月・8月
  • 前月:9月・8月・7月

となって 当月 - 前月 = 10月 - 7月 のようにオマケの項「7月分」がついてくるからです。

結局、ある月のストレージ量と最も寄与すると考えるファクターの比が、当月分 \displaystyle{c _ i = \frac{s_i}{a_i}} とnヶ月前の分 \displaystyle{c _ {i-n} = \frac{s _ {i-n}}{a _ {i-n}}} について必要になるので、未知数が2つで式が1つの方程式になりそのままでは解けない訳です。

なんか振り出しに戻った気がしますね。

風呂敷をさらに畳む

もっと単純化してみます。

ファクターに対する係数が、毎月一定の値 c であるとします。

当月の総ストレージ量の増分は

\begin{align}
S^n_i - S^n_{i-1} = c(a_{i} - a_{i-n})
\end{align}

そうすると、ある月の総ストレージ量はこうなりますので、

\begin{align}
S^n_i = c \sum_{j=1}^n a_{i-j+1}
\end{align}

未知数は1つになるため、バチっと求めることができます。

当月の総ストレージ量を直近 n ヶ月のファクターの合計で割れば良い訳です。

\begin{align}
c = \frac{S^n_i}{a_{i} + a_{i-1} + \cdots + a_{i-n+1}}
\end{align}

当月のストレージ量の増分は \displaystyle{s _ i = ca _ i} でわかります。

パラメータを一番寄与の大きいファクターに押し付けた代償を c_i で吸収できていたような気もするので、これを定数にするのは少し心配ですが、実績値から計算してみて、安定してそうだったら採用しても良いのではないでしょうか。

まとめ

もっと汎用的かつ意味のある結果を得ようとすれば、やっぱりファクターの数を最初から一つに決めつけたり c を固定値にしたりせず、重回帰分析をやれよって話になるんですかね(鼻ホジ

いや、でも元々の問題設定から言えば回帰分析でどうのこうのはそんなに重要ではない気もする

何もわからない。