Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 615 – Unterstützung für die IANA Zeitzonendatenbank in der Standardbibliothek

Autor:
Paul Ganssle <paul at ganssle.io>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Erstellt:
22. Feb. 2020
Python-Version:
3.9
Post-History:
25. Feb. 2020, 29. Mär. 2020
Ersetzt:
431

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie nun unter zoneinfo.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Dieses PEP schlägt die Hinzufügung eines Moduls, zoneinfo, vor, das eine konkrete Zeitzonenimplementierung zur Unterstützung der IANA-Zeitzonendatenbank bereitstellt. Standardmäßig verwendet zoneinfo die Zeitzonendaten des Systems, falls verfügbar; wenn keine Systemzeitzonendaten verfügbar sind, greift die Bibliothek auf das First-Party-Paket tzdata zurück, das auf PyPI bereitgestellt wird. [d]

Motivation

Die datetime-Bibliothek verwendet einen flexiblen Mechanismus zur Handhabung von Zeitzonen: alle Konvertierungen und Abfragen von Zeitzoneninformationen werden an eine Instanz einer Unterklasse der abstrakten Basisklasse datetime.tzinfo delegiert. [10] Dies ermöglicht es Benutzern, beliebig komplexe Zeitzonenregeln zu implementieren, aber in der Praxis wünschen sich die meisten Benutzer Unterstützung für nur drei Arten von Zeitzonen: [a]

  1. UTC und feste Offsets davon
  2. Die lokale Systemzeitzone
  3. IANA-Zeitzonen

In Python 3.2 wurde die Klasse datetime.timezone eingeführt, um die erste Art von Zeitzone zu unterstützen (mit einem speziellen datetime.timezone.utc Singleton für UTC).

Obwohl es immer noch keine "lokale" Zeitzone gibt, wurde in Python 3.0 die Semantik von naiven Zeitzonen geändert, um viele "lokale Zeit"-Operationen zu unterstützen, und es ist nun möglich, einen festen Zeitzonen-Offset von einer lokalen Zeit zu erhalten.

>>> print(datetime(2020, 2, 22, 12, 0).astimezone())
2020-02-22 12:00:00-05:00
>>> print(datetime(2020, 2, 22, 12, 0).astimezone()
...       .strftime("%Y-%m-%d %H:%M:%S %Z"))
2020-02-22 12:00:00 EST
>>> print(datetime(2020, 2, 22, 12, 0).astimezone(timezone.utc))
2020-02-22 17:00:00+00:00

Es gibt jedoch immer noch keine Unterstützung für die in der IANA-Zeitzonendatenbank (auch "tz"-Datenbank oder Olson-Datenbank genannt [6]) beschriebenen Zeitzonen. Die Zeitzonendatenbank ist gemeinfrei und weit verbreitet – sie ist auf vielen Unix-ähnlichen Betriebssystemen standardmäßig vorhanden. Es wird sehr auf die Stabilität der Datenbank geachtet: es gibt IETF RFCs sowohl für die Wartungsverfahren (RFC 6557) als auch für das kompilierte Binärformat (TZif) (RFC 8536). Daher ist es wahrscheinlich, dass die Unterstützung für die kompilierten Ausgaben der IANA-Datenbank den Endbenutzern auch bei der relativ langen Kadenz von Standardbibliotheksversionen einen großen Mehrwert bringt.

Vorschlag

Dieses PEP hat drei Hauptanliegen

  1. Die Semantik der zoneinfo.ZoneInfo Klasse (zoneinfo-class)
  2. Verwendete Zeitzonendatenquellen (data-sources)
  3. Optionen zur Konfiguration des Zeitzonen-Suchpfads (search-path-config)

Aufgrund der Komplexität des Vorschlags sind die Designentscheidungen und Begründungen nicht in separaten Abschnitten "Spezifikation" und "Begründung" aufgeteilt, sondern nach Themen gruppiert.

Die zoneinfo.ZoneInfo Klasse

Konstruktoren

Das ursprüngliche Design der zoneinfo.ZoneInfo Klasse hat mehrere Konstruktoren.

ZoneInfo(key: str)

Der primäre Konstruktor nimmt ein einzelnes Argument, key, das ein String ist, der den Namen einer Zonendatei in der Systemzeitzonendatenbank angibt (z.B. "America/New_York", "Europe/London"), und gibt eine ZoneInfo zurück, die aus der ersten passenden Datenquelle im Suchpfad erstellt wurde (weitere Details siehe Abschnitt data-sources). Alle Zoneninformationen müssen beim Erstellen aus der Datenquelle (normalerweise einer TZif-Datei) sofort gelesen werden und dürfen während der Lebensdauer des Objekts nicht mehr geändert werden (diese Einschränkung gilt für alle ZoneInfo-Konstruktoren).

Wenn im Suchpfad keine passende Datei gefunden wird (entweder weil das System keine Zeitzonendaten bereitstellt oder weil der Schlüssel ungültig ist), löst der Konstruktor eine zoneinfo.ZoneInfoNotFoundError aus, die eine Unterklasse von KeyError sein wird.

Eine etwas ungewöhnliche Garantie dieses Konstruktors ist, dass Aufrufe mit identischen Argumenten *identische* Objekte zurückgeben müssen. Insbesondere muss für alle Werte von key die folgende Aussage immer gültig sein [b]

a = ZoneInfo(key)
b = ZoneInfo(key)
assert a is b

Der Grund dafür liegt in der Tatsache, dass die Semantik von Datums-/Zeitoperationen (z.B. Vergleiche, Arithmetik) davon abhängt, ob die beteiligten Datums-/Zeitobjekte dieselben oder verschiedene Zonen darstellen; zwei Datums-/Zeitobjekte befinden sich nur dann in derselben Zone, wenn dt1.tzinfo is dt2.tzinfo. [1] Zusätzlich zum geringen Leistungsvorteil, der durch die Vermeidung einer unnötigen Vermehrung von ZoneInfo-Objekten entsteht, sollte diese Garantie überraschendes Verhalten für Endbenutzer minimieren.

