LoginSignup
475

More than 1 year has passed since last update.

リスト内包表記の活用と悪用

Last updated at Posted at 2017-12-13

便利なリスト内包表記

Pythonにはリスト内包表記というとても便利なものがあります。
どれくらい便利かというと、チューリング完全です。
証拠のbrainf*ckインタープリター を作りました。1 2 3

続編
その1 リスト内包表記で始める超"実用的"なPythonワンライナー入門
その2 【挑戦状】あなたはpythonワンライナーが書けますか?
その3 【上級者も知らない!?】Pythonで関数を作る第3の方法【lambda式より便利?!】

リスト内包表記の基本

リスト内包表記は基本的に

[iの処理 for i in 配列など]

と書かれます。
例えば、[1, 6, 9]の各要素を二倍にしたいなら、

[i * 2 for i in [1, 6, 9]]

とすれば、

[2, 12, 18]

が出力されます。
また、条件を満たす配列の要素を抽出して、処理を適用するには

[iの処理 for i in 配列など if 判定式]

のように、最後にif 判定式をつけます。

また、iの取り出し先として「配列など」と書きましたが、pythonのfor文で使えれば、なんでも使えます。(辞書ならキーを、文字列なら一文字ずつ、その他、タプルとかrange()とか)

辞書内包表記、ジェネレータ内包表記、集合内包表記

リスト内包表記、ではないですが、似たものとして、
- 辞書内包表記
- ジェネレータ4内包表記(正確にはジェネレーター式)
- 集合内包表記
があります。
これは、辞書やジェネレータ4や集合をリスト内包表記と同様に作れます。残念ながらタプルは作れないので、リストを変換してください。

辞書内包表記 

踊る人形をプレゼントされた時、各人形の出現回数を調べて見るといいことがあるかもしれません。

5

code = 'EBNO CSBU GJHVSJOH JU PVU TP RVJDLMZ'
{letter: code.count(letter) for letter in code}

この例6のように、辞書内包表記では

{キー: for i in 配列など}

と記載し、キーや値にiを使えます。

ジェネレータ内包表記

ジェネレータ内包表記は

(iの処理 for i in 配列など)

のように、角かっこ[]ではなく、丸かっこ()を使うので、一見タプルに見えます。
タプル内包表記をしたとおもって、タイプエラーが返ってきたら、多分こいつのせいです。
どうしても7タプル内包表記をしたかったら、

tuple(iの処理 for i in 配列など)

をしてください。

集合内包表記

集合内包表記は

{iの処理 for i in 配列など}

のように、角かっこ[]ではなく、波かっこ{}を使います。
辞書と間違えやすいっちゃ間違えやすいです。

リスト内包表記の活用

ここでは、私がよく使うリスト内包表記の例を紹介します。

map関数の代わり

例えば、[1, 2, 3]という整数型の配列があって、各要素を文字列型にしたい時って、よくありますよね。(その後''.join(配列)したいとか)
そんな時によく使うのが、map関数です。

map関数は、配列の各要素に何らかの関数を適用したい場合に使います。
例えば、[1, 2, 3]['1','2','3']にしたいなら、各要素にstr()'を適用すればいいので

list(map(str, [1, 2, 3]))

['1','2','3']をちゃんと返してくれます。8
ですが例えば、すべての要素を「2倍してから、5の剰余をとる」といったニッチな処理に関しては、ラムダ式で無名関数を定義する必要があります。

list(map(lambda x: (x * 2) % 5, [1, 2, 3])) # 出力は[2, 4, 1]

あああ、うっとうしい!!
リスト内包表記では、それぞれ次のように表記します。

[str(i) for i in [1, 2, 3]] # 出力は['1','2','3']
[(i * 2) % 5 for i in [1, 2, 3]] # 出力は[2, 4, 1]

分かりやすいでしょ!!
ほとんどの場合、リスト内包表記が速いです。9

filter関数の代わり

リストの中の奇数だけを抽出したい場合、filter関数とリスト内包表記の選択肢があります。

list(filter(lambda x: x % 2, [1, 2, 3]))
[i for i in [1, 2, 3] if i % 2]

どっちも[1, 3]を返すので、好きなほうを選んでください。

map関数 + filter関数 の代わり

条件をみたすリストを抽出して、関数を適用するには、先ほどの合わせ技を使います。
整数が並んだ文字列から3の倍数以外を選び、2倍したリストがほしいとしましょう。

