項目 14: None を返すよりは例外を選ぶ

このあたりは Java と同じだ。null を返すより例外を選ぶ。

項目 15: クロージャが変数スコープとどう関わるかを知っておく

外側の関数の変数が内側の関数のスコープ内で参照できるのは知っていたが、外側の関数の変数への代入の挙動は知らなかった。 コードを見てみるのが早い:

def sort_priority(values: list, group: set):
    def helper(x: int):
        return 0 if x in group else 1, x  # group が helper 関数内で参照できる
    values.sort(key=helper)

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
sort_priority(numbers, {2, 3, 5, 7})
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8]

が、以下は正しく動かない:

def sort_priority2(values: list, group: set):
    found = False
    def helper(x: int):
        if x in group:
            found = True  # 外側の関数の変数に代入しているように見えるが, 実際は新たなローカル変数の定義, 代入
            return 0, x
        return 1, x
    values.sort(key=helper)
    return found

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
found = sort_priority2(numbers, {2, 3, 5, 7})
print(found)  # False (想定どおりではない)
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8] (これは合っている)

こういう場合 Python 3 だと nonlocal を使うらしい。上記コードの def helper(x: int): の下に nonlocal found とすれば動く。 ただ何か global 変数定義と似た危うさを感じるのでなるべく使わないほうがいいように見える。

ちなみに PyCharm だと nonlocal を使う前のコードで外側のローカル変数名と同じだと警告を出してくれる。

項目 16: リストを返さずにジェネレータを返すことを考える

その通りとしか言いようがないが、内包表記からジェネレータに変えるのは []() に替えるだけなので楽だが、 普通の関数の場合はなかなか思考が働かなかったりはする。

項目 17: 引数に対してイテレータを使うときには確実さを尊ぶ

いきなり内容が難しくなった気がする。イテレータは結果を一度しか生成しないので以下の様なことが起きる:

def f():
    for x in range(10):
        yield x

r = f()  # イテレータを変数に代入
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(r))  # [] になる. 既にイテレートは終了してしまっているがエラーは発生しない

print(list(f())  # 常に新たなイテレータを使うようにすれば OK
print(list(f())  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

クラスで __iter()__ を定義したものを使った場合は挙動が違うのか。知らなかった:

class A(object):
    def __iter__(self):
        for x in range(10):
            yield x

r = A()  # インスタンスを変数に代入
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

内部的には list() が新たなイテレータオブジェクトを作成するために A.__iter__() を呼び出すのだということが書いてある。 ただ、これだと複数回イテレートしてしまうことになるので、そうしたくない場合は結果を変数に代入して再利用するのがよい。

項目 18: 可変長位置引数を使って、見た目をすっきりさせる

これは Java にもある機能ではある。特筆すべきことはない:

def f(x, *args):
    return x + ': ' + ', '.join(args)

print(f('hoge', 'fuga', 'hage'))  # hoge: fuga, hage

項目 19: キーワード引数にオプションの振る舞いを与える

Python のキーワード引数は非常に有用だと思う。これのお陰で可読性が高まる:

def location(name, lat=35, lng=139, bearing=0):
    return '{}: ({}, {}) / {}'.format(name, lat, lng, bearing)

print(location(name='家', lat=35.1829, lng=139.8237))  # 引数の意味が明確なので読みやすい
print(location('家', 35.1829, 139.8237))  # これでも呼べるがどれが何の引数なのかがパット見わからない

特に関数定義が離れていた場合引数の位置と意味を調べるのが非常に面倒なので、キーワード引数にすることにより意味が明確になり読みやすい。

項目 20: 動的なデフォルト引数を指定するときには None とドキュメンテーション文字列を使う

関数の引数のデフォルト値が mutable なインスタンスだった場合に値が関数の初回呼び出し時の 1 回しか評価されないので 2 回目以降の呼び出しで前回の値が残っており奇妙な動作になってしまう、というのは Python では有名な話だと思う:

def log(message, when=datetime.now()):
    print('{}: {}'.format(when, message))

log('hoge')  # 2016-02-06 09:04:39.305342: hoge
log('fuga')  # 2016-02-06 09:04:39.305342: fuga

現在時刻をロギングするはずが、常に最初の log() 呼び出しの日時が出力されてしまう。 これを避けるためにデフォルト引数に None を使用しドキュメンテーション文字列に文書化せよとある:

def log(message, when=None):
    """タイムスタンプを使用したログメッセージを出力する.

    :param message: 出力するメッセージ
    :param when: メッセージ出力時の datetime. デフォルトは現在時刻
    """

    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

log('hoge')  # 2016-02-06 09:09:36.333739: hoge
log('fuga')  # 2016-02-06 09:09:36.333805: fuga

項目 21: キーワード専用引数で明確さを高める

Python 3 にキーワード引数を要求する構文があるとは知らなかった。普通の引数とキーワード引数の間に *, を挟むというもの。これは使いたい:

def log(message, *, when=None):
    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

log('hoge')
log('fuga', datetime.now())  # キーワード引数を使用していないので TypeError
log('fuga', when=datetime.now())  # OK

Python 2 にはこの構文が無いので **kwargs を使用せよとの事。