Help us understand the problem. What is going on with this article?

timedelta(Python)のマイナス値表現の謎

python(3.7)のtimedeltaで負になるような値を作ろうとすると、困惑することがあります。その謎を追ってみました。

timedeltaで正・負それぞれつくってみる

正になるパターン

In [1]: import datetime

In [2]: n = datetime.datetime.now()

# 現在時刻 - (現在時刻1秒前) = 1秒
In [3]: t = n - (n - datetime.timedelta(seconds=1))

In [4]: t
Out[4]: datetime.timedelta(seconds=1)

特に何の違和感もないですね。

負になるパターン

# 現在時刻 - (現在時刻1秒後) = -1秒
In [5]: t = n - (n + datetime.timedelta(seconds=1))

In [6]: t
Out[6]: datetime.timedelta(days=-1, seconds=86399)

!?

直感ではtimedelta(seconds=-1)とか出てきそうなもんですが、なんだかデカイ値がでてきました。しかしこの結果、冷静に考えてみれば、

  1. 1日前かつ、
  2. 86399秒後(1日は86400秒)

なので、「1秒前(-1秒)」と同じになります。

なぜか?

ドキュメントよく見ると書いてあるんですよね、これ。

さらに、値が一意に表されるように days, seconds, microseconds が以下のように正規化されます
・0 <= microseconds < 1000000
・0 <= seconds < 3600*24 (一日中の秒数)
・-999999999 <= days <= 999999999

https://docs.python.org/ja/3/library/datetime.html#datetime.timedelta

つまり、値の一意性を保つためにsecondsとmicrosecondsの値を正の値に限っています。 (あと、上の単位にまとめられるは場合はまとめるもしている)

どういうこと?

なぜこのような設計にしたか、という具体的な議論や文書は見つかりませんでした。
普通に考えられる点としては、比較の実装が単純になるということでしょうか。

例えば、secondsにマイナス値を許す場合、以下は同じであると判定しなければいけません。

  • timedelta(seconds=-1)
  • timedelta(days=-1, seconds=86399)

こうなると、timedeltaオブジェクトの比較時に毎回計算が必要になってしまいそうです。しかし、一意性があるならば、それぞれ3つの数値フィールドがすべて同じであれば同一であるという簡単な実装が行えます。

実際、timedeltaの実装(ぽいもの)を見てみるとそのような感じでした。
以下、cpythonレポジトリから抜粋

# 抜粋
static int
delta_cmp(PyObject *self, PyObject *other)
{
    int diff = GET_TD_DAYS(self) - GET_TD_DAYS(other);
    if (diff == 0) {
        diff = GET_TD_SECONDS(self) - GET_TD_SECONDS(other);
        if (diff == 0)
            diff = GET_TD_MICROSECONDS(self) -
                GET_TD_MICROSECONDS(other);
    }
    return diff;
}

すごく単純な計算ですよね。ちなみに正規化の計算自体はこちらでやってそうでした。

ちなみに

コンストラクタは負の値を受け入れることができます。内部で先ほどの正規化関数により値が操作されています。

In [14]: datetime.timedelta(seconds=-1)
Out[14]: datetime.timedelta(days=-1, seconds=86399)

注意点

これ、つまりはtimedeltaオブジェクトのseconds, microsencondsフィールドの単体での利用は注意が必要ということです。

In [15]: datetime.timedelta(seconds=-1).seconds
Out[15]: 86399

ここで得たいのは-1なはず。total_secondsメソッドを使えば期待した結果が得られます。(floatですけど)

In [16]: datetime.timedelta(seconds=-1).total_seconds()
Out[16]: -1.0

total_microsecondsの実装はないようでした。

現場からは以上です。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away