タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

覚えるだけでPythonのコードが少し綺麗になる頻出イディオム

Sponsored link

まえがき

今年の春から今まで、2年ぶりにPythonを沢山書いているわけなんですが、JavaScriptのクソに頭をやられて久しぶり書くだけあって基本的なところから色々と頭から抜け落ちていたわけです。
そんで何か思い出すたびに会社のwikiを使ってメモっていたのですが、せっかくなので少々訂正をしてブログにも書きます。

また、弊社はPython2を使っているので、2が前提の記述になっているところがいくつかあります。なるべくフォローしていますが、参考にする場合は自分が使っているバージョンを確認することをおすすめします。
また、今から新しくPythonでプロジェクトを始めようと思っている人は3系を使いましょう。

知ってる人は当然知ってる、でも結構長いことPythonを書いてても知らなかったりするような小技を載っけました。
なお、メタプログラミングとかの黒魔術っぽい記事のまとめはこちら:

hachibeechan.hateblo.jp

追記

おもったよりブクマがついてびっくりしたので、指摘の箇所の訂正と文字列操作の項を追加しました。

文字列の処理

もしもPython2を使っている場合、ファイルの先頭にfrom __future__ import unicode_literalsを記載するようにしましょう。また、その他のfutureも全て読み込むことをおすすめします。

文字列フォーマット

Deprecated:

C言語に似た、%形式の文字列フォーマットは後方互換性のためだけに残されており、機能の面でも劣っています。将来的に廃止されることが決定されてます(早く消えて欲しい)。

'Hello %s'.format('John')

Good:

'Hello {}'.format('John')

Python3.6以降

いわゆるテンプレートリテラルが使えるようになっています。
詳細はこちらをどうぞ。

name = 'John'
f'Hello {name}'

www.python.org

左揃え、右揃え、中央揃え(left pad的な)

str.format()の書式は沢山ありすぎてここでは紹介しきれないのですが、一番よく使いそうなこれを紹介しておきます。

>>> '{0:0<5}'.format('1')
'1000'

>>> '{0:0^5}'.format('1')
'00100'

>>> '{0:0>5}'.format('1')
'00001'

詳しいところは公式のドキュメントに譲りますが、最初の0は置換したい値のインデックス(省略した場合は登場順にインクリメントされます)、次の0は埋めたい文字(アルファベットも入ります)、次の<^>が寄せたい方向を指し、最後がいくつ埋めるかを指します。

また、formatに与える引数がstrかintかによってalignmentの挙動や受け入れられる書式が変わります。
ここに限らず、Pythonにおいては(というかどの言語でもそうですが)変数の型がなんなのかははっきりさせておくようにしましょう。

文字寄せ メソッド呼び出し版

詳しくは公式のstringモジュールの項を参照してください。

stringモジュールにはそれぞれljust, rjust, centerという関数があり、任意の文字を使って文字詰めを行うことができます。
strインスタンスのメソッドにも同様のものがあり、その場合は第一引数が自動的にインスタンスそのものになります。

リストの処理

インデックスを取りながら値も欲しい

Not Good:

datas = ['a', 'b', 'c']
for i in range(len(data)):
    print(i)
    print(datas[i])

Good:

datas = ['a', 'b', 'c']
for i, data in enumerate(datas):
    print(i)
    print(data)

複数のリストを同時に処理したい

Not Good:

data_a = ['a', 'a', 'a', 'a']
data_b = ['b', 'b', 'b', 'b']
min_length = min(len(data_a), len(data_b))
for i in range(min_length):
    a = data_a[i]
    b = data_b[i]

Good:

for a, b in zip(data_a, data_b):
    # use a, b

NOTE:

Generatorの概念が理解できているならばitertools.izipを使いましょう。ですが、貴方がバージョン3以上のPythonを使っているならば、デフォルトでzipはGeneratorを返します。
他にも、itertoolには数多くの便利な関数が用意されているので一通り目を通しておきましょう。扱いたいリストたちの状態次第では、izip_longest(あるいはzip_longest)が役に立つでしょう。

要素をリストに追加していきたい (map)

Not Pythonic:

li = []
for i in range(10):
    li.append(i)

Pythonic:

li = [i for i in range(10)]

条件に合った要素だけをリストに追加していきたい (filter)

same as filter()

Not Pythonic:

li = []
for i in range(10):
    if i % 2 == 0:
        continue
    li.append(i)

Pythonic:

li = [i for i in range(10) if not i % 2 == 0]

リストの要素それぞれに変化を加えていきたい (map)

same as map()

Not Pythonic:

li = []
for i in range(10):
    li.append(i * 2)

Pythonic:

li = [i * 2 for i in range(10)]

You can use this with the filter() pattern:

li = [i * 2 for i in range(10) if i % 2 == 0]

条件に合致した最初の値が欲しい (find)

