R - 2.03.内包表記 Editorial /

Time Limit: 0 msec / Memory Limit: 0 KiB

前のページ | 次のページ

キーポイント

  • (リスト内包表記)
    [(変数を使った処理) for (変数名) in (変数を動かす範囲)] のように書くことで
    • 変数を使った処理 の結果を要素に持ち、
    • 変数は for 文で指定の範囲を動く

リストを取得できる。
* (標準入力から数値リストを取得)
数値の列を標準入力から受け取ってリストにする処理は
[int(item) for item in input.split()] として記述できる
* (フィルタリング)
[(変数を使った処理) for (変数名) in (変数を動かす範囲) if (条件式)] のように書くことで
* 変数を使った処理 の結果を要素に持ち、
* 変数は for 文で指定の範囲を動くが、
* 条件式 が True の範囲のみに絞り込んだ

リストを取得できる。

  • 多重の for 文や内包表記をネストさせることも可能。

リストの内包表記 : for のもう一つの使い方

今回は for の新しい使い方について説明します。

以下のコードの方法1は今までの for の使い方によって5つの要素からなるリストを作っていますが、方法2のように書き換えることが可能です。

# 方法1 : 従来の for 文
l = []
for i in range(5):
    l.append(i*i)

# 方法2 : リスト内包表記
l2 = [i*i for i in range(5)]

# l と l2 はともに [0, 1, 4, 9, 16] であり、等しい
print(l)
print(l2)
出力
[0, 1, 4, 9, 16]
[0, 1, 4, 9, 16]

この [i*i for i in range(5)] は新しいリストの作成を簡潔に記述するための記法で、リスト内包表記と呼びます。この部分は次のように解釈すると読みやすいでしょう:

  • i*i を要素に持つリストである
  • i は range(5) の範囲 (=0,1,2,3,4) を動く
  • 結果として、[0*0, 1*1, 2*2, 3*3, 4*4] すなわち [0, 1, 4, 9, 16] というリストが作成される

より一般的に、リスト内包表記では
[(変数を使った処理) for (変数名) in (変数を動かす範囲)]
のように記述します。
これによって、

  • 変数を使った処理 の結果を要素に持ち、
  • 変数は for 文で指定の範囲を動く

ような要素からなるリストを得ることができます。

※ 注意として、四角括弧([...])ではなく丸括弧((...))で内包表記を書いてしまうと結果はリストではなくジェネレータという別の型になってしまいます。この場合リストに対して行えるようなスライスなどの操作が行えなくなってしまいます。

l = (i*i for i in range(5))
print(l[0])
出力
TypeError                                 Traceback (most recent call last)
<ipython-input-5-38cb2059caa6> in <module>
      1 l = (i*i for i in range(5))
----> 2 print(l[0])

TypeError: 'generator' object is not subscriptable
(訳 : 「ジェネレータ」オブジェクトは添字アクセスできません)

競技プログラミングの文脈でジェネレータが必要になることは稀ですので、内包表記はリスト([...])の記法で書く、と覚えてしまうのがよいでしょう。

リスト内包表記の例

内包表記は全く新しい記法になるため慣れるのに時間がかかると思いますが、慣れると非常に便利な記法ですのでぜひ習熟しておくとよいでしょう。
下記に内包表記を使ったコード例を示します:

# 例1. range(3) の要素それぞれに 1 を足す処理をする -> [1,2,3]
l = [hoge+1 for hoge in range(3)]

# 例2. l の要素それぞれに 2 倍する処理をする -> [2,4,6]
l2 = [2*i for i in l]

# 例3. l2 の要素それぞれに、「要素とそれから 1 を引いた値の2要素からなるリストを作る」処理をする -> [[2,1], [4,3], [6,5]]
l3 = [[val, val - 1] for val in l2]

例1. のように、変数の名前は最初の宣言と for 文の中で整合していれば何でも良いです1
例2. のように、変数を動かす範囲range に限らず 2.01 節で説明したような色々な記述方法を使うことができます。この例では既に作成したリストを動かす範囲に指定しています。
例3. のように、変数を使った処理の結果 は数値に限らず任意の型の要素をとることができます。この例では処理の結果としてリストが得られています。そのため、リスト内包表記全体として得られるものはネストしたリスト(二重リスト)になっています。

もうひとつ例を紹介します。空白で区切られた数値の列を入力を受け取るために内包表記を使うことができます。
次の例は入力として与えられる数値の列をリストで受け取り、その合計を出力します:

入力
3 1 4 1 5
l = [int(item) for item in input.split()]
print(sum(l))
出力
14

この例のように、内包表記の「変数を使った処理」で記述する処理は関数呼び出しを使うことも可能です。

組み込み関数に渡す

1.12節で扱った組み込み関数に対し、内包表記で引数を与えることが可能です。

l = [-3, -1, 1, 2]
a = max(v*v for v in l)
b = min(v*v for v in l)
c = sum(v*v for v in l)
print(a,b,c)
出力
9 1 15