dateutil.tz.gettz bietet seit Version 2.7.0 (Veröffentlichung März 2018) eine ähnliche Garantie. [16]

Hinweis

Die Implementierung kann entscheiden, wie das Cache-Verhalten umgesetzt wird, aber die hier gegebene Garantie verlangt nur, dass solange zwei Referenzen auf das Ergebnis identischer Konstruktoraufrufe bestehen, sie Referenzen auf dasselbe Objekt sein müssen. Dies stimmt mit einem referenzgezählten Cache überein, bei dem ZoneInfo-Objekte freigegeben werden, wenn keine Referenzen mehr auf sie zeigen (z.B. ein Cache, der mit einem weakref.WeakValueDictionary implementiert ist) – es ist erlaubt, aber nicht erforderlich oder empfohlen, dies mit einem "starken" Cache zu implementieren, bei dem alle ZoneInfo-Objekte unbegrenzt am Leben erhalten werden.

ZoneInfo.no_cache(key: str)

Dies ist ein alternativer Konstruktor, der den Cache des Konstruktors umgeht. Er ist identisch mit dem primären Konstruktor, gibt aber bei jedem Aufruf ein neues Objekt zurück. Dies ist wahrscheinlich am nützlichsten für Testzwecke oder um absichtlich "unterschiedliche Zonen"-Semantik zwischen Datums-/Zeitobjekten mit derselben nominalen Zeitzone zu induzieren.

Selbst wenn ein mit dieser Methode konstruiertes Objekt ein Cache-Fehlschlag gewesen wäre, darf es nicht in den Cache aufgenommen werden; mit anderen Worten, die folgende Aussage sollte immer wahr sein

>>> a = ZoneInfo.no_cache(key)
>>> b = ZoneInfo(key)
>>> a is not b
ZoneInfo.from_file(fobj: IO[bytes], /, key: str = None)

Dies ist ein alternativer Konstruktor, der die Erstellung eines ZoneInfo-Objekts aus jedem TZif-Byte-Stream ermöglicht. Dieser Konstruktor nimmt einen optionalen Parameter, key, entgegen, der den Namen der Zone für die Zwecke von __str__ und __repr__ festlegt (siehe Representations).

Im Gegensatz zum primären Konstruktor erstellt dieser immer ein neues Objekt. Es gibt zwei Gründe, warum dies vom Caching-Verhalten des primären Konstruktors abweicht: Stream-Objekte haben einen veränderlichen Zustand und es ist daher schwierig oder unmöglich zu bestimmen, ob zwei Eingaben identisch sind, und es ist wahrscheinlich, dass Benutzer, die aus einer Datei erstellen, speziell von dieser Datei und nicht aus einem Cache laden möchten.

Wie bei ZoneInfo.no_cache dürfen Objekte, die mit dieser Methode konstruiert wurden, nicht in den Cache aufgenommen werden.

Verhalten bei Datenaktualisierungen

Es ist wichtig, dass sich das Verhalten eines bestimmten ZoneInfo-Objekts während seiner Lebensdauer nicht ändert, da die Methode utcoffset() eines datetime sowohl in seinen Gleichheits- als auch in seinen Hash-Berechnungen verwendet wird, und wenn sich das Ergebnis während der Lebensdauer des datetime ändert, könnte dies die Invariante für alle hashbaren Objekte brechen [3] [4], dass wenn x == y, es auch wahr sein muss, dass hash(x) == hash(y) [c] .

Unter Berücksichtigung sowohl der Wahrung der datetime-Invarianten als auch der primären Konstruktorvereinbarung, immer dasselbe Objekt zurückzugeben, wenn es mit identischen Argumenten aufgerufen wird, darf bei einer Aktualisierung der Zeitzonendatenquelle während der Ausführung des Interpreters keine Caches invalidiert oder vorhandene ZoneInfo-Objekte geändert werden. Neu erstellte ZoneInfo-Objekte sollten jedoch aus der aktualisierten Datenquelle stammen.

Dies bedeutet, dass der Zeitpunkt, zu dem die Datenquelle für neue Aufrufe des ZoneInfo-Konstruktors aktualisiert wird, hauptsächlich von der Semantik des Caching-Verhaltens abhängt. Die einzig garantierte Möglichkeit, ein ZoneInfo-Objekt aus einer aktualisierten Datenquelle zu erhalten, besteht darin, einen Cache-Miss zu induzieren, entweder durch Umgehung des Caches und Verwendung von ZoneInfo.no_cache oder durch Löschen des Caches.

Hinweis

Das spezifizierte Cache-Verhalten erfordert nicht, dass der Cache träge gefüllt wird – es ist mit der Spezifikation vereinbar (wenn auch nicht empfohlen), den Cache proaktiv mit Zeitzonen vorab zu füllen, die noch nie konstruiert wurden.

Bewusste Cache-Invalidierung

Neben ZoneInfo.no_cache, mit dem ein Benutzer den Cache *umgehen* kann, stellt ZoneInfo auch eine Methode clear_cache bereit, um entweder den gesamten Cache oder selektive Teile des Caches absichtlich zu invalidieren.

ZoneInfo.clear_cache(*, only_keys: Iterable[str]=None) -> None

Wenn keine Argumente übergeben werden, werden alle Caches invalidiert und der erste Aufruf für jeden Schlüssel des primären ZoneInfo-Konstruktors nach dem Löschen des Caches gibt eine neue Instanz zurück.

>>> NYC0 = ZoneInfo("America/New_York")
>>> NYC0 is ZoneInfo("America/New_York")
True
>>> ZoneInfo.clear_cache()
>>> NYC1 = ZoneInfo("America/New_York")
>>> NYC0 is NYC1
False
>>> NYC1 is ZoneInfo("America/New_York")
True

Ein optionaler Parameter, only_keys, nimmt ein iterierbares Objekt von Schlüsseln entgegen, die aus dem Cache gelöscht werden sollen, andernfalls bleibt der Cache unberührt.