JavaScriptやRubyであればfindメソッドを使うところですが、Pythonでは組み込みでこれを一気に行うメソッドは存在しません。
自分で書くとすると

def find(cond, li, default=None):
    return next((l for l in li if cond(l)), default)

# 以下と同義
def find_(cond, li, default=None):
    for l in li:
        if cond(l):
            return l
    else:
        return default

という定義になるでしょうか。なおfor文にelseが出てきているのが奇妙にうつるかもしれませんが、仕様です。
挙動はこうなります。

>>> find(lambda l: l == 3, [1,2,3,4])
3
>>> find(lambda l: l == 3, [1,2,4], default='noooo')
'noooo'

utility関数を作ってもいいかもしれませんが、前者のイディオムを暗記してしまうのも手だとおもいます。チームで話し合いましょう。

全ての要素が条件を満たしているか調べたい(あるいはどれか一つでも条件を満たしているか調べたい)

全ての

Not Pythonic:

all_valid = True
for val in some_values:
    if not some_validation(val):
        all_valid = False
        break

Pythonic:

all(some_validation(val) for val in some_values)

どれかが

is_any_valid = False
for val in some_values:
    if some_validation(val):
       is_any_valid = True
       break

Pythonic:

any(some_validation(val) for val in some_values)

Pythonでコードを書いていて、for文と共にcontinueやbreakが出てきた場合、それは黄色信号です。
itertoolなどの便利な関数を見逃しているか、データ構造が複雑すぎる可能性があります。

リスト内包表記以外のシンタックスについて

Generator

(i * 2 for i in range(10) if i % 2 == 0)

Set(集合型)

{i * 2 for i in range(10) if i % 2 == 0}

Dictionary

{i: i * 2 for i in range(10) if i % 2 == 0}

辞書の処理

副作用なしで、二つの辞書を連結したい

Pythonにはこの操作を直接的に行うメソッドやユーティリティがありません。
イディオムとしてはいくつかありますが、一番シンプルなものはこちらです。

>>> d1 = {'a': 1}
>>> d2 = {'b': 2}
>>> d_merged = dict(d1, **d2)
>>> d_merged
{'a': 1, 'b': 2}

また、Python3を使っているならば

>>> d_merged = {**d1, **d2}
>>> d_merged
{'a': 1, 'b': 2}

と書くことが可能です。また、この書き方ならば二つ以上の辞書を簡単に連結することができます。

keyが存在するかどうかをチェックする

Bad:

some_dictionary = some_func()
try:
    some_dictionary['foo']
except KeyError:
    print('no key!')

Not Efficient:

if some_dictionary.get('foo') == None
    print('no key!')

Good:

if 'foo' not in some_dictionary:
    print('no key!')

NOTE:

has_key新しいPythonでは使えません。

デフォルトの値を持った辞書を作成したい

Not Efficient:

result = {}
for key, v in some_values:
    if key not in result:
        result[key] = []
    result[key].append(v)

Efficient:

from collections import defaultdict

result = defaultdict(list)
for key, v in some_values:
    result[key].append(v)

値が存在しない場合にだけ要素を追加したい

Not Efficient:

some_dictionary = some_func()
if 'foo' not in some_dictionary:
    some_dictionary['foo'] = 'default value'

Efficient:

some_dictionary.setdefault('foo', 'default value')

要素の数え上げをしていきたい

Not Efficient:

data = ['aaa', 'bbb', 'ccc', 'aaa', 'ddd']

word_and_counts = {}
for word in data:
    if word_and_counts.has_key(word):
        word_and_counts[word] += 1
    else:
        word_and_counts[word] = 1

Efficient:

from collections import Counter

data = ['aaa', 'bbb', 'ccc', 'aaa', 'ddd']
counter = Counter(data)

その中でも最も頻度の高いものを取得する

If dict:

top_n =[]
count = 0
for w, c in sorted(word_and_counts.iteritems(), key=lambda x: x[1], reverse=True):
    if count == 3:
        continue
    result[w] = c
    count = count + 1

If Counter:

top_n = counter.most_common(3)

ValueObjectを作りたい

Not Efficient:

and this is not Immutable

class Rect(object):
    x = 0
    y = 0
    width = 0
    height = 0

    def __init__(x, y, width, height):
        # ellipsis

rect = Rect(0, 0, 0, 0)

Efficient:

and this is immutable

from collections import namedtuple

Rect = namedtuple('Rect', ('x', 'y', 'width', 'height', ))
rect = Rect(0, 0, 0, 0)

所感

こうして振り返るとリスト処理関係のイディオムが多い気がしますね。
何か気がつくことや、要望があれば随時書き足していきます。


Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考

サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考

  • 作者: Justin Seitz,青木一史,新井悠,一瀬小夜,岩村誠,川古谷裕平,星澤裕二
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2015/10/24
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (10件) を見る