上記の例ではリストの要素をそれぞれ二乗した値の最大 / 最小 / 総和を max / min / sum 関数を使って計算しています。これらの関数は内包表記2を引数として受け取れるように実装されているため、このような書き方が可能です。

もちろん、a = max([v*v for v in l]) のようにリスト内包表記で作ったリストを関数に渡しても同じ結果になります。
この場合リストを作る処理が一度行われるため、計算速度・メモリ使用量が僅かではありますが大きくなります。

if 文によるフィルタリング

内包表記の for 文では直後に if 文をつなげて書くことが可能です。この場合、リストの要素は if 文の判定が真である要素のみに絞り込まれ(フィルタリングされ)ます。
ひとつ例を示します:

l = [3, 1, 4, 1, 5]
l_only_even = [v for v in l if v%2==0]
l_only_odd = [v for v in l if v%2==1]
print(l_only_even)
print(l_only_odd)
出力
[4]
[3, 1, 1, 5]

この例では与えられたリストに対し、そのうち偶数/奇数であるもののみからなるリストを作成しています。
l_only_odd のように該当する要素が複数ある場合、元のリストにおける順番で取得されます。

整理すると、
[(変数を使った処理) for (変数名) in (変数を動かす範囲) if (条件式)]
のように書くことで
* 変数を使った処理 の結果を要素に持ち、
* 変数は for 文で指定の範囲を動くが、
* 条件式 が True の範囲のみに絞り込んだ

ような要素からなるリストを得ることができます。

(発展) 複雑な内包表記

発展として、より複雑な内包表記の記法について説明します。
ただし、ここに示す書き方はコードを短く書くには便利ですが、内包表記を使わずに同じ処理を実現することもできるため、必ずしも習得する必要はありません。
興味のある方はご覧ください。

二重ループを伴う内包表記

前節で二重ループを扱いましたが、内包表記でも同様に書くことが可能です。
次の例は2つのリストが与えられたときに、それぞれから1つの要素を取るリストを要素に持つ二重リストを計算しています:

xs = [1, 2, 3]
ys = ["a", "b", "c"]
xys = []
for x in xs:
    for y in ys:
        xys.append([x,y])
print(xys)
出力
[[1, 'a'], [1, 'b'], [1, 'c'], [2, 'a'], [2, 'b'], [2, 'c'], [3, 'a'], [3, 'b'], [3, 'c']]

このコードは次のように内包表記で書き換えることが可能です。

xs = [1, 2, 3]
ys = ["a", "b", "c"]
xys = [[x,y] for x in xs for y in ys]
print(xys)

多重の for 文の場合でも、for 文を通常通り書いたものをリストの中に入れ込めばよいです。
若干複雑な記法になるため、難しく感じる場合は冒頭のように二重ループを使って書くほうが確実でしょう。

ネストした内包表記

先程のコードを少し変えた例を紹介します。次のコードはどのような出力になるでしょうか?

xs = [1, 2, 3]
ys = ["a", "b", "c"]
xys = [[[x,y] for x in xs] for y in ys]
print(xys)

このコードでは内側のリスト内包表記 [(x,y) for x in xs] が外側のリスト内包表記における 変数を使った処理 に相当します。
そのため、リストを要素として持つリスト、すなわち二重リストが生成されます:

出力
[[[1, 'a'], [2, 'a'], [3, 'a']], [[1, 'b'], [2, 'b'], [3, 'b']], [[1, 'c'], [2, 'c'], [3, 'c']]]

ネストした内包表記が複雑だと感じる場合、以下のように二重ループを使うか、1つのループと1つの内包表記に分解することもできます:

xys = []
for y in ys:
    item = []
    for x in xs:
        item.append([x,y])
    xys.append(item)
xys = []
for y in ys:
    xys.append([[x,y] for x in xs])

はじめのうちは自分が書きやすいと感じる方法を使って慣れていくのが良いでしょう。

二重ループとフィルタリングとの組み合わせ

上述した二重ループを伴う内包表記、ネストした内包表記はいずれも if 文を用いたフィルタリングと組み合わせることが可能です。
二重ループとフィルタリングの組み合わせの例を以下に示します:

xs = [1,2,3]
ys = [4,5,6]

# 和が3の倍数である (x,y) のリスト
xys_filter = [[x,y] for x in xs for y in ys if (x+y)%3==0]
print(xys_filter)
出力
[[1, 5], [2, 4], [3, 6]]

二重ループにフィルタリングも組み合わせると、慣れているプログラマにとっても読みづらくなるため、あまり推奨はしません。このような場合は通常の for 文で記述したほうが良いでしょう。
この例は次のようにすれば内包表記を使わずに記述することができます:

xys_filter2 = []
for x in xs:
    for y in ys:
        if (x+y)%3==0:
            xys_filter2.append([x,y])

問題

リンク先の問題を解いてください。

EX16.英単語テスト対策

前のページ | 次のページ


  1. 内包表記の中は名前空間が分けられるため、前後のコードにおいて変数に用いられている名前を用いても問題ありません(読みやすさのために避けたほうが無難ではあります)。 

  2. より厳密には、内包表記が生成するジェネレータという型のオブジェクト