python3x: list comprehension, list slicing


個人的に魅了されたpythonの特徴の一つでもあるdo a lot with littleという考え方をメモ代わりにと。どうやらこのlist comprehensionという手法を使えばより効率的にpythonを操れることが出来ると聞いたので、try it out see if its as kwl as it appears to beです。

list comprehensionとは

list comprehensionsとは簡単にいえば「何行にも渡ってループ使って変数をいじっていくのが面倒なのでリストの中で全て完結させてしまいましょう」ってこと。一般的なモデルは[thing for thing in list_of_things]

例えばリストの中身の数を2倍にしてくれるという関数を作りたいとする。通常なら:

mylist = [1,2,3]

def list_doubler(lst):
    doubled = []
    for num in lst:
        double.append(num*2)
    return doubled

関数の中でplace holderとしてdoubled = []としそこに処理を終えた数字を一つづつ入れていくイメージ。つまりmy_doubled_list = list_doubler(lst) #[2,4,6]としてmy_doubled_listに入れておくことも可能。確かにこういうやりかたでもいいのだが、極端な話わざわざこんなに長いコードを書かなくても実質やりたいことはリストの中身を弄りたいだけなのでリストの中でどうにか完結させられないの?と。ここでlist comprehensionの出番。

list comprehensionを作ってみる

doubled = [thing for thing in list_of_things]
doubled = [num*2 for num in lst]
# forの前にあるのがdoubledに追加される

これでlstの中にあるそれぞれの要素を一個ずつnumがループしてnum*2してくれる。直感的にリストの中に入っていきdoubledというリストに保存される。これを一番最初に使った関数と組み合わせると:

def list_doubler(lst):
    doubled = [num*2 for num in lst]
    return doubled

何行もかけて書いたfor loopがたったの一行に。これはすごい。これに限ったことではないが、この関数に適当なリストを放り込んで返ってきた値(正確には返ってきた値をまとめたリスト)を他の変数に保存する。というかこの場合は普通に:

def list_doubler(lst):
    return [num*2 for num in lst]

その他の例

>>> squares = []
>>> for x in range(10):
...     squares.append(x**2)
...
>>>squares
[0,1,4,9,16,25,36,49,64,81]

ただこの場合はxside-effctとして残ってしまう。そこで:

squares = list(map(lambda x: x**2, range(10)))

で綺麗さっぱり。もしくはlist comprehensionを使って:

squares = [x**2 for x in range(10)]

これのほうが読みやすいし、さっぱりした構造になる。以下はリストの中身を調べて違う要素が入っていたらくっつけてくれる。

>>> [(x,y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
>>> combs = []
>>> for x in [1,2,3]:
        for y in [3,1,4]:
            if x != y:
                combs.append((x,y))
...
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

その他有効活用できそうなものをまとめておいた。

>>> vec = [-4, -2, 0, 2, 4]
>>> # create a new list with the values doubled
>>> [x*2 for x in vec]
[-8, -4, 0, 4, 8]
>>> # filter the list to exclude negative numbers
>>> [x for x in vec if x >= 0]
[0, 2, 4]
>>> # apply a function to all the elements
>>> [abs(x) for x in vec]
[4, 2, 0, 2, 4]
>>> # call a method on each element
>>> freshfruit = ['  banana', '  loganberry ', 'passion fruit  ']
>>> [weapon.strip() for weapon in freshfruit]
['banana', 'loganberry', 'passion fruit']
>>> # create a list of 2-tuples like (number, square)
>>> [(x, x**2) for x in range(6)]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
>>> # the tuple must be parenthesized, otherwise an error is raised
>>> [x, x**2 for x in range(6)]
  File "<stdin>", line 1, in ?
    [x, x**2 for x in range(6)]
               ^
SyntaxError: invalid syntax
>>> # flatten a list using a listcomp with two 'for'
>>> vec = [[1,2,3], [4,5,6], [7,8,9]]
>>> [num for elem in vec for num in elem]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']

List Comprehension with conditionals

一般的な形は[thing for thing in list if (condition)]

例えば以下の様な「5文字以上の単語をリストにいれて返す」という関数があったとする。

def long_words(lst):
    words = []
    for word in lst:
        if len(word) > 5:
            words.append(word)
    return words

実際にlist comprehensionを適用させると:

def long_words(lst):
    return [word for word in lst]
def long_words(lst):
    return [word for word in list if len(word) > 5]

Nested List Comprehensions

List comprehensionは以下の様なマトリックス表にも適用できる。

>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]

>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

図で表すとこんな感じ:

これは以下2つと同じである。

>>> transposed = []
>>> for i in range(4):
...     transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
>>> transposed = []
>>> for i in range(4):
...     # the following 3 lines implement the nested listcomp
...     transposed_row = []
...     for row in matrix:
...         transposed_row.append(row[i])
...     transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

List Slicing

参考のためについでにリストスライスもSOから引っ張ってきたのを貼っておく。

a[start:end] # items start through end-1
a[start:]    # items start through the rest of the array
a[:end]      # items from the beginning through end-1
a[:]         # a copy of the whole array
a[start:end:step] # start through not past end, by step

もう既に知っているかもしれないが

The key point to remember is that the :end value represents the first value that is not in the selected slice. So, the difference beween end and start is the number of elements selected (if step is 1, the default).

文字をひっくり返したい時とかに役に立つのが以下:

a[-1]    # last item in the array
a[-2:]   # last two items in the array
a[:-2]   # everything except the last two items

ちなみにリストにないものを指定すると空のリストが返ってくるので注意。

Python is kind to the programmer if there are fewer items than you ask for. For example, if you ask for a[:-2] and a only contains one element, you get an empty list instead of an error. Sometimes you would prefer the error, so you have to be aware that this may happen.

参考にしたリンク