Python3での内包表記あれこれ

Share

内包表記(Comprehension)とは、あるリスト(などのイテラブル)から別のリスト(あるいはセットや辞書)を作成するような処理を行うときに、それを簡便に書くことができる記法だ。ちなみに、内包表記で書くと、普通に書き下すより動作も速くなることが多い。

今回の記事は、内包表記って何?という人から、内包表記の存在は知っているけど書き方がイマイチよく分からない……という人向けとなっている。

まず、簡単な例を挙げてみる。

例1:ある整数のリストLがあるとき、Lの各要素を自乗した新しいリストを作成する。

forによるループを用いて書くと、だいたい次のような感じだろうか。

L = [1, 2, 3, 4, 5]
new_list = []
for i in L:
    new_list.append(i * i)
# new_list: [1, 4, 9, 16, 25]

これを、リスト内包表記を用いて書いてみると……

new_list = [i * i for i in L]

と、かなりスッキリした書き方になる。

ここで、内包表記のごく基本的な書き方を一般化すると、以下のような感じになる。

# 普通の書き方
new_list = []
for <元の各要素の一時代入先> in <元の要素を含むイテラブル>:
    new_list.append(<新たなリストの要素>)

# 上記の書き方に対応するリスト内包表記
new_list = [<新たなリストの要素> for <元の各要素の一時代入先> in <元の要素を含むイテラブル>]

慣れないうちは、一度通常通り書いてみた後、内包表記に変換してみるというのを試してみると、分かりやすいかもしれない。基本的には、for ~ in ~のループが回るとともに、要素が一つずつ確定・追加されていくので、通常のループによるリストの作成と動きは同じだ。

以下、さらに具体的な例を挙げつつ、もう少しこみいった内容を順次説明していこうと思う。

まず、アンパック代入を用いて複数の要素を代入に使用したり、新たなリストの要素をリストやタプルにすることもできる。

例2:[a, b]を複数含むリストが与えられ、[a + b, a * b]を要素とする新しいリストを作成する。

L = [[1, 2], [3, 4], [5, 6]]
new_list = [[a + b, a * b] for a, b in L]
# new_list: [2, 12, 30]

そして、実はさらに、条件に合うものだけを取り出すことができる。その場合、末尾にif <条件文>を追加する。

例3:[s, t]を複数含むリストが与えられ、s * t が10以上20以下のものだけを取り出し、新たなリストを作成する。

L = [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]
new_list = [a * b for a, b in L if 10 <= a * b <= 20]
# new_list: [12, 20]

ここで気づいた人もいるかも知れないが、内包表記はmap()とfilter()を合わせた感じの動作を、一度に書ける表記ともいえる。

for ~ in ~ を重ねることもできる。この場合、普通にforを書いたときのように、後の方が先にループされる形になる。下の例では、それぞれのイテラブルの直積(全ての組み合わせ)を扱う形になる。

例4:2つのリストが与えられ、それぞれから要素を一つずつ取り出し、それをかけ合わせたもの全てを含むリストを作成する。

L1 = [1, 2, 3]
L2 = [1, 10, 100]
new_list = [a * b for a in L1 for b in L2]
# new_list: [1, 10, 100, 2, 20, 200, 3, 30, 300]

あるいは、前のforで取り出した要素を、後のforで回すこともできる。

例5:与えられた2次元リストを、出てきた要素順の1次元のリストにする。

L = [[1, 2], [3, 4, 5], [6]]
new_list = [n for items in L for n in items]
# new_list:[1, 2, 3, 4, 5, 6]

なんだこれ?と混乱した人は、次の書き方と見比べてみると、分かりやすいかもしれない。

L = [[1, 2], [3, 4, 5], [6]]
new_list = []
for items in L:
    for n in items:
        new_list.append(n)

また、よくあるパターンとして、単純なループの代わりに使用することもある。

例6:リスト[0, 1]を5つ含むリストを作成する。

new_list = [[0, 1] for i in range(5)]
# new_list: [[0, 1], [0, 1], [0, 1], [0, 1], [0, 1]]

