読者です 読者をやめる 読者になる 読者になる

Effective Python 1章

Effective Pythonの内容を身につけるため、各項目に関連した内容を書いていく。

  • 本の内容そのまま写経ではなく、できるだけ独自に例を作る
  • Python2に固有の話は基本的に無視する

項目1:使っている Pythonのバージョンを知っておく

自分のパソコンの環境

$ python --version
Python 2.7.10
# システムデフォルトのやつ

$ python3 --version
Python 3.6.0
# homebrewで入れた。virtualenvとかの仮想環境的な仕組みは使っていない

項目2:PEP 8スタイルガイドに従う

PEP8の本家ドキュメント

SublimeTextでは、SublimeLinter-pep8を使ってスタイルチェックができる。

項目3:bytes, str, unicodeの違いを知っておく

unicodeはpython2用なので気にしない。

>>> # ascii文字値から構成されるbytesは、先頭に'b'をつけたリテラルで表現できる
>>> bytes = b'a lazy fox'
>>> bytes
b'a lazy fox'
>>> # bytesへのインデックスアクセスで得られる要素はint
>>> bytes[0].__class__
<class 'int'>
>>> list(bytes)
[97, 32, 108, 97, 122, 121, 32, 102, 111, 120]
>>> 
>>> str = 'a lazy fox'
>>> str
'a lazy fox'
>>> # strへのインデックスアクセスで得られる要素はstr
>>> str[0].__class__
<class 'str'>
>>> list(str)
['a', ' ', 'l', 'a', 'z', 'y', ' ', 'f', 'o', 'x']

項目4:複雑な式の代わりにヘルパー関数を書く

たいへん当たり前の内容なので特に何も言うことはないが、本文に登場する例について。

def get_first_int(values, key, default=0):
    found = values.get(key, [''])
    if found[0]:
        found = int(found[0])
    else:
        found = 0
    return found

変数 found に再代入しているのがイマイチと感じる。このくらいの分岐だったら個人的には条件演算子の方が好み。

Pythonの条件演算子、ちょっと独特なのでそこでの読みづらさはある

def get_first_int(values, key, default=0):
    found = values.get(key, [''])
    return int(found[0]) if found[0] else 0

項目5:シーケンスをどのようにスライスするか知っておく

スライスの範囲がシーケンスの長さをはみ出ている場合、スライスの範囲長よりも小さいサイズのシーケンスが返される。

>>> l = [0, 1, 2, 3]
>>> l[:10]
[0, 1, 2, 3]
>>> l[10:20]
[]  # 範囲をはみ出ている場合、空リスト
>>> l[3:2]
[]  # スライスの範囲が大小逆転している場合、空リスト

ちなみにRubyの場合、指定するRangeが配列の長さの範囲から完全にはみ出ている場合、空の配列ではなくてnilが返ってくることが異なる。

> a = [0, 1, 2, 3]
 => [0, 1, 2, 3] 
> a[0...10]
 => [0, 1, 2, 3] 
> a[10...20]
 => nil  # 範囲をはみ出ている場合、nil
> a[3...2]
 => [] 

インデックスアクセスは、 __getitem__()__setitem__() で実装されている。これらのメソッドを定義すると、自作クラスでインデックスアクセスを提供できる。

class IndexedObject:

    def __init__(self):
        self._prop0 = 0
        self._prop1 = 1

    def __getitem__(self, key):
        print("getitem:" + str(key))
        if key == 0:
            return self._prop0
        elif key == 1:
            return self._prop1
        else:
            return None

    def __setitem__(self, key, value):
        print("setitem:" + str(key))
        if key == 0:
            self._prop0 = value
        elif key == 1:
            self._prop1 = value
        else:
            pass
>>> o = IndexedObject()
init
>>> o[1]
getitem:1
1
>>> o[1] = 'str'
setitem:1
>>> o[1]
getitem:1
'str'

このコードでは、あくまでサンプルとして key は整数にしか対応していない。スライスを渡す形式に対応したかったら、 __getitem__()__setitem__() の中で key の型を判定して処理を分ける。

項目6:1つのスライスでは、 start, end, strideを使わない

ややこしいスライスを行おうとしてコードが複雑になる場合、それを避けるためにスライスを複数回適用すると、リストのコピーが複数回行われて効率が悪い。

>>> a = [0,1,2,3,4,5,6,7,8,9]
>>> b = a[1:6]
>>> c = b[::2]
>>> c
[1, 3, 5]
# a[1:6:2] と同じ結果

itertools#islice() を使うと、イテレータの形で中間状態を持てるため、効率が良い。

>>> from itertools import islice
>>> a = [0,1,2,3,4,5,6,7,8,9]
>>> iter = islice(a, 1, 6)
>>> list(islice(iter, None, None, 2))
[1, 3, 5]

項目7:mapやfilterの代わりにリスト内包表記を使う

適用したい関数がはじめからある場合には、別にmapでも問題なさそう。

あと、mapだと変換したシーケンスそのものではなくてイテレータが返ってくるという違いがある。 まあ、リスト内包表記のほうもジェネレータにすればいいだけではあるが。

>>> import math
>>> a = [1, 2, 4, 8]
>>> [math.log2(x) for x in a]
[0.0, 1.0, 2.0, 3.0]
>>> list(map(math.log2, a))
[0.0, 1.0, 2.0, 3.0]

filterも一緒に実行したい場合は、map/filterだと関数のネストになってしまうのでさすがにリスト内包表記の方が読みやすい。 (とはいえ、Rubyのメソッドチェーン形式のほうがより読みやすいんだけど)

項目8:リスト内包表記には、 3つ以上の式を避ける

リスト内包表記のネストは本当に読むの厳しいのでやめた方が良いですね…

項目9:大きな内包表記にはジェネレータ式を考える

ジェネレータを使うと無限リストも扱える

>>> from datetime import *
>>> 
>>> def generate_int():
>>>    i = 0
>>>    while True:
>>>        yield i
>>>        i = i + 1
>>> 
>>> start_time = datetime.now()
>>> for v in generate_int():
>>>    if (datetime.now() - start_time).total_seconds() > 1:
>>>        print(v)
>>>        break
661670

項目10:rangeよりは enumerateにする

enumerate、要はeach_with_indexだ

初期値を指定できるところは便利

>>> for i, e in enumerate(['a', 'b', 'c'], start=1):
...     print(f'{i}:{e}')
1:a
2:b
3:c

項目11:イテレータを並列に処理するには zipを使う

zipは3つ以上の引数も扱える。

>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> c = [7, 8, 9]
>>> for x, y, z in zip(a, b, c):
...     print(f'x:{x} y:{y} z:{z}')
x:1 y:4 z:7
x:2 y:5 z:8
x:3 y:6 z:9

項目12:forとwhileループの後の elseブロックは使うのを避ける

はい。

項目13:try/except/else/finallyの各ブロックを活用する

  • finallyブロック
    • Pythonでfinallyブロックを使った覚えがない。これまでのところだいたいwith文で事足りている気がする
  • elseブロック
    • 成功時の処理をtry本体と切り離して明示できるのはいいですね
    • 変数のスコープがtryブロック内に閉じないからこれが実現できるのか