>>> NYC0 = ZoneInfo("America/New_York")
>>> LA0 = ZoneInfo("America/Los_Angeles")
>>> ZoneInfo.clear_cache(only_keys=["America/New_York"])
>>> NYC1 = ZoneInfo("America/New_York")
>>> LA0 = ZoneInfo("America/Los_Angeles")
>>> NYC0 is NYC1
False
>>> LA0 is LA1
True

Die Manipulation des Cache-Verhaltens wird als Nischenanwendungsfall erwartet; diese Funktion dient hauptsächlich zum Testen und um Benutzern mit ungewöhnlichen Anforderungen die Anpassung des Cache-Invalidierungsverhaltens an ihre Bedürfnisse zu ermöglichen.

String-Darstellung

Die __str__-Darstellung der ZoneInfo-Klasse wird aus dem Parameter key abgeleitet. Dies liegt teilweise daran, dass der key einen menschenlesbaren "Namen" des Strings darstellt, aber auch, weil es ein nützlicher Parameter ist, der den Benutzern zur Verfügung gestellt werden soll. Es ist notwendig, einen Mechanismus zur Anzeige des Schlüssels für die Serialisierung zwischen Sprachen bereitzustellen und weil er auch ein primärer Schlüssel für Lokalisierungsprojekte wie CLDR (Unicode Common Locale Data Repository [5]) ist.

Ein Beispiel

>>> zone = ZoneInfo("Pacific/Kwajalein")
>>> str(zone)
'Pacific/Kwajalein'

>>> dt = datetime(2020, 4, 1, 3, 15, tzinfo=zone)
>>> f"{dt.isoformat()} [{dt.tzinfo}]"
'2020-04-01T03:15:00+12:00 [Pacific/Kwajalein]'

Wenn kein key angegeben ist, darf die str-Operation nicht fehlschlagen, sondern sollte die __repr__ des Objekts zurückgeben.

>>> zone = ZoneInfo.from_file(f)
>>> str(zone)
'ZoneInfo.from_file(<_io.BytesIO object at ...>)'

Die __repr__ einer ZoneInfo ist implementierungsabhängig und nicht notwendigerweise stabil zwischen Versionen, darf aber kein gültiger ZoneInfo-Schlüssel sein, um Verwechslungen zwischen einer vom Schlüssel abgeleiteten ZoneInfo mit einer gültigen __str__ und einer aus einer Datei abgeleiteten ZoneInfo, die auf die __repr__ zurückgefallen ist, zu vermeiden.

Da die Verwendung von str() zum Zugriff auf den Schlüssel keine einfache Möglichkeit bietet, die *Präsenz* eines Schlüssels zu überprüfen (die einzige Möglichkeit besteht darin, zu versuchen, eine ZoneInfo daraus zu erstellen und zu erkennen, ob eine Ausnahme ausgelöst wird), werden ZoneInfo-Objekte auch ein schreibgeschütztes Attribut key bereitstellen, das None ist, wenn kein Schlüssel angegeben wurde.

Pickle-Serialisierung

Anstatt alle Übergangsdaten zu serialisieren, werden ZoneInfo-Objekte nach Schlüssel serialisiert, und ZoneInfo-Objekte, die aus rohen Dateien erstellt wurden (auch solche mit einem angegebenen key), können nicht gepickelt werden.

Das Verhalten eines ZoneInfo-Objekts hängt davon ab, wie es konstruiert wurde.

  1. ZoneInfo(key): Wenn es mit dem primären Konstruktor erstellt wird, wird ein ZoneInfo-Objekt nach Schlüssel serialisiert, und bei der Deserialisierung wird der primäre Konstruktor im Deserialisierungsprozess verwendet, und es wird erwartet, dass es dasselbe Objekt wie andere Referenzen auf dieselbe Zeitzone ist. Zum Beispiel, wenn europe_berlin_pkl ein String ist, der ein Pickle enthält, das aus ZoneInfo("Europe/Berlin") erstellt wurde, würde man folgendes Verhalten erwarten
    >>> a = ZoneInfo("Europe/Berlin")
    >>> b = pickle.loads(europe_berlin_pkl)
    >>> a is b
    True
    
  2. ZoneInfo.no_cache(key): Wenn es vom Cache-umgehenden Konstruktor erstellt wird, wird das ZoneInfo-Objekt immer noch nach Schlüssel serialisiert, aber bei der Deserialisierung wird der Cache-umgehende Konstruktor verwendet. Wenn europe_berlin_pkl_nc ein String ist, der ein Pickle enthält, das aus ZoneInfo.no_cache("Europe/Berlin") erstellt wurde, würde man folgendes Verhalten erwarten
    >>> a = ZoneInfo("Europe/Berlin")
    >>> b = pickle.loads(europe_berlin_pkl_nc)
    >>> a is b
    False
    
  3. ZoneInfo.from_file(fobj, /, key=None): Wenn es aus einer Datei erstellt wird, löst das ZoneInfo-Objekt beim Pickling eine Ausnahme aus. Wenn ein Endbenutzer ein ZoneInfo-Objekt pickeln möchte, das aus einer Datei erstellt wurde, wird empfohlen, dass er einen Wrapper-Typ oder eine benutzerdefinierte Serialisierungsfunktion verwendet: entweder die Serialisierung nach Schlüssel oder das Speichern des Inhalts des Datei-Objekts und dessen Serialisierung.

Diese Serialisierungsmethode erfordert, dass die Zeitzonendaten für den benötigten Schlüssel sowohl auf der serialisierenden als auch auf der deserialisierenden Seite verfügbar sind, ähnlich wie Referenzen auf Klassen und Funktionen in beiden Umgebungen vorhanden sein müssen. Sie bedeutet auch, dass keine Garantien für die Konsistenz der Ergebnisse übernommen werden, wenn eine ZoneInfo entpickelt wird, die in einer Umgebung mit einer anderen Version der Zeitzonendaten gepickelt wurde.

Quellen für Zeitzonendaten

