Python3のdatetimeはタイムゾーンを指定するだけで高速になる

# はじめに
Pythonで時間を扱う定番の標準ライブラリといえば`datetime`ですね．
適当に使っても全く問題なく使えるライブラリですし，ちょろっと呼び出すだけならパフォーマンスを気にする必要すらありません．
しかし，数万回，数千万回とdatetimeを生成する必要に迫られた時，ボトルネックは顕在化してきます．

そこで，ちょっとしたことに気をつけるだけでパフォーマンスが若干向上する事を発見したので，そのご紹介です．

結論を書くと標準ライブラリの`timezone`を使って`datetime`を生成しようというお話です．

※ちなみに，タイトルの通りPython3限定のお話です．

# 結論
いきなり結論だけ書きます．それ以降は興味があればご覧ください．
datetimeは次のように生成するのが良いと思います．
ポイントは`timezone`を指定するかどうか．．．それだけです．

```py3
from datetime import datetime, timedelta, timezone

# タイムゾーンの生成
JST = timezone(timedelta(hours=+9), 'JST')

# GOOD, タイムゾーンを指定している．早い
datetime.now(JST)
datetime.fromtimestamp(UNIX時間, JST)

# NG, 環境に依存した時刻を使用している．タイムゾーンを指定しない場合と比較して遅い
datetime.now()
datetime.fromtimestamp(UNIX時間)
```

# パフォーマンス比較
早速ですが，色々な方法で`datetime`を生成してみます．
`datetime`を1000万回生成した際の処理時間を測定します．
タイムゾーンの指定の有無でどれくらいパフォーマンスが変わるのか．など参考にして頂ければと思います．

- 実行環境:
    - OS: Mac
    - CPU: Core i5 1.6Ghz
    - メモリ: 4GB DDR3
- 言語: Python3.6.2

##  タイムゾーンを指定した場合
(僕の知る限り)最も高速に動作させられるパターンです．

```py3:zikan1.py
from datetime import datetime, timedelta, timezone

JST = timezone(timedelta(hours=+9), 'JST')

for _ in range(10000000):
  datetime.now(JST)
```

```shell-session
$ time python zikan1.py
real	0m7.581s
user	0m7.167s
sys	0m0.114s
```
1000万回ループして結果は７秒．

## タイムゾーンを指定しなかった場合
タイムゾーンを指定しないと若干遅くなります．若干．．

```py3:zikan2.py
from datetime import datetime

for _ in range(10000000):
  datetime.now()
```
```shell-session
$ time python zikan2.py
real	0m9.609s
user	0m9.149s
sys	0m0.111s
```
９秒くらいですね．若干遅くなりました．

## pytzでタイムゾーンを指定した場合  
pytzとはPython2系でタイムゾーンを指定するために良く使われているライブラリです．というのも，Python2にはtimezoneクラスがまだ実装されていなかったのです．．．

```py3:zikan3.py
import pytz
from datetime import datetime

# third party
JST = pytz.timezone('Asia/Tokyo')

# performance testing
for _ in range(10000000):
  datetime.now(JST)
```

```shell-session
$ time python zikan3.py
real	1m9.173s
user	1m6.999s
sys	0m0.584s
```
思った以上にめっちゃ遅かったです．これについては後で考察します．

## Python2系でタイムゾーンを指定した場合
折角なので，Python2系でもベンチマークしてみました．
Python2系だと，`tzinfo`というタイムゾーン用のインタフェースクラスが提供されているだけなので，自分で実装しないといけません．面倒です．面倒ゆえに`pytz`が流行ったのでしょう．

```py:zikan4.py
from datetime import datetime, timedelta, tzinfo

class JST(tzinfo):
  def utcoffset(self, dt):
    return timedelta(hours=9)


  def dst(self, dt):
    return timedelta(0)


  def tzname(self, dt):
    return 'JST'

for _ in range(10000000):
  datetime.now(JST())
```

```shell-session
$ time python zikan3.py
real	0m55.416s
user	0m51.131s
sys	0m1.532s
```
遅い．．．．

## 結果
タイムゾーン指定 (7s) < タイムゾーン未指定 (9s) < python2 (51s) < pytz (66s)
という感じになりました．

# 細かい話

## pytzはなんで遅いのか
標準ライブラリのタイムゾーンクラスも`pytz`のタイムゾーンクラスもどちらも`tzinfo`の実装クラスです．なのにパフォーマンスは天と地の差があります．
なぜなのか？
明確な答えに辿り着いた訳ではないですが，プロファイリングをすると両者の違いは一目瞭然です．

```shell-session
$ python -m cProfile -s cumtime zikan1.py
         10001072 function calls (10001061 primitive calls) in 9.107 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      2/1    0.000    0.000    9.107    9.107 {built-in method builtins.exec}
        1    3.149    3.149    9.107    9.107 zikan1.py:1(<module>)
 10000000    5.950    0.000    5.950    0.000 {built-in method now}
      3/1    0.000    0.000    0.009    0.009 <frozen importlib._bootstrap>:958(_find_and_load)
      3/1    0.000    0.000    0.009    0.009 <frozen importlib._bootstrap>:931(_find_and_load_unlocked)
```

```shell-session
$ python -m cProfile -s cumtime zikan3.py
         70022021 function calls (70021903 primitive calls) in 83.138 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     14/1    0.001    0.000   83.138   83.138 {built-in method builtins.exec}
        1    3.185    3.185   83.138   83.138 zikan5.py:1(<module>)
 10000000    9.225    0.000   79.868    0.000 {built-in method now}
 10000000   17.973    0.000   70.643    0.000 tzinfo.py:179(fromutc)
 20000000   43.347    0.000   43.347    0.000 {method 'replace' of 'datetime.datetime' objects}
```

注目したい部分はzikan3.pyを実行した際の

```
10000000   17.973    0.000   70.643    0.000 tzinfo.py:179(fromutc)
20000000   43.347    0.000   43.347    0.000 {method 'replace' of 'datetime.datetime' objects}
```
の部分です．
pytzは`datetime.replace()`が呼び出されており，かつ2000万回も実行されています．それだけでなく，`fromutc`関数が呼び出されていますね．
つまり，`tz.fromutc(datetime.now().replace(tzinfo=tz))`という処理が走っているということです．
UTCで時間のdatetimeを生成 => タイムゾーンを付与したdatetimeを生成 => タイムゾーンのdatetimeに変換．．してそうです．

一方で標準ライブラリのタイムゾーンを引数に与えた場合は，その辺を`{built-in method now}`が処理するようで，内部の仕様はわかりませんが効率的に処理しているっぽいことが伺えます．

# おわりに
と，いうことで・・
つまり標準ライブラリのtimezoneを皆さん使っていきましょう！
以上です！

誤字脱字を始め不正確な内容がございましたら遠慮なくご指摘頂けると幸いです．