list(map(lambda x: int(x) * 2, filter(lambda x: int(x) % 3, '299792458'))
[int(i) * 2 for i in '299792458' if int(i) % 3]

選択の権利は認めますが、迷う理由はあまりないように思います。

二次元リストを作る

リスト内包表記ではforがいくつでも使えます。そのため、二次元リストも簡単に作れてしまいます。
例えば、[0, 0]から[9, 9]がほしい場合、

[[i, j] for i in range(10) for j in range(10)]

で書けます。

最初の要素だけを取り出す

文字列のリストがあって、頭文字をつなげたい、なんて事はありませんか?
リスト内包表記だと、簡単にできます。

nameList = \
['Havoc', 'Oscar', 'Mike', 'Uni', 'Nora', 'Charlie', 'Uni', 'Lucy', 'Uni', 'Sterling']
''.join([name[0] for name in nameList])

インデックスを使う

"インデックス: 要素"のように成形した文字列のリストがほしい時があります。
その場合は、

Mikado = ['Jimmu', 'Suizei', 'Annei', 'Itoku']
['{0}: {1}'.format((i + 1, Mikado[i]) for i in range(len(Mikado))]

のように、range(len())でインデックスを動かし、それでリスト内の要素を指定してください。

コメント欄で指摘を受け、enumerate()を思い出しました。

Mikado = ['Jimmu', 'Suizei', 'Annei', 'Itoku']
['{0}: {1}'.format((i + 1, v) for i, v in enumerate(Mikado)]

こちらのほうがPythonicでいいですね。

forを複数個を使う際の注意点とリストの結合

リスト内包表記でforを複数使おうと色々試していると遭遇するエラーがあります。
それは、変数が定義されていない、というエラーです。
例えば、二次元リストを一次元リストに変換したい!としましょう。

arr = [[1,2,3], [4,5,6], [7,8,9]]
[i for i in j for j in arr]

というものが思いつきます。
しかし、この場合、

NameError: name 'j' is not defined

と怒られてしまいます。

この場合、

arr = [[1,2,3], [4,5,6], [7,8,9]]
[i for j in arr for i in j]

としましょう。

リスト内包表記と使うと便利な小技

(ここら辺から、活用というより悪用に近づきます。用法用量を守って正しくお使いください)

if elseと一緒に使う

一緒に使うことが多いものに三項演算子(if else)があります。
リスト内包表記のifは、ifに適合しない場合の処理が記述できないからです。
三項演算子は、

var = Trueのときのvarの値 if 条件式 else Falseのときのvarの値
num = 3
oddEven = '奇数' if num % 2 else '偶数' #num % 2 が1を返すので'奇数'が代入される。  

という風に、ifの後の条件式の真偽に応じてvarに代入する値が変わります。
リスト内包表記のifと併用できますし、ネストすることも可能ですが、業が深くなるのでやめておきましょう。
FizzBuzzはこのように書けます。

[
'FizzBuzz' if not num % 15 else 
'Fizz'     if not num % 3  else
'Buzz'     if not num % 5  else 
str(num)
for num in range(1, 1 + 100)]

forで代入する

リスト内包表記内では変数に代入ができません。(出来なくていいのですが)
その場合、forを使いましょう。

[number**2 - number for number in [int(input())]]

これは入力された$x$に対して、$x^2-x$を計算するプログラムです。
普通に書いたら

number = int(input())
[number**2 - number]

と同じになります。普通に書いたほうが分かりやすいですね。

追記

Python 3.8からはセイウチ構文:=が導入されて、リスト内包表記内で変数に代入ができるようになりました。出来なくていいのに。

ちょっと考えてみたら面白そうだったので、そのうち記事を書くかもしれません。

ブール演算子の短絡評価を使う

True or False

を評価するとき、pythonインタープリターはor以降のFalseを無視します。というのも、最初の項がTrueであるため二項目以降は気にする必要ないから、です。これを短絡評価といいます。False and Trueでも似たことが起きます。
これを悪用すると、こんなことができます。

print("or1" ) or  print("or2" )
print("and1") and print("and2")

#=> or1
#=> or2
#=> and1

printをブール値として評価するとNoneつまりFalseとして扱われるため、このような挙動を示します。
これを[i for i in list if hoge]のhogeに使うと、複数の関数を実行できます。

[list_ for list_ in [[1,2,3,4,5]] 
if not #run function
(
    list_.remove(2) or 
    list_.append(7)
)
]

#=> [[1, 3, 4, 5, 7]]

ただの難読化です。

forで無限ループする

for for ∞ for pythonでも説明しているのですが、for文で無限ループができます。

for文が参照するリストをfor文中でいじるとfor文の挙動が一気にやばくなります。
"Hello"と打ち込むまで入力を要求するコードは通常

while True:
    if input() == "Hello":
        break
    else:
        continue #いらないけどね

と書きますが、for文でも

loop = [None]
for i in loop:
    stdin = input()
    if stdin == "Hello":
        loop.clear()       #break
    else:
        loop.append(None)  #continue

と書けます。
これはリスト内包表記の中でも使える技です。

[
  [None
   for stdin in [input()]
   if not #run function
    (
      loop.clear() if stdin == "Hello" #break
      else
      loop.append(None)               #continue
    )
  ]
  for loop in [[None]]
  for i in loop
]

ifの中に入れることで、loopのメソッドを実行してます。

lambda関数を使う

lambdaを使うと、リスト内包表記の中で関数が定義できます。
$x^2-x$を計算する関数を定義してみましょう。

[func(number) for number in [int(input())] for func in [lambda x:x**2 - x]]

これは

def func(x):
    return x**2 - x
number = int(input())
[func(number)]

と同じです。普通に書いたほうが分かりやすいですね。

再帰もできます。

[fib(4)
  for _ in [[]] 
  for fib in 
  [
   lambda n:1 if n in {1,2} 
              else fib(n - 1) + fib(n - 2)
  ]
]

これは

def fib(n):
    if n in {1,2}:
        return 1
    else:
        return fib(n - 1) + fib(n - 2) 
[fib(4)]

と同じです。if ... else ...には三項演算子を使ってます。
普通に書いたほうがよっぽどましですね。10

typeでクラスを使う

クラスは実はclass hoge():以外でも定義できます。
type('ClassName', 親クラス一覧のタプル, プロパティーやメソッドの辞書)という方法があるのです。
詳しくて身になる話はこちらを読んでください。

class Player():
    def __init__(self):
        self.HP = 20
        self.x = 0
        self.y = 0
    def move(self,dx,dy):
        self.x += dx
        self.y += dy
player = Player()
print(player.x)
#=> 0
player.move(3,4)
print(player.x)
#=> 3

なんてことはない、ただ普通のクラスです。これをtypeで作ると、こうなります。代入操作はsetattrを使います。

Player = type('Player',
     (),
     {
        "__init__":lambda self:
                   (
                       setattr(self, "HP",20) or 
                       setattr(self, "x", 5) or
                       setattr(self, "y", 5)
                   ),
        "move":lambda self, dx, dy:
                   (
                       setattr(self, "x", self.x + dx) or
                       setattr(self, "y", self.y + dy)
                   ),
    }
)

すでにやばいですが、これをまとめてリスト内包表記に入れると、

[None
for Player in 
[type('Player',
     (),
     {
        "__init__":lambda self:
                   (
                       setattr(self, "HP",20) or 
                       setattr(self, "x", 0) or
                       setattr(self, "y", 0)
                   ),
        "move":lambda self, dx, dy:
                   (
                       setattr(self, "x", self.x + dx) or
                       setattr(self, "y", self.y + dy)
                   ),
    }
)]
for player in [Player()]
if not #run funciton
 (
   print(player.x) or
   player.move(3,4) or
   print(player.x)
 )
]

#=> 0
#=> 3

とってもわかりやすいですね!!!

ライブラリを使う

おすすめは出来ませんが、リスト内包表記ではライブラリも使用できます。
__import__(ライブラリ名)
を使用します。

[math.inf for math in [__import__('math')]]
#=> [inf]

最後に

  • 後半に紹介した手法は用法用量を守ってご使用ください。

続編
その1 リスト内包表記で始める超"実用的"なPythonワンライナー入門
その2 【挑戦状】あなたはpythonワンライナーが書けますか?
その3 【上級者も知らない!?】Pythonで関数を作る第3の方法【lambda式より便利?!】

追記

raise

(_ for _ in ()).throw(ValueError("hoge"))

でエラーが出せます。
Pythonのリスト内包表記はチューリング完全だから純LISPだって実装できるのコメントで@koji-kojiroさんが残したコードを丸パクしてます。


  1. 最初の行がstdin、二行目がbrainf*ckのコードです。三行目以降は読み込みません。 

  2. 後述するように三項演算子やlambda、typeを使うともっと書きやすいですが、純粋なリスト内包表記の機能のみでも書けることを示すため、この様なコードにしました。 

  3. 元になっているコードはこれです。 

  4. ジェネレータは、反復処理で便利なメモリを食わないリスト、と考えてください。[i for i in range(10**10)]というリストを作ると、生成時にリストが作られ、10の10乗分のメモリを食いますが、ジェネレータはfor文で呼び出される際、必要な要素だけを計算して返します。python2までxrange()が推奨されたのは、xrange()はジェネレータを作るからです。python3では今までリストを返していたrange()がジェネレータ(厳密にはジェネレーターと違うらしい)として振舞うことになり、xrangeは姿を消しました。 

  5. カエサル式暗号なので頑張ってください 

  6. codeから取り出すよりも、set(code)から取り出したほうがPythonっぽいらしいです。(出典:「オライリー 入門Python3」) 

  7. というか、タプル内包表記が必要な場合は、タプルを使うこと自体が間違っている場合が多いです。タプルの各要素はそのタプルの様々な属性を示す役割を持ち、逆にタプルは各要素から形成されるオブジェクトです。(ex. ある座標のx座標という属性と、y座標という属性)OrderedDictの作り方や、zip関数の振舞いや、namedtupleや、タプルがイミュータブルな事の意味を考えると理解していただける考え方かと思います。 

  8. python3では、list()を抜かすとイテレータとして振舞うmapオブジェクトを返します。配列を返していたpython2との大きな変更点の一つです。 

  9. 最初の例ではmap関数が、二番目の例ではリスト内包表記のほうが速いです。lambdaを使わないmap>リスト内包>lambdaを使うmapっていう順番なのかな… 

  10. for _ in [[]]がないと、再帰してくれませんが理由は知りません。誰か教えてください。 @koji-kojiro さんのコメントを受けて、現在検証中です。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
475