Eine der schwierigsten Herausforderungen für die Unterstützung von IANA-Zeitzonen ist die Aktualität der Daten; zwischen 1997 und 2020 gab es zwischen 3 und 21 Veröffentlichungen pro Jahr, oft als Reaktion auf Änderungen der Zeitzonenregeln mit wenig oder gar keiner Vorankündigung (weitere Details siehe [7]). Um aktuell zu bleiben und dem Systemadministrator die Kontrolle über die Datenquelle zu geben, schlagen wir vor, systemweit bereitgestellte Zeitzonendaten zu verwenden, wo immer dies möglich ist. Allerdings liefern nicht alle Systeme eine öffentlich zugängliche Zeitzonendatenbank – insbesondere Windows verwendet ein anderes System zur Verwaltung von Zeitzonen – und so greift zoneinfo, falls verfügbar, auf ein installierbares First-Party-Paket, tzdata, zurück, das auf PyPI verfügbar ist. [d] Wenn keine Systemzoneninformationsdateien gefunden werden, aber tzdata installiert ist, wird der primäre ZoneInfo-Konstruktor tzdata als Zeitzonenquelle verwenden.

Systemzeitzoneninformationen

Viele Unix-ähnliche Systeme stellen Zeitzonendaten standardmäßig bereit oder bieten ein kanonisches Zeitzonendatenpaket (oft tzdata genannt, wie z.B. unter Arch Linux, Fedora und Debian). Wo immer möglich, wäre es vorzuziehen, auf die Systemzeitzoneninformationen zurückzugreifen, da dies ermöglicht, Zeitzoneninformationen für alle Sprachstacks an einem Ort zu aktualisieren und zu pflegen. Python-Distributoren werden ermutigt, sicherzustellen, dass Zeitzonendaten wann immer möglich zusammen mit Python installiert werden (z.B. durch Deklaration von tzdata als Abhängigkeit für das python-Paket).

Das Modul zoneinfo wird eine "Suchpfad"-Strategie verwenden, analog zur PATH-Umgebungsvariablen oder der sys.path-Variable in Python; die Variable zoneinfo.TZPATH wird eine schreibgeschützte, geordnete Liste von Speicherorten für Zeitzonendaten sein, die durchsucht werden sollen (weitere Details siehe search-path-config). Beim Erstellen einer ZoneInfo-Instanz aus einem Schlüssel wird die Zonendatei aus der ersten Datenquelle im Pfad erstellt, in der der Schlüssel existiert. Zum Beispiel, wenn TZPATH wäre

TZPATH = (
    "/usr/share/zoneinfo",
    "/etc/zoneinfo"
    )

und (obwohl dies sehr ungewöhnlich wäre) /usr/share/zoneinfo nur America/New_York enthielte und /etc/zoneinfo sowohl America/New_York als auch Europe/Moscow enthielte, dann würde ZoneInfo("America/New_York") durch /usr/share/zoneinfo/America/New_York bedient werden, während ZoneInfo("Europe/Moscow") durch /etc/zoneinfo/Europe/Moscow bedient würde.

Auf Windows-Systemen ist der Suchpfad standardmäßig leer, da Windows keine offizielle Kopie der Zeitzonendatenbank mitliefert. Auf Nicht-Windows-Systemen ist der Suchpfad standardmäßig eine Liste der gebräuchlichsten Suchpfade. Obwohl dies in zukünftigen Versionen geändert werden kann, wird der Standard-Suchpfad bei der Veröffentlichung sein

TZPATH = (
    "/usr/share/zoneinfo",
    "/usr/lib/zoneinfo",
    "/usr/share/lib/zoneinfo",
    "/etc/zoneinfo",
)

Dies kann sowohl zur Kompilierungszeit als auch zur Laufzeit konfiguriert werden; weitere Informationen zu den Konfigurationsoptionen unter search-path-config.

Das Python-Paket tzdata

Um den Endbenutzern einen einfachen Zugriff auf Zeitzonendaten zu ermöglichen, schlägt dieses PEP die Erstellung eines reinen Datenpakets tzdata als Fallback für den Fall vor, dass Systemdaten nicht verfügbar sind. Das tzdata-Paket würde auf PyPI als "First Party"-Paket [d] vertrieben und vom CPython-Entwicklungsteam gepflegt.

Das tzdata-Paket enthält nur Daten und Metadaten, keine öffentlichen Funktionen oder Klassen. Es wird so konzipiert, dass es sowohl mit neueren importlib.resources [11]-Zugriffsmuster als auch mit älteren Mustern wie pkgutil.get_data [12] kompatibel ist.

Obwohl es speziell für die Verwendung von CPython entwickelt wurde, ist das tzdata-Paket ein eigenständiges öffentliches Paket und kann als "offizielle" Quelle für Zeitzonendaten für Drittanbieter-Python-Pakete verwendet werden.

Konfiguration des Suchpfads

Der Zeitzonen-Suchpfad ist sehr systemabhängig und manchmal sogar anwendungsabhängig, daher ist es sinnvoll, Optionen zur Anpassung bereitzustellen. Dieses PEP sieht drei solche Anpassungsmöglichkeiten vor.

  1. Globale Konfiguration über eine Kompilierungszeitoption
  2. Konfiguration pro Ausführung über Umgebungsvariablen
  3. Laufzeit-Konfigurationsänderung über eine Funktion reset_tzpath

Bei allen Konfigurationsmethoden muss der Suchpfad nur absolute, keine relativen Pfade enthalten. Implementierungen können wählen, ob sie ignoriert, eine Warnung ausgeben oder eine Ausnahme auslösen, wenn ein String, der kein absoluter Pfad ist, gefunden wird (und können unterschiedliche Entscheidungen je nach Kontext treffen – z.B. eine Ausnahme auslösen, wenn ein ungültiger Pfad an reset_tzpath übergeben wird, aber eine Warnung ausgeben, wenn er in der Umgebungsvariablen enthalten ist). Wenn keine Ausnahme ausgelöst wird, dürfen keine Strings, die keine absoluten Pfade sind, in den Zeitzonen-Suchpfad aufgenommen werden.

Kompilierungszeitoptionen

