done is better than perfect

自分が学んだことや、作成したプログラムの記事を書きます。すべての記載は他に定める場合を除き個人的なものです。

Pythonを始めた時から知っていたかったベターな書き方

かなり基本的な内容です.それと記事に一貫性があるわけではないです.

1. with statement

定番のwithです.ほとんどの場合で使えると思うので,積極的に使いましょう. Goでは同じようなことをdeferを使ったりしますね.

# not good...
f = open("./test.txt")
foo = f.read()とかその辺
f.close()
# better!
with open("./test.txt") as f:
    foo = f.read()

上のコードでは,例えばfoo = f.read()の部分のコードが長い場合,f.close()を忘れがちですよね. 下のコードではそのような心配が入りませんし,簡潔です.

2. collections

pythonはよくbattery includedな言語と言われますが,その名の通り外部のライブラリに あまり頼らず標準ライブラリだけで結構戦えます.

collectionsも便利な標準ライブラリの一つです.collectionsの中でも特にCounterdefaultdictは 使う機会が個人的に多いです. どちらもdict型によく似た構造体ですが,dict型にはない便利な機能が付いています.

  • Counter Counterはその名の通り引数として渡されたiterableなものの要素の数を数えてくれます.できたものは keyに要素,valueに要素の数が入ります. またそれ以外にも,most_common(n)というメソッドを使うと最頻出の要素から上位n個の要素を要素をとって来てくれます.
from collections import Counter
c = Counter([1,1,2,3,3,4])
print(c.most_common(2)) # ==> [(1, 2), (3, 2)]

同じことはdict型にもできますが,よくやりがちなミスが下のような感じです.

d = {}
for token in [1,1,2,3,3,4]:
    d[token] += 1 # ==> 当然エラー.初期値が割り当てられていない,

setdefaultなどで頑張ってもいいですが,あまり美しくないですよね,

  • defaultdict defaultdictdict型に似たようなクラスです.先ほどのダメなコードの例では初期値が割り当てられていなくて エラーとなっていましたが,dictを使っていて最初から初期値を割り当てておいて欲しいということはよくあります. そんなときにこういう風に書くと便利です.
from collections import defaultdict
d = defaultdict(list)
for i, token in enumerate([1,1,2,3,3,4]):
    d[token].append(i)
print(d) # ==> defaultdict(<class 'list'>, {1: [0, 1], 2: [2], 3: [3, 4], 4: [5]})

このように,定義するところで最初に空のlistを初期値にすると示すだけで,新たなkeyでアクセスされた時に 空のlistを最初から割り当てていてくれます.

3. sort

あるlistdictをソートした上でイテレーションを回したいことはよくあると思います.listはともかく, dictの場合はkeyでソートしたいときやvalueでソートしたいとき,色々な場合があると思います.

d = {2: 3, 1: 4, 5: 1}
for k, v in sorted(d.items()):
    print(k, v) # ==> 1 4, 2 3, 5 1の順番.keyで昇順の並び
for k, v in sorted(d.items(), key=lambda x:x[1]):
    print(k, v) # ==> 5 1, 2 3, 1 4の順番.valueで昇順の並び

もっと複雑な場合,例えばPythonの公式ドキュメントでは operatorモジュールを利用して以下のようなコードを紹介しています.

from operator import itemgetter, attrgetter
class Student:
    def __init__(self, name, grade, age):
        self.name = name
        self.grade = grade
        self.age = age
    def __repr__(self):
        return repr((self.name, self.grade, self.age))

student_objects = [
    Student('john', 'A', 15),
    Student('jane', 'B', 12),
    Student('dave', 'B', 10),
]

print(sorted(student_tuples, key=itemgetter(2))) # ==> [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
print(sorted(student_objects, key=attrgetter('age'))) # ==> [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
# gradeでソートしてさらにageでソートする場合
print(sorted(student_tuples, key=itemgetter(1,2))) # ==> [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
print(sorted(student_objects, key=attrgetter('grade', 'age'))) # ==> 上と一緒

4. all(), any()

以前に少しだけ書いたことがありますが,anyallはかなり便利です. 例えば,以下のようなコードを書いている場合,allを使うとスッキリかけます.(ちょっと例えが極端ですが...)

# not good...
cnt = 0
l = [1,1,2,3,4,4,5]
for i in l:
    if i < 10:
        cnt += 1
if cnt == len(l):
    print("All numbers are less than 10")
# better!
if all(x < 10 for x in l):
    print("All numbers are less than 10")
if any(x >= 10 for x in l):
    print("All numbers are less than 10")

5. ファイルオブジェクトに対するイテレーション

Pythonでは,ファイルオブジェクトに対してイテレーションをかけられます.巨大なファイルを行ごとに読みたい なんて時にすごく便利です.

with open("./test.txt") as f:
    for line in f:
        print(line)

公式ドキュメントのとおり細かい調整(シーケンス場所を変えるなど)はできませんが,多くの場合はこの書き方だけ覚えておけば十分でしょう.

5. sum

sum関数はジェネレータも取ることができます.例えば以下のようなコードだと,わざわざlistを生成するより早いです.

In [1]: %timeit sum(x for x in range(100))
100000 loops, best of 3: 6.21 µs per loop

In [2]: %timeit sum([x for x in range(100)])
100000 loops, best of 3: 6.94 µs per loop

sum以外にも結構こういう風に書けるのは多いので,遅いなーと思ったらジェネレータで書いてみるのもいいでしょう.


以上です.もっと紹介したいのですがめんどくなったので終わります. もし何か間違いやもっといい書き方などあれば教えてくれると嬉しいです.