リスト内包表記を読む
A = [str(i) for i in range(10)]
# ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
変数Aは、range(10)の各値をそれぞれ文字列に変換した値が格納されたリストです。
[<値> for <変数> in <回すオブジェクト>]
というような構文によって、リストを速度的にもメモリ的にも効率よく作成することができます。
このような構文はリスト内包表記と呼ばれています。
内包表記という名称には「共通の性質を持った値の集まり」を説明的に表現する記述方法という意味が込められています。
例えば上記の処理で、実際にAに入る値は['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
ですが、そのように具体的に入っている値やリストをイメージするのではなく、
「range(10)をfor文で回して各値を文字列に変換し、格納したもの」という説明的な表現によってAを捉えることができる表記方法ということです。
「説明的な表現」というのが実はとても大切で、プログラミングにおいて処理の本質をつかむために重要な抽象的思考力や論理的思考力を高める手助けとなります。
リスト内包表記には以下のような構文も用意されています。
A = [str(i) for i in range(10) if i % 3 == 0]
rangeの後にifと条件式を追加しています。こうすることで条件式を満たす値のみを抽出することができます。形式としては以下のようになります。
[<値> for <変数> in <回すオブジェクト> if <条件式>]
上記の処理では実際にA入る値は['0', '3', '6', '9']
ですが、具体的な値をイメージするのではなく、
先ほどと同じように「range(10)をfor文で回して各値のうち3で割り切れるものを文字列に変換し、格納したもの」と説明的にAを捉えます。
リスト以外にも辞書型や集合型を作る際もこの内包表記を利用することができます。
# 辞書型
D = {i: i*i for i in range(10)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
# 集合型
S = {i * 2 for i in range(10)}
# {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}
いずれにしても、「共通の性質を持った集まり」を説明的に表現しています。
なぜこのような表現方法なのか
前節での「説明的な表現」というのがやはり重要です。これが活きるのは、オブジェクトを回す回数が非常に多い場合や一個一個追うと頭で処理しきれない処理がある場合にスッキリするからです。
構文の形としては、数学の影響を受けています。実は数学においても同じように膨大な大きさの集合(無限個の要素を持つ集合など)を扱ったり、手でするには途方もない計算をする場合、そもそも計算方法を捉えられない場合があるからです。
「複雑で膨大な量の処理をする」というプログラミングにおける大きなテーマを、数学と結びつけていることになります。
この節では以下の二つについて説明します。
- 可読性向上のメリット
- 数学の何に影響された?
可読性向上
では内包表記を使った具体例として業務処理「お祭りに参加した屋台のうち、ある区画に絞ってその利益を計上する」という処理が欲しいとしましょう。こうした場合Pythonでは以下のように説明的にプログラムを組めます。
(変数にはすでに値が入っているとします。雰囲気で読んでみてください。)
profits = [shop.profit() for shop in shops if shop.area == area]
total = sum(profits)
すこし読みづらいので、改行して整えます
profits = [shop.profit()
for shop
in shops
if shop.area == area]
total = sum(profits)
profitsを使い回さないのであれば、以下のように説明的にコーディングできます。(なおかつ性能が良い)
total_profit = sum(shop.profit()
for shop
in shops
if shop.area == area)
まさに、「屋台のうち、ある区画に絞ってその利益を計上」していると読み取れるコードです。この記述の仕組みは内包表記と双子のような関係の「ジェネレータ式」によるものですが詳しい説明は割愛します。
とにかく、<値> for <変数> in <回すオブジェクト> if <条件式>
という記述で、「共通の性質を持った集まり」を説明的に表現することができるということをおさえてください。
では、このような構文となった元について説明します。
数学の影響を受けているPython
実は内包表記は、数学における集合論の内包的表記の影響を受けています。
数学における集合は、さまざまな数学的対象を定義したり議論したりする際に言葉(言語)として用いられることがしばしばあります。内包的表記は、この記事の中心の話題であるリスト内包表記と同様に説明的に集合を捉えることができます。
集合の内包的表記は以下のような形となっています。補足事項はWikipediaを参照してください(https://ja.wikipedia.org/wiki/%E9%9B%86%E5%90%88#%E8%A8%98%E6%B3%95)
$$
\{ 関数(要素) | 要素 \in 集合, 命題(要素) \}
$$
縦棒|
の左側が、集合に格納される要素となります。右側には、集合やその要素に関する関係を記述します。抽象的でわかりにくいかもしれないので、具体例を挙げます。
集合 $S$ を、お祭りに参加した屋台の集まり、
要素 $s$ は、屋台、
関数 $f(s)$ を、屋台 $s$ から利益を計算する処理(※)、
命題 $P(s)$ を、「$s$ は三丁目の屋台である」
とします。
そうすると、「三丁目の屋台の利益を集めた集合」は以下のように表現できます。
$$
\{ f(s) | s \in S, P(s) \}
$$
(※ 気にしなくても良いですが、関数 $f$ は利益の値を区別するため(屋台のID, 利益)という組を出力するとします)
Pythonのリスト内包表記と照らし合わせてみます。変数名もそろえてみます。
S = shops
def f(s):
return s.profit()
def P(s):
return s.area == "三丁目"
profits = [f(s) for s in S if P(s)]
似ていませんか? 縦棒|
はfor
に対応し、$∈$ はin
に対応し、命題(条件式)を使う際はif
を使います。
数学の影響を受けていることが伝わったでしょうか…?
最後にそのメリットを書いて終わりとします。
おわりに
気づいた方もいるかもしれませんが、数学において集合は実は数の集まりだけでなく一般的に"モノ"の集まりを扱うことができます。
よってシステム開発などで、あるデータの集まりの各データに対して一定の処理を施すような業務処理(バッチ処理)などは集合論的に考えることが可能です。
数学は頭ではイメージしきれない(イメージする必要がない)集合(無限集合など)をどうにか扱うため、対象の本質をおさえながら論理的に議論を進めることがあります。
そんな無限集合さえも扱ってしまう数学の圧倒的な道具をプログラミングにも活用することで、コードに処理の本質を表現しておくことができるのです。それができるPythonは良い言語だと思います。(RubyやJavaScriptなど多くのメジャー言語も実は数学の道具を取り入れています。その数学的な記述の傾向が強いのが関数型言語で、その中でも傑出しているのはHaskellという言語です)
これは「プログラミングに数学は必要あるのか」議論に対する「数学、特に集合はプログラミングに大いに活用できるからおすすめ!」「必要ないけど、あなたがやってるのは実は数学やで!安心して!」といういくつかの回答になりそうです。
数学がプログラミングの理解を深め、プログラミングが数学の理解を深めます。