Es ist am wahrscheinlichsten, dass nachgeschaltete Distributoren genau wissen, wo ihre Systemzeitzonendaten bereitgestellt werden, und daher wird eine Kompilierungszeitoption PYTHONTZPATH bereitgestellt, um den Standard-Suchpfad festzulegen.

Die Option PYTHONTZPATH sollte ein mit os.pathsep getrennter String sein, der mögliche Speicherorte für die Bereitstellung der Zeitzonendaten auflistet (z.B. /usr/share/zoneinfo).

Umgebungsvariablen

Beim Initialisieren von TZPATH (und immer dann, wenn reset_tzpath ohne Argumente aufgerufen wird) verwendet das Modul zoneinfo die Umgebungsvariable PYTHONTZPATH, falls sie vorhanden ist, um den Suchpfad festzulegen.

PYTHONTZPATH ist ein mit os.pathsep getrennter String, der den Standard-Zeitzonenpfad *ersetzt* (und nicht ergänzt). Einige Beispiele für die vorgeschlagene Semantik

$ python print_tzpath.py
("/usr/share/zoneinfo",
 "/usr/lib/zoneinfo",
 "/usr/share/lib/zoneinfo",
 "/etc/zoneinfo")

$ PYTHONTZPATH="/etc/zoneinfo:/usr/share/zoneinfo" python print_tzpath.py
("/etc/zoneinfo",
 "/usr/share/zoneinfo")

$ PYTHONTZPATH="" python print_tzpath.py
()

Dies bietet keinen integrierten Mechanismus zum Voranstellen oder Anhängen an den Standard-Suchpfad, da diese Anwendungsfälle wahrscheinlich eher eine Nische darstellen. Es sollte möglich sein, eine Umgebungsvariable mit dem Standard-Suchpfad relativ einfach zu befüllen.

$ export DEFAULT_TZPATH=$(python -c \
    "import os, zoneinfo; print(os.pathsep.join(zoneinfo.TZPATH))")

reset_tzpath Funktion

zoneinfo stellt eine Funktion reset_tzpath bereit, die es ermöglicht, den Suchpfad zur Laufzeit zu ändern.

def reset_tzpath(
    to: Optional[Sequence[Union[str, os.PathLike]]] = None
) -> None:
    ...

Wenn sie mit einer Sequenz von Pfaden aufgerufen wird, setzt diese Funktion zoneinfo.TZPATH auf ein Tupel, das aus dem gewünschten Wert konstruiert wurde. Wenn sie ohne Argumente oder mit None aufgerufen wird, setzt diese Funktion zoneinfo.TZPATH auf die Standardkonfiguration zurück.

Dies ist wahrscheinlich hauptsächlich nützlich, um die Verwendung von System-Zeitzonenpfaden zu deaktivieren (dauerhaft oder vorübergehend) und das Modul zu zwingen, das tzdata-Paket zu verwenden. Es ist unwahrscheinlich, dass reset_tzpath eine häufige Operation sein wird, außer vielleicht in Testfunktionen, die empfindlich auf die Zeitzonenkonfiguration reagieren, aber es scheint vorzuziehen, einen offiziellen Mechanismus zur Änderung zu bieten, anstatt eine Vermehrung von Hacks rund um die Unveränderlichkeit von TZPATH zuzulassen.

Vorsicht

Obwohl das Ändern von TZPATH während einer Ausführung eine unterstützte Operation ist, sollten Benutzer darauf hingewiesen werden, dass dies gelegentlich zu ungewöhnlichen Semantiken führen kann, und bei Design-Abwägungen wird größeren Gewicht auf die Verwendung eines statischen TZPATH gelegt, was der weitaus häufigere Anwendungsfall ist.

Wie in Konstruktoren erwähnt, verwendet der primäre ZoneInfo-Konstruktor einen Cache, um sicherzustellen, dass zwei identisch konstruierte ZoneInfo-Objekte immer als identisch verglichen werden (d.h. ZoneInfo(key) is ZoneInfo(key)), und die Natur dieses Caches ist implementierungsabhängig. Das bedeutet, dass das Verhalten des ZoneInfo-Konstruktors in einigen Situationen bei Verwendung desselben key unter verschiedenen Werten von TZPATH unvorhersehbar inkonsistent sein kann. Zum Beispiel

>>> reset_tzpath(to=["/my/custom/tzdb"])
>>> a = ZoneInfo("My/Custom/Zone")
>>> reset_tzpath()
>>> b = ZoneInfo("My/Custom/Zone")
>>> del a
>>> del b
>>> c = ZoneInfo("My/Custom/Zone")

In diesem Beispiel existiert My/Custom/Zone nur in /my/custom/tzdb und nicht im Standard-Suchpfad. In allen Implementierungen muss der Konstruktor für a erfolgreich sein. Es ist implementierungsabhängig, ob der Konstruktor für b erfolgreich ist, aber wenn er es ist, muss gelten, dass a is b, da sowohl a als auch b Referenzen auf denselben Schlüssel sind. Es ist auch implementierungsabhängig, ob der Konstruktor für c erfolgreich ist. Implementierungen von zoneinfo *können* das in früheren Konstruktoraufrufen erstellte Objekt zurückgeben, oder sie können mit einer Ausnahme fehlschlagen.

Abwärtskompatibilität

Dies wird keine Abwärtskompatibilitätsprobleme verursachen, da es eine neue API erstellt.

Mit nur geringfügigen Änderungen könnte ein Backport mit Unterstützung für Python 3.6+ des Moduls zoneinfo erstellt werden.

Das Paket tzdata ist als "nur Daten" konzipiert und sollte jede Python-Version unterstützen, für die es erstellt werden kann (einschließlich Python 2.7).

Sicherheitsimplikationen

Dies erfordert das Parsen von Zeitzoneninformationen von der Festplatte, hauptsächlich aus Systemverzeichnissen, aber potenziell auch aus vom Benutzer bereitgestellten Daten. Fehler in der Implementierung (insbesondere im C-Code) könnten zu potenziellen Sicherheitsproblemen führen, aber es gibt kein besonderes Risiko im Vergleich zum Parsen anderer Dateitypen.

