何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

Python Puzzler 「Puzzle 1: Once Read」

きっかけは紀伊國屋書店

紀伊國屋新宿で、『Scalaパズル』を見つけた。

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

もちろん、元ネタは『Java Puzzlers』である。
Java™ Puzzlers: Traps, Pitfalls, and Corner Cases

Java™ Puzzlers: Traps, Pitfalls, and Corner Cases

『Effective Python』や『Python文法詳解』で書かれている「落とし穴」みたいなものを集めてみるのも面白そうと思い、 デフォルト引数にまつわるもので1つ試作してみた。

Puzzle 1: Once Read

函数get_unixtimeは時刻をUNIXタイムスタンプに変換するものである。 引数nowはデフォルト引数として現在時刻を与えている。 以下のプログラムを実行すると、どのような出力になるか。

import datetime
import time

def get_unixtime(now=datetime.datetime.now()):
    time_tuple = now.timetuple()
    utc_unixtime = time.mktime(time_tuple)
    return utc_unixtime

print(get_unixtime())
time.sleep(1)
print(get_unixtime())

Solution 1: Once Read

プログラムを実行すると次のようになる。

1455710144.0
1455710144.0

time.sleepで1秒間を開けているのでUNIXタイムスタンプも1秒ほど進むはずだが、全く同じ結果が出力された。 これは、函数get_unixtimeのデフォルト引数は函数オブジェクトが作成されるタイミングで1回のみ実行されるためである。 そのため、何度デフォルト引数のまま実行しても函数オブジェクトが作成された時刻のUNIXスタンプを返す。

クラスではどうなるのか。

import datetime
import time
import pytz

class TimeZoneTranser(object):

    def __init__(self, now=datetime.datetime.now(), local_tz=pytz.timezone("Asia/Tokyo")):
        self.now = now
        self.local_tz = local_tz
        self.local_dt = self.local_tz.localize(self.now)
        self.utc_dt = pytz.utc.normalize(self.local_dt.astimezone(pytz.utc))

    def translate(self, target_tz=pytz.timezone("US/Pacific")):
        self.target_dt = target_tz.normalize(self.utc_dt.astimezone(target_tz))
        return self.target_dt

tr1 = TimeZoneTranser()
time.sleep(1)
tr2 = TimeZoneTranser()
print(tr1.translate())
print(tr2.translate())
2016-02-17 04:42:11.063276-08:00
2016-02-17 04:42:11.063276-08:00

これもまた、1秒ずれた出力を期待されていたが同一の結果が返される。 __init__()の引数でミュータブルなデフォルト引数を与えてしまうとクラスのインスタンス間で共通の値を参照してしまうのである。

対策は、『Effective Python』 項目20の通り、Noneを使う。

import datetime
import time


def get_unixtime(now=None):
    now = datetime.datetime.now() if now is None else now
    time_tuple = now.timetuple()
    utc_unixtime = time.mktime(time_tuple)
    return utc_unixtime


print(get_unixtime())
time.sleep(1)
print(get_unixtime())
1455716364.0
1455716365.0

なお、『Effective Python』 項目20にはドキュメンテーション文字列で振る舞いを文書化するよう指示している。

参考文献

  • 『Effective Python』 項目20
  • Python文法詳解』 6.1.3 デフォルト引数