Edited at

python3と秘密のリスト内包表記


この記事は作成途中です

闇の魔術に対する防衛術が結構人気そうでワロタ、埋まってくれて嬉しいです。


皆さんリスト内包表記使ってますか?

可読性等の理由からあまり使っている人はいないんじゃないでしょうか?

僕もたまには使っていますが、ガッツリ使うことはないです。

ただリスト内包表記はとても奥が深く、本当はやらなくてもいい無駄なことまでできます。

今回はその無駄極まる世界の鱗片を紹介したいと思います。


リスト内包表記入門

この記事を呼んでいる方の中にはリスト内包表記を知らない方もいるかも知れないので、簡単にリスト内包表記を紹介しましょう。

リスト内包表記は簡単にlistを生成できる方法で

a = [i for i in range(10)]

みたいに書くものです。

この例だと0~9までの数が入ったlistが生成されます

ちなみにリスト内包表記は名前から勘違いされやすいのですがsetやdict型でも使用できます。

例えばlist keyにkey名、list dataに各データが入っているときkeyとdataの長さが同じ場合

key = ["key1", "key2", "key3"]

data = ["data1", "data2","data3"]
length = len(key)
new_dict = {key[i]:data[i] for i in range(length)}

とすると各keyに対応したdataを持つnew_dictが作れることがわかります。

またリスト内包表記はこのようなこともできます。

例えば文字列の数が入ったリストnumberがあるとして、これの中身をint型に変換したいとします。

リスト内包表記を知らなかったら以下のように書くでしょう

number = ["1", "2", "3"]

new_number = []
for num in number:
new_number.append(int(num))

しかしリスト内包表記を知っていれば以下のように短く書くことができます。

number = ["1", "2", "3"]

new_number = [int(num) for num in number]

先程のコードよりだいぶ短くなったことがわかります。

ちなみにmap関数を知っていれば

number = ["1", "2", "3"]

new_number = list(map(int, number))

と書くこともできます。

また別の例として以下のような例を考えてみます。

listが2つあり、それぞれa,bとします。ではaの要素がbに入っている場合True、そうでない場合Falseを要素として持つlistを作るコードを書いてみましょう

exam = [elm in b for elm in a]

適当にa,bを作り、このコードを実行するとTrue,Falseを要素として持つlistが作られることがわかります。

他にリスト内包表記でどのようなことができるかは他の記事で見てもらうことにして、早速応用に進んでいきましょう。


リスト内包表記応用

リスト内包表記はいろいろできることがわかりました。しかしリスト内包表記には様々な問題点があります。

1.変数が使えない

2.変数に代入できない

などです。この章ではこれらの解決とその他、様々な技術について簡単に説明します。


変数を使う

リスト内包表記で以下のコードは動かないことがわかります

[a = 1]

これはリスト内包表記では変数への代入が許可されていないためです。

しかし代入しないものであれば変数のようなものを扱うことができます。

ここで例としてある数$a$場合のみTrueとなる$m$までのリストを考えてみるとこのように書くことができます。

l = [n==i for n in [int(a)] for i in range(m)]

このコードは動きます。

このコードで特筆すべきはfor n in [int(a)]のところでしょう。ここで言うなればnというint型の変数を宣言しています。

これは[int(a)]と書くとlistの中の要素が取り出されるのでint(a)がnに入るためです。こうすることで変更不可能ですがnというint型でaという値を持つ変数を宣言しているわけです。

しかし、この書き方では代入ができません。次は代入可能な変数のようなものを作成してみましょう。


代入可能な変数のようなものを使う

リスト内包表記では変数に対し代入が行なえません。

こういうものを考えてみます

[(var.update({"a":1}), var["a"])[1] for var in [dict()]]

動きましたね?

実はリスト内包表記では代入はできませんが、listのappend,extendやdictのupdateはできます。

上のコードのようにしておけばaという変数のように扱うことが可能です。


闇を生成する

この章ではいくつかの例をもとにリスト内包表記の使うかどうかわからない応用例を見ていきましょう。


次元の変換

以下のlistを見てください

test = [['00', '01', '02'], ['10', '11', '12'], ['20', '21', '22']]

このlistはすぐわかるように2次元のlistです。ではこのリストの[i][j]番目の要素が[i*3+j]で参照できるような1次元のlistに変換してみましょう。

素直に書くと以下のようなコードになると思うのですが、どうでしょうか?

test = [['00', '01', '02'], ['10', '11', '12'], ['20', '21', '22']]