Da die Schlüssel für Zeitzonendaten im Wesentlichen Pfade relativ zu einem Zeitzonen-Root sind, sollten Implementierungen darauf achten, Pfad-Traversal-Angriffe zu vermeiden. Das Anfordern von Schlüsseln wie ../../../path/to/something sollte nichts über den Zustand des Dateisystems außerhalb des Zeitzonenpfads preisgeben.

Referenzimplementierung

Eine erste Referenzimplementierung ist verfügbar unter https://github.com/pganssle/zoneinfo

Dies könnte schließlich in einen Backport für 3.6+ umgewandelt werden.

Abgelehnte Ideen

Erstellung eines benutzerdefinierten tzdb-Compilers

Ein Hauptbedenken bei der Verwendung des TZif-Formats ist, dass es nicht genügend Informationen enthält, um immer korrekt den Wert für tzinfo.dst() zu bestimmen. Dies liegt daran, dass TZif für jeden gegebenen Zeitzonen-Offset nur den UTC-Offset und die Angabe, ob es sich um einen DST-Offset handelt, speichert. tzinfo.dst() gibt jedoch den gesamten Betrag der DST-Verschiebung zurück, sodass der „Standard“-Offset aus datetime.utcoffset() - datetime.dst() rekonstruiert werden kann. Der für dst() zu verwendende Wert kann durch Ermittlung des entsprechenden STD-Offsets und Berechnung der Differenz ermittelt werden, aber das TZif-Format gibt nicht an, welche Offsets STD/DST-Paare bilden, sodass Heuristiken verwendet werden müssen, um dies zu bestimmen.

Eine gängige Heuristik – die Betrachtung des jüngsten Standard-Offsets – versagt insbesondere bei den Zeitzonenänderungen in Portugal 1992 und 1996, wo der „Standard“-Offset während eines DST-Übergangs um 1 Stunde verschoben wurde, was zu einem Übergang vom STD- zum DST-Status ohne Änderung des Offsets führte. Tatsächlich ist es möglich (wenn auch noch nie geschehen), dass eine Zeitzone geschaffen wird, die dauerhaft DST ist und keine Standard-Offsets hat.

Obwohl diese Informationen in den kompilierten TZif-Binärdateien fehlen, sind sie in den rohen tzdb-Dateien vorhanden, und es wäre möglich, diese Informationen selbst zu parsen und ein besser geeignetes Binärformat zu erstellen.

Diese Idee wurde aus mehreren Gründen abgelehnt

  1. Sie schließt die Verwendung von systemweit bereitgestellten Zeitzoneninformationen aus, die normalerweise nur im TZif-Format vorliegen.
  2. Das rohe tzdb-Format ist zwar stabil, aber *weniger* stabil als das TZif-Format; einige nachgelagerte tzdb-Parser hatten bereits Probleme mit alten Bereitstellungen ihrer benutzerdefinierten Parser, die mit neueren tzdb-Releases inkompatibel wurden, was zur Erstellung eines „Nachhut“-Formats führte, um den Übergang zu erleichtern. [8]
  3. Heuristiken reichen derzeit in dateutil und pytz für alle bekannten Zeitzonen, historische und aktuelle, aus, und es ist unwahrscheinlich, dass neue Zeitzonen entstehen, die nicht durch Heuristiken erfasst werden können – obwohl es etwas wahrscheinlicher ist, dass neue Regeln auftreten, die nicht von der *aktuellen* Generation von Heuristiken erfasst werden; in diesem Fall wären Fehlerbehebungen erforderlich, um die geänderte Situation zu berücksichtigen.
  4. Der Nutzen der dst()-Methode (und tatsächlich des isdst-Parameters in TZif) ist von vornherein fraglich, da fast alle nützlichen Informationen in den Methoden utcoffset() und tzname() enthalten sind, die nicht denselben Problemen unterliegen.

Kurz gesagt, die Pflege eines benutzerdefinierten tzdb-Compilers oder eines kompilierten Pakets verursacht Wartungsaufwand sowohl für das CPython-Entwicklungsteam als auch für Systemadministratoren, und sein Hauptvorteil besteht darin, einen hypothetischen Fehler zu beheben, der im Falle seines Auftretens wahrscheinlich nur geringe reale Auswirkungen hätte.

Standardmäßige Einbeziehung von tzdata in die Standardbibliothek

Obwohl PEP 453, der den ensurepip-Mechanismus in CPython eingeführt hat, eine praktische Vorlage für ein in PyPI gepflegtes Standardbibliotheksmodul bietet, ist ein potenziell ähnlicher ensuretzdata-Mechanismus etwas weniger notwendig und wäre so kompliziert, dass er als außerhalb des Geltungsbereichs dieser PEP betrachtet wird.

Da das Modul zoneinfo darauf ausgelegt ist, die System-Zeitzonendaten zu verwenden, wo immer dies möglich ist, ist das Paket tzdata auf Systemen, die Zeitzonendaten bereitstellen, unnötig (und möglicherweise unerwünscht). Daher erscheint es nicht kritisch, tzdata mit CPython auszuliefern.

Es ist auch noch nicht klar, wie diese hybriden Standardbibliotheks-/PyPI-Module aktualisiert werden sollen (außer pip, das über einen natürlichen Mechanismus für Updates und Benachrichtigungen verfügt), und da dies für den Betrieb des Moduls nicht kritisch ist, erscheint es ratsam, einen solchen Vorschlag aufzuschieben.

Unterstützung für Schaltsekunden

Neben Zeitzonen-Offset- und Namensregeln bietet die IANA-Zeitzonendatenbank auch eine Quelle für Schaltsekundendaten. Dies wird als außerhalb des Geltungsbereichs betrachtet, da datetime.datetime derzeit keine Unterstützung für Schaltsekunden hat und die Frage der Schaltsekundendaten aufgeschoben werden kann, bis eine Unterstützung für Schaltsekunden hinzugefügt wird.

