2015/11/30

Python Tips:引数のデフォルト値を活用したい

Python では関数の引数としてデフォルトの値を設定することができます。

今回はそのデフォルト値についての注意点をまとめてみます。

まず、デフォルトの値が与えられた引数については省略できるようになります。

import time

def take_nap(duration = 60):
    print('Will sleep for {} seconds...'.format(duration))
    time.sleep(duration)

take_nap()
# => Will sleep for 60 seconds...

デフォルト値が与えられた引数の後にデフォルト値のない引数を置くことはできません。宣言時に SyntaxError が発生します。デフォルト値にない引数の後にデフォルト値のある引数を持ってくるようにしましょう。

def take_nap(duration = 60, callback):
    print('Will sleep for {} seconds...'.format(duration))
    time.sleep(duration)
    callback()

# => SyntaxError: non-default argument follows default argument

デフォルト値にリストや辞書などを指定するのはやめましょう。デフォルト値は関数宣言時に一度だけ評価され、関数の呼び出し時には再利用されるようになっているため、誤って使うと以下のような挙動が発生します。

def print_lines(messages = []):
    prefix_message = 'Messages are:'
    messages.insert(0, prefix_message)
    for m in messages:
        print(m)

messages = ['Ciao', 'Hello', 'KONICHIWA']

print_lines(messages)
# => Messages are:
#    Ciao
#    Hello
#    KONICHIWA
print_lines(messages)
# => Messages are:
#    Messages are:
#    Ciao
#    Hello
#    KONICHIWA

# 'Messages are:' が messages に追加されたままとなり
# 呼び出される度に積み上がっていく

「デフォルト値は関数宣言時に一度だけ評価され、関数の呼び出し時には再利用される」というところがポイントです。 mutable な値だけが問題になるわけではありません。以下のような場合にも思わぬ挙動が発生します。

import datetime
import time

def get_timestamp(target_datetime = datetime.datetime.now()):
    """datetime 型の変数から timestamp を取得
    """
    return target_datetime.timestamp()

print(get_timestamp())
time.sleep(3)
print(get_timestamp())
# 2 回ともまったく同じタイムスタンプがかえってくる

これは datetime.datetime.now() が関数の宣言時に一度だけ呼び出されるために発生します。これが意図した挙動の場合には問題ありませんが、関数の呼び出し時の時刻がかえってくることを期待していると想定外の結果が返ってくるので要注意です。

この動きを防ぐには、デフォルト引数にはあくまでも None を渡しておいて関数の内部で初期化してしまうことが必要になります。

def get_timestamp(target_datetime = None):
    """datetime 型の変数から timestamp を取得
    """
    if not target_datetime:
        target_datetime = datetime.datetime.now()
    return target_datetime.timestamp()

この形で見ると、関数のデフォルト引数もクロージャと同じような挙動をするものとして覚えておくのがいいかもしれません。

2015/11/24

Python のイテレータ生成クラスの使い方

今回は Python のイテレータ生成クラスを使う方法についてご紹介します。

Python でイテレータを生成する関数のことを「ジェネレータ」と呼びますが、関数と同様にクラスを使う形でもイテレータを生成することができます。

具体的には __iter__() メソッドを定義してこれがイテレータを返すようにすれば OK です。
yield を使わない場合は __iter__() とは別に __next__() ( Python 2 の場合は next() )メソッドを定義する必要がありますが、シンプルなケースでは __iter__() と yield だけで十分かと思います。

与えられた整数の因数を返すジェネレータクラスを書いてみます。

# coding: utf-8

# 因数を返すイテレータクラス
class PrimeFactor(object):
   
    // number で対象の数を、 max_count で返す因数の数を指定する
    def __init__(self, number, max_count):
        self.number = number
        self.max_count = max_count

    // クラスをジェネレータ化する
    def __iter__(self):
        count = 0
        for i in range(2, self.number):
            if self.number % i == 0:
                count += 1
                // 指定された数だけ因数が返されたらイテレータ処理を終了するために StopIteration() 例外をあげる
                if count > self.max_count:
                    raise StopIteration()
                yield i

ここで定義した PrimeFactor は以下の形で使うことができます。

pf = PrimeFactor(number = 100, max_count = 5)

# 1 周目
for n in pf:
    print(n, end=", ")
print()
# => 2, 4, 5, 10, 20,

# 2 周目
for n in pf:
    print(n, end=", ")
print()
# => 2, 4, 5, 10, 20,

実装方法の問題でもあるかと思いますが、イテレータを返す関数とのちがいとして、クラスの場合はいったんループが回りきった後にもう一度ループを開始できるという特徴があります。

// PrimeFactor の関数版
def prime_factor(number, max_count):
    count = 0
    for i in range(2, number):
        if number % i == 0:
            count += 1
            if count > max_count:
                raise StopIteration()
            yield i

// こちらはいったんループを回したらその後は StopIteration 例外を出し続けるため
// 2 周目のループは実行されない
pf2 = prime_factor(number = 100, max_count = 5)

# 1 周目
for n in pf2:
    print(n, end=", ")
print()
# => 2, 4, 5, 10, 20,

# 2 周目
for n in pf2:
    print(n, end=", ")
print()
# => 出力なし

ちなみにこのイテレータを生成するクラスが持つインタフェースのことを「イテレータプロトコル( iterator protocol )」と呼んだりもするそうです。

以上です。

参考:

- 3.5 イテレータ型
- python - How to make class iterable? - Stack Overflow

2015/11/17

Python Tips:現在の関数の名前や引数を取得したい

Python で現在の関数の名前を取得する方法をご紹介します。 PHP でいうところの __FUNCTION__ に相当する情報を取得するイメージです。

結論からいうと inspect モジュールの inspect.currentframe() の戻り値の f_code.co_name というプロパティで確認することができます。

import inspect

def my_special_function(*args):
    print("Now in " + inspect.currentframe().f_code.co_name)

if __name__ == '__main__':
    my_special_function()  # => Now in my_special_function

上の例では my_special_function という名称が取得できていることが確認できます。

以上です。

2015/11/11

Python Tips:特定の文字コードの CSV ファイルを読み込みたい

今回は Python で特定の文字コードの CSV をデコードしながら読み込む方法をご紹介したいと思います。

標準ライブラリの csv を使う場合は、セル単位に切り離したときに decode するとよいかと思います。ファイル名と文字コードを指定すると内容を抽出してくれるヘルパー関数を書いてみます。

import csv

def csv_read(filename, encoding):
    """CSV ファイルを文字コードを指定して読み込む
    """
    with open(filename, "rb") as f:
        csvfile = csv.reader(f)
        rows = [[c.decode(encoding) for c in r] for r in csvfile]
        return rows


if __name__ == "__main__":
    # 確認
    csv_filename = "target.csv"
    encoding = 'sjis'
    csv_rows = csv_read(csv_filename, encoding)

ファイルサイズがものすごく大きな場合などにはそれなりの工夫が必要かと思いますが、通常のサイズの CSV ならこれで十分かと思います。