日時・丸め・乱数の再現性
同じ瞬間をUTCと東京の時刻で表示し、夏時間の終了時に同じ時計表示が二回現れる例を扱います。続いて丸め方法と乱数の状態を変え、最後は必要な条件を全て引数へ渡して同じ結果を再現します。
説明
同じ入力でも、地域の時刻表示、丸め方法、乱数の状態などが違えば結果は変わります。依存パッケージの版を揃えるだけでは、実行時に参照する条件まで同じになるとは限りません。
同じ瞬間をUTCと東京の時刻で表示し、夏時間の終了時に同じ時計表示が二回現れる例を扱います。続いて丸め方法と乱数の状態を変え、最後は必要な条件を全て引数へ渡して同じ結果を再現します。
1同じ瞬間を別の地域で表示
UTCの2026年11月9日0時を東京へ変換します。
from datetime import datetime, timezone; from zoneinfo import ZoneInfo; instant = datetime(2026, 11, 9, 0, 0, tzinfo=timezone.utcUTCとして時刻を作る); tokyo = instant.astimezone(ZoneInfo("Asia/Tokyo"))同じ瞬間を東京で表示; print(instant.isoformat(), tokyo.isoformat()); print(instant.timestamp() == tokyo.timestamp()時系列上の位置を比較)UTCの00:00と東京の09:00は同じ瞬間で、タイムスタンプも一致します。表示上の日付・時刻が異なることと、時系列上の位置が異なることを混同しないようにします。
2タイムゾーンを付け替えた場合
astimezoneによる変換と、replaceで地域情報だけを変更した場合を比較します。
relabelled = instant.replace(tzinfo=ZoneInfo("Asia/Tokyo"))時計表示を保って地域情報を変更; print(relabelled.isoformat()); print(int(relabelled.timestamp() - instant.timestamp()瞬間の差を秒で確認))時計表示は00:00のまま+09:00になり、元の瞬間より32400秒前を表します。replaceは「同じ瞬間の別地域表示への変換」ではありません。地域情報のない値へ意味を与える場面と、既存の瞬間を変換する場面を分けます。
3同じ地域時刻が二回現れる場合
2024年11月3日のニューヨークでは夏時間終了時に01:30が二回現れます。foldでその前後を指定します。
ny = ZoneInfo("America/New_York"); first = datetime(2024, 11, 3, 1, 30, tzinfo=ny, fold=0繰返し前半の01:30); second = first.replace(fold=1)繰返し後半の01:30; print(first.isoformat(), second.isoformat()); print(first.astimezone(timezone.utc)最初のUTC時刻.isoformat(), second.astimezone(timezone.utc)二回目のUTC時刻.isoformat())最初はUTC-04:00で05:30Z、二回目はUTC-05:00で06:30Zです。同じ01:30という文字列だけでは、どちらの瞬間か決まりません。地域規則とオフセット、必要ならfoldを保ちます。
4時差をUTCで計算する
二つの01:30をUTCへ変換して差を求めます。
elapsed = second.astimezone(timezone.utc) - first.astimezone(timezone.utc)同じ基準で差を取る; print(elapsed.total_seconds()経過秒数)差は3600秒です。同じ地域情報を持つdatetime同士の演算には壁時計の時刻としての規則があるため、経過時間を求めるこの場面ではUTCへ変換してから差を計算します。
5小数の丸め方法を指定する
2.5を整数桁へ丸め、ちょうど中間の場合の規則を比較します。
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP; amount = Decimal("2.5")十進文字列から値を作る; print(amount.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN中間値を偶数側へ), amount.quantize(Decimal("1"), rounding=ROUND_HALF_UP中間値を0から遠い側へ))偶数側へ丸めるHALF_EVENでは2、通常の四捨五入に相当するこの正数のHALF_UPでは3です。Decimalを使っても、丸め方法を指定しなければ規則の違いが消えるわけではありません。
6乱数生成器の初期状態
同じseedから生成器を二つ作り、最初の三つの値を取得します。
import random; r1 = random.Random(7)初期状態を指定した生成器; r2 = random.Random(7)同じ初期状態の別生成器; print([round(r1.random()一つ目の列, 6) for _ in range(3)]); print([round(r2.random()二つ目の列, 6) for _ in range(3)])二つの列は一致します。同じ生成方式と同じ初期状態なら同じ順序で値を得られます。これは再現用の疑似乱数であり、秘密値の生成に使う方式の説明ではありません。
7途中の状態を保存して再開する
r1の現在状態を保存し、次の値を取得してから状態を戻します。
saved = r1.getstate()現在位置を含む状態を保存; next_value = r1.random()次の値を取得; r1.setstate(saved)取得前へ状態を戻す; print(next_value == r1.random()同じ位置の値を比較)Trueです。seedだけでなく、そこから何回値を取り出したかも次の結果へ影響します。状態を復元すると、途中の同じ位置から再開できます。
8計算に使う条件をすべて引数で渡す
現在時刻を内部で取得せず、対象の瞬間・表示地域・金額・丸め・乱数seedを全て受け取る関数を定義します。
def make_record(when, zone, text_amount, rounding, seed):計算条件を全て受け取る
rng = random.Random(seed)この呼出し専用の生成器
return {
"at": when.astimezone(ZoneInfo(zone))明示した地域で表示.isoformat(),
"amount": str(Decimal(text_amount).quantize(Decimal("1"), rounding=rounding明示した丸め方法)),
"sample": round(rng.random(), 6),
}関数の外に隠れた現在時刻や共有乱数の進行位置へ依存しません。ただし、地域規則のデータや実行系の版は環境条件として残るため、それらも再現条件に含めます。
9同じ条件で再現する
同じ引数を二度渡し、内容を比較します。
args =再現に使う条件の組 (instant, "Asia/Tokyo", "2.5", ROUND_HALF_UP, 7); one = make_record(*args)一回目の結果; two = make_record(*args)同じ条件で再実行; print(one); print(one == two結果全体を比較)日時、丸め後の金額3、標本値が一致します。現在時刻や共有乱数を内部で取得する方式より、どの条件から結果が得られたかを記録しやすくなります。
10一つの条件だけを変える
丸め方法だけをHALF_EVENへ変更します。
changed = make_record(instant, "Asia/Tokyo", "2.5", ROUND_HALF_EVEN丸め方法だけを変更, 7); print({key: (one[key], changed[key]) for key in one if one[key] != changed[key]変化した項目だけを表示})差はamountの3と2だけです。条件を明示して一つずつ変えると、出力の違いをどの条件が生んだか確認できます。依存の版、入力、地域規則、時刻、乱数状態、丸めを必要な範囲で揃えることが再現の土台です。
まとめ
時刻の表示地域の変換は、同じ瞬間を保ちます。時計表示だけから瞬間が一意に決まらない場合もあります。
丸め規則と疑似乱数の状態は結果へ影響するため、再現に必要な条件として明示します。
入力以外に依存する時刻・設定・実行系・地域データも確認し、一つの条件だけを変えた比較で原因を調べます。
操作履歴と解説を振り返れます。
Python 3
from datetime import datetime, timezone; from zoneinfo import ZoneInfo; instant = datetime(2026, 11, 9, 0, 0, tzinfo=timezone.utc); tokyo = instant.astimezone(ZoneInfo("Asia/Tokyo")); print(instant.isoformat(), tokyo.isoformat()); print(instant.timestamp() == tokyo.timestamp())2026-11-09T00:00:00+00:00 2026-11-09T09:00:00+09:00 True
relabelled = instant.replace(tzinfo=ZoneInfo("Asia/Tokyo")); print(relabelled.isoformat()); print(int(relabelled.timestamp() - instant.timestamp()))2026-11-09T00:00:00+09:00 -32400
ny = ZoneInfo("America/New_York"); first = datetime(2024, 11, 3, 1, 30, tzinfo=ny, fold=0); second = first.replace(fold=1); print(first.isoformat(), second.isoformat()); print(first.astimezone(timezone.utc).isoformat(), second.astimezone(timezone.utc).isoformat())2024-11-03T01:30:00-04:00 2024-11-03T01:30:00-05:00 2024-11-03T05:30:00+00:00 2024-11-03T06:30:00+00:00
elapsed = second.astimezone(timezone.utc) - first.astimezone(timezone.utc); print(elapsed.total_seconds())3600.0
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP; amount = Decimal("2.5"); print(amount.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN), amount.quantize(Decimal("1"), rounding=ROUND_HALF_UP))2 3
import random; r1 = random.Random(7); r2 = random.Random(7); print([round(r1.random(), 6) for _ in range(3)]); print([round(r2.random(), 6) for _ in range(3)])[0.323833, 0.150849, 0.650934] [0.323833, 0.150849, 0.650934]
saved = r1.getstate(); next_value = r1.random(); r1.setstate(saved); print(next_value == r1.random())True
def make_record(when, zone, text_amount, rounding, seed):
rng = random.Random(seed)
return {
"at": when.astimezone(ZoneInfo(zone)).isoformat(),
"amount": str(Decimal(text_amount).quantize(Decimal("1"), rounding=rounding)),
"sample": round(rng.random(), 6),
}args = (instant, "Asia/Tokyo", "2.5", ROUND_HALF_UP, 7); one = make_record(*args); two = make_record(*args); print(one); print(one == two){'at': '2026-11-09T09:00:00+09:00', 'amount': '3', 'sample': 0.323833}
Truechanged = make_record(instant, "Asia/Tokyo", "2.5", ROUND_HALF_EVEN, 7); print({key: (one[key], changed[key]) for key in one if one[key] != changed[key]}){'amount': ('3', '2')}演習完了