この場合は、iには0, 1, 2, 3, 4が順番に代入されるが、その数値自体は新たなリストには使用されず、単純にループカウントとして用いられる形になる。一般的な回数制御のループで使う、for i in range(N): と同じ形だ。

念の為に書いておくのだが、これを

new_list = [[0, 1]] * 5

などと書くと、後で予想外の動作をすることになるので注意。具体的には、次のようなことになる。

>>> new_list = [[0, 1]] * 5
>>> new_list
[[0, 1], [0, 1], [0, 1], [0, 1], [0, 1]]
>>> new_list[0][0] = 2
>>> new_list
[[2, 1], [2, 1], [2, 1], [2, 1], [2, 1]]

これは、リスト[0, 1]が一度しか作成されないため、結果的にnew_listが同一オブジェクトへの参照を5つ並べただけのリストになってしまうためだ。
最初のように、内包表記でループを回す形にすれば、その都度新しいオブジェクト(全て異なるリスト[0, 1]への参照)が作成されるので、上記のような問題は起こらない。

閑話休題。
少し凝ったテクニックとして、複数のリストが与えられる時は、zip()と組み合わせるとリスト内包表記を使いやすい。

例7:3つの同じ長さのリストが与えられ、それぞれの同じ場所にある数字の積を持つ新たなリストを作成する。

L1 = [1, 2, 3, 4, 5]
L2 = [6, 7, 8, 9 , 10]
L3 = [11, 12, 13, 14, 15]
new_list = [a * b * c for a, b, c in zip(L1, L2, L3)]
# new_list: [66, 168, 312, 504, 750]

さて、内包表記の一番外側の角括弧[]を、波括弧{}にするとセット(set)を、丸括弧()にするとジェネレーターを作成することもできる。

例8:リストの各要素を自乗したものを含むセットを作成する。

L = [-2, -1, 0, 1, 2]
new_set = {n * n for n in L}
# new_set: set(0, 1, 4)

辞書については、波括弧{}を使用しつつ、新しい要素は キー: 値 の形で指定する必要がある。
特に、キーのリストと値のリストを別に保持しておき、zip()でまとめつつ内包表記で辞書に変換するのは、常套手段なので知っておくといいだろう。

例9:{'A': 1, 'B': 2, 'C': 3, … , 'Z': 26} という内容の辞書、およびそのキーと値が逆の内容の辞書を作成する。

chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
nums = range(1, 27) # [1, 2, 3, ... , 26]
char_to_num = {key: value for key, value in zip(chars, nums)}
num_to_char = {k: v for v, k in char_to_num.items()}
# char_to_num: {'A': 1, 'B': 2, ... , 'Z': 26}
# num_to_char: {1: 'A', 2: 'B', ... , 26: 'Z'}

ちなみに上の例では、以下のように文字列をそのままforで回せば一文字ずつ取り出すことができることを利用している。

>>> [c for c in "ABC"]
['A', 'B', 'C']
>>> list(zip("ABC", range(1, 4)))
[('A', 1), ('B', 2), ('C', 3)]

上記のパターンを理解できれば、大抵の内包表記は読んだり書いたりできるようになると思う。後は慣れの問題だろう。

最後に、少し引っかかりやすい点を2つほど挙げておく。他の人のコードを読むときのためにも、知っておくと嬉しいかもしれない。

番外1:sum()などの引数に内包表記を書く時は、外側の括弧を省略できる。

L = [-1, 3, 2, -5, 4]
result = sum(n for n in L if n > 0)
# result: sum([3, 2, 4]) → 9となる

番外2:内包表記のifは最後に書く。途中に書かれている場合は三項演算子。

L = [-1, 3, 2, -5, 4]
pos_neg = ['pos' if n >= 0 else 'neg' for n in L]
# 上記の 'pos' if n >= 0 else 'neg' は三項演算子のif~else~。
# pos_neg = ['neg', 'pos', 'pos', 'neg', 'pos']

今回は、以上。こんな感じで、みんなも内包表記でいろいろと楽をしよう。

Share

コメントを残す

メールアドレスが公開されることはありません。