Das „First-Party“-Paket tzdata sollte die Schaltsekundendaten liefern, auch wenn sie nicht vom Modul zoneinfo verwendet werden.

Verwendung einer pytz-ähnlichen Schnittstelle

Eine pytz-ähnliche ([18]) Schnittstelle wurde in PEP 431 vorgeschlagen, aber letztendlich wegen mangelnder Unterstützung für mehrdeutige Datums- und Zeitangaben zurückgezogen/abgelehnt. PEP 495 fügte das Attribut fold hinzu, um dieses Problem zu lösen, aber fold macht die pytz-eigenen tzinfo-Klassen überflüssig, und daher ist eine pytz-ähnliche Schnittstelle nicht mehr notwendig. [2]

Der zoneinfo-Ansatz basiert stärker auf dateutil.tz, das die Unterstützung für fold (einschließlich eines Backports für ältere Versionen) kurz vor der Veröffentlichung von Python 3.6 implementierte.

Windows-Unterstützung über die ICU-API von Microsoft

Windows liefert die Zeitzonendatenbank nicht als TZif-Dateien, aber seit dem Creators Update 2017 von Windows 10 stellt Microsoft eine API für die Interaktion mit dem International Components for Unicode (ICU)-Projekt [13] [14] bereit, die eine API zum Zugriff auf Zeitzonendaten enthält – bezogen aus der IANA-Zeitzonendatenbank. [15]

Die Bereitstellung von Bindungen hierfür würde es uns ermöglichen, Windows „out of the box“ zu unterstützen, ohne das Paket tzdata installieren zu müssen. Leider bieten die von Windows bereitgestellten C-Header keinen Zugriff auf die zugrunde liegenden Zeitzonendaten – nur eine API zur Abfrage des Systems nach Übergangs- und Offset-Informationen ist verfügbar. Dies würde die Semantik jeder ICU-basierten Implementierung einschränken, auf eine Weise, die möglicherweise nicht mit einer Nicht-ICU-basierten Implementierung kompatibel ist – insbesondere im Hinblick auf das Verhalten des Caches.

Da es den Anschein hat, dass ICU nicht einfach als zusätzliche Datenquelle für ZoneInfo-Objekte verwendet werden kann, betrachtet diese PEP die ICU-Unterstützung als außerhalb des Geltungsbereichs und wahrscheinlich besser durch eine Drittanbieterbibliothek unterstützt.

Alternative Konfigurationen von Umgebungsvariablen

Diese PEP schlägt die Verwendung einer einzigen Umgebungsvariable vor: PYTHONTZPATH. Dies basiert auf der Annahme, dass die Mehrheit der Benutzer, die den Zeitzonenpfad manipulieren möchten, ihn vollständig ersetzen möchten („Ich weiß genau, wo meine Zeitzonendaten sind“), und andere Anwendungsfälle wie das Voranstellen des bestehenden Suchpfads weniger verbreitet wären.

Es gibt mehrere andere betrachtete und abgelehnte Schemata

  1. Aufteilung von PYTHON_TZPATH in zwei Umgebungsvariablen: DEFAULT_PYTHONTZPATH und PYTHONTZPATH, wobei PYTHONTZPATH Werte zum Anhängen (oder Voranstellen) des Standard-Zeitzonenpfads enthält und DEFAULT_PYTHONTZPATH den Standard-Zeitzonenpfad *ersetzen* würde. Dies wurde abgelehnt, da es wahrscheinlich zu Verwirrung bei den Benutzern führen würde, wenn der Hauptanwendungsfall der Ersatz statt der Ergänzung ist.
  2. Hinzufügen von PYTHONTZPATH_PREPEND, PYTHONTZPATH_APPEND oder beidem, sodass Benutzer den Suchpfad an beiden Enden erweitern können, ohne versuchen zu müssen, den Standard-Zeitzonenpfad zu ermitteln. Dies wurde abgelehnt, da es wahrscheinlich unnötig ist und leicht in zukünftigen Updates abwärtskompatibel hinzugefügt werden könnte, wenn eine solche Funktion stark nachgefragt wird.
  3. Nur die Variable PYTHONTZPATH verwenden, aber einen benutzerdefinierten Sonderwert bereitstellen, der den Standard-Zeitzonenpfad darstellt, z. B. <<DEFAULT_TZPATH>>, sodass Benutzer den Zeitzonenpfad mit z. B. PYTHONTZPATH=<<DEFAULT_TZPATH>>:/my/path erweitern könnten, um /my/path an das Ende des Zeitzonenpfads anzuhängen.

    Ein Vorteil dieses Schemas wäre, dass es einen natürlichen Erweiterungspunkt für die Angabe nicht dateibasierter Elemente im Suchpfad hinzufügt, wie z. B. die Änderung der Priorität von tzdata, falls vorhanden, oder wenn in Zukunft eine native Unterstützung für TZDIST zur Bibliothek hinzugefügt würde.

    Dies wurde hauptsächlich abgelehnt, da diese Art von speziellen Werten normalerweise nicht in PATH-ähnlichen Variablen vorkommt und der einzige derzeit vorgeschlagene Anwendungsfall ein Platzhalter für den Standard-TZPATH ist, der durch Ausführen eines Python-Programms zur Abfrage des Standardwerts ermittelt werden kann. Ein zusätzlicher Faktor bei der Ablehnung ist, dass, da PYTHONTZPATH nur absolute Pfade akzeptiert, jede Zeichenkette, die kein gültiger absoluter Pfad ist, implizit für die zukünftige Verwendung reserviert ist. Daher wäre es möglich, diese speziellen Werte bei Bedarf abwärtskompatibel in zukünftigen Versionen der Bibliothek einzuführen.

Verwendung des datetime-Moduls

Eine mögliche Idee wäre, ZoneInfo dem Modul datetime hinzuzufügen, anstatt ihm ein eigenes separates Modul zu geben. Diese PEP bevorzugt die Verwendung eines separaten zoneinfo-Moduls, obwohl auch ein verschachteltes datetime.zoneinfo-Modul in Betracht gezogen wurde.