new_list = []
for i in test:
for j in i:
new_list.append(j)

2次元から1次元への変換だけでこれだけのコードを書くのはめんどくさいですね。

ではこれをリスト内包表記で書いてみましょう。

実はリスト内包表記ではforのあとにそのままforを書くことができます。例えば以下のようにです。

new_list = [j for i in test for j in i]

この場合、まずは最初のforが評価されます、なのでiは['00','01','02'],...,['20', '21', '22']の各リストが入ります。

次にその次のforが評価されます、なのでjは'00','01',...,'22'の各値が取り出されることとなります。

なのでこのコードでは最初の目的通り2次元のlistを1次元のlistに変換できることがわかりました。

あ、そこ、numpyのreshape使えとか言わない。


リスト内包表記だけでfizzbuzz

普通にfizzbuzzを書くと以下のようになると思います。

for i in range(1,100):

if i%3==0 and i%5==0:
print("fizzbuzz")
elif i%3==0:
print("fizz")
elif i%5==0:
print("buzz")
else:
print(i)

余談ですが短く書くとここまで縮められます。

for i in range(100):print(i%3//2*"Fizz"+i%5//4*"Buzz"or-~i)

まあ、それはそれとしてリスト内包表記でやっていきましょう。

リスト内包表記では普通のifが使用できません。なのでリスト内包表記内で条件分岐を行いたいとき三項演算子を使用します。

今回の場合このようになるでしょうか、シンプルに書きましょう。

print("\n".join(["fizzbuzz" if i%15==0 else "fizz" if i%3==0 else "buzz" if i%5==0 else str(i) for i in range(1, 101)]))

長くなりました。正直1行でfizzbuzzを書けることに嬉しさが無いような気がしますがとりあえず書けることがわかりました。

また余談ですが、リスト内包表記ではlist内にprint等の関数を埋め込むことができます。

例えばデバッグのために以下のようにすることもできます。

[print(i**2) for i in range(10)]

これを動かしてみると、このコードだけでリストの中身が表示されることがわかります。

このようにlistの中身を表示したいときにもリスト内包表記が使えます。ちょっとしたデバッグに便利だったりします。


リスト内包表記でフィボナッチ数列

できる。$m$までのフィボナッチ数列を表示してみましょう

fib = [(l.append(l[i]+l[i+1]), l[i])[1] for l in [[1, 1]] for i in range(m)]

このコードは見ての通り、最初のfor文では

list(list(~))

という形のものを参照していることがわかります。

この際、最初のlistの中身がlに入るため、lは[1,1]というリストが入リます。

次に評価されるのは最後のfor文です。mまでぐるぐるするだけなので特に説明する必要はないと思います。

そして最後に

(l.append(l[i]+l[i+1]), l[i])[1]

が評価されることになります。

まず()の内部がtupleとして評価されます、pythonで

(1,2,...,n)

というtupleが存在した場合、tuple内は1,2...と順番に評価されます。

なので今回はまず

l.append(l[i]+l[i+1])

が評価されます。これはフィボナッチ数列の新しい要素をlに追加しているだけなので難しくないですね。

そしてその後作成してあるl[i]を呼んでいます。このとき、このtupleは

(None, l[i])

という要素を持つtupleになるので、このtupleの1番目の要素をよぶと無事フィボナッチ数列のi番目がリストの要素になります。


他のリスト内包表記の例

ある整数$n$をカンマ区切りの文字列にするリスト内包表記を使用したコード例です

s = ",".join(["".join([str(n)[::-1][i*3+j] for j in range(3) if i*3+j < len(str(n))]) for i in range(-(-len(str(n))//3))])[::-1]

やっていることは簡単なのですぐに理解できると思います。

最初のループ

for i in range(-(-len(str(n))//3))

では$n$を文字列にしたときの長さを3で割ったときの繰り上げの分ループしています。

中の内包表記を見てみましょう

"".join([str(n)[::-1][i*3+j] for j in range(3) if i*3+j < len(str(n))])

これは見ての通り文字列にした$n$を逆順にある地点$3i$から3個あとまで取っているだけになります。

ここで後ろに

if i*3+j < len(str(n))

をつけているのは文字列の外に出ないようにするためです。

これで評価の結果はsを逆順にした際の3つ区切りの数字の文字列のリストが出来上がることがわかりました。

あとは適当にカンマでjoinして逆順にすればいいだけなので簡単だと思います。


普通のコードを闇に変換する

あとで書く