Argumente gegen die direkte Aufnahme von ZoneInfo in datetime

Das Modul datetime ist bereits etwas überladen, da es viele Klassen mit relativ komplexem Verhalten enthält – datetime.datetime, datetime.date, datetime.time, datetime.timedelta, datetime.timezone und datetime.tzinfo. Die Implementierung und Dokumentation des Moduls sind bereits recht kompliziert, und es ist wahrscheinlich vorteilhaft, das Problem nicht noch zu vergrößern, wenn es vermieden werden kann.

Die Klasse ZoneInfo unterscheidet sich auch in gewisser Weise von allen anderen Klassen, die von datetime bereitgestellt werden; die anderen Klassen sind alle als schlanke, einfache Datentypen konzipiert, während die Klasse ZoneInfo komplexer ist: Sie ist ein Parser für ein bestimmtes Format (TZif), eine Darstellung der in diesem Format gespeicherten Informationen und ein Mechanismus zur Abfrage der Informationen an bekannten Systemorten.

Schließlich ist es zwar wahr, dass jemand, der das Modul zoneinfo benötigt, auch das Modul datetime benötigt, aber das Gegenteil ist nicht unbedingt der Fall: Viele Leute werden datetime ohne zoneinfo verwenden wollen. In Anbetracht der Tatsache, dass zoneinfo wahrscheinlich zusätzliche, möglicherweise schwergewichtigere Standardbibliotheksmodule nach sich ziehen wird, wäre es vorzuziehen, die beiden separat importieren zu lassen – insbesondere wenn zukünftige „Tree Shaking“-Distributionen für Python in Frage kommen. [9]

Letztendlich ist es sinnvoll, zoneinfo als separates Modul mit einer eigenen Dokumentationsseite beizubehalten, anstatt seine Klassen und Funktionen direkt in datetime zu integrieren.

Verwendung von datetime.zoneinfo anstelle von zoneinfo

Eine besser akzeptable Konfiguration könnte darin bestehen, zoneinfo als Modul unter datetime zu verschachteln, als datetime.zoneinfo.

Argumente dafür

  1. Es gruppiert zoneinfo übersichtlich zusammen mit datetime
  2. Die Klasse timezone befindet sich bereits in datetime, und es mag seltsam erscheinen, dass einige Zeitzonen in datetime und andere in einem Top-Level-Modul sind.
  3. Wie bereits erwähnt, erfordert der Import von zoneinfo zwangsläufig den Import von datetime, daher ist es keine Zumutung, den Import des Elternmoduls zu verlangen.

Argumente dagegen

  1. Um zu vermeiden, dass alle datetime-Benutzer zoneinfo importieren müssen, müsste das Modul zoneinfo verzögert importiert werden, was bedeutet, dass Endbenutzer explizit datetime.zoneinfo importieren müssten (im Gegensatz zum Import von datetime und dem Zugriff auf das Attribut zoneinfo des Moduls). So funktioniert dateutil (alle Untermodule werden verzögert importiert), und es ist eine ständige Quelle der Verwirrung für Endbenutzer.

    Diese verwirrende Anforderung für Endbenutzer kann durch ein Modul-Level __getattr__ und __dir__ gemäß PEP 562 vermieden werden, aber dies würde die Implementierung des Moduls datetime etwas komplizierter machen. Diese Art von Verhalten in Modulen oder Klassen neigt dazu, statische Analysewerkzeuge zu verwirren, was für eine so weit verbreitete und kritische Bibliothek wie datetime unerwünscht sein könnte.

  2. Das Verschachteln der Implementierung unter datetime würde wahrscheinlich erfordern, dass datetime von einem einzelnen Dateimodul (datetime.py) zu einem Verzeichnis mit einer __init__.py umorganisiert wird. Dies ist eine geringfügige Angelegenheit, aber die Struktur des Moduls datetime ist seit vielen Jahren stabil, und es wäre wünschenswert, Änderungen zu vermeiden, wenn möglich.

    Dieses Problem *könnte* durch die Implementierung von zoneinfo als _zoneinfo.py und dessen Import als zoneinfo aus datetime behoben werden, aber dies erscheint aus ästhetischer Sicht oder unter dem Gesichtspunkt der Codeorganisation nicht wünschenswert, und es würde die Verschachtelungsversion ausschließen, bei der Endbenutzer explizit datetime.zoneinfo importieren müssen.

Diese PEP vertritt den Standpunkt, dass es im Großen und Ganzen am besten wäre, ein separates Top-Level-Modul zoneinfo zu verwenden, da die Vorteile der Verschachtelung nicht so groß sind, dass sie die praktischen Implementierungsaspekte überwiegen.

Fußnoten

[a]
Die Behauptung, dass die überwiegende Mehrheit der Benutzer nur wenige Arten von Zeitzonen wünscht, basiert auf anekdotischen Eindrücken und nicht auf wissenschaftlichen Erkenntnissen. Als einzelner Datenpunkt bietet dateutil viele Zeitzonentypen, aber der Benutzersupport konzentriert sich hauptsächlich auf diese drei Typen.
[b]
Die Aussage, dass identisch konstruierte ZoneInfo-Objekte identische Objekte sein sollten, kann verletzt werden, wenn der Benutzer den Zeitzonen-Cache bewusst löscht.
[c]
Der Hash-Wert für ein gegebenes datetime wird beim ersten Berechnen zwischengespeichert, sodass wir uns keine Sorgen über das möglicherweise ernstere Problem machen müssen, dass sich der Hash eines gegebenen datetime-Objekts während seiner Lebensdauer ändern würde.
[d] (1, 2, 3)
Der Begriff „First Party“ unterscheidet sich hier von „Third Party“ dadurch, dass er, obwohl er über PyPI vertrieben wird und derzeit nicht standardmäßig in Python enthalten ist, als offizielles Unterprojekt von CPython betrachtet wird und nicht als „gesegnetes“ Drittanbieterpaket.

Referenzen

Andere Zeitzonenimplementierungen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0615.rst

Zuletzt geändert: 2025-02-01 07:28:42 GMT