Tidszoner¶
Översikt¶
När stöd för tidszoner är aktiverat lagrar Django datetime-information i UTC i databasen, använder tidszonmedvetna datetime-objekt internt och konverterar dem till slutanvändarens tidszon i formulär. Mallar kommer att använda standardtidszonen, men detta kan uppdateras till slutanvändarens tidszon genom användning av filter och taggar.
Detta är praktiskt om dina användare bor i mer än en tidszon och du vill visa datatidsinformation enligt varje användares väggklocka.
Även om din webbplats bara är tillgänglig i en enda tidszon är det ändå bra att lagra data i UTC i databasen. Det främsta skälet är sommartid (DST). Många länder har ett system med sommartid, där klockorna ställs fram på våren och tillbaka på hösten. Om du arbetar med lokal tid kommer du sannolikt att stöta på fel två gånger om året, när övergångarna sker. Det här spelar förmodligen ingen roll för din blogg, men det är ett problem om du överfakturerar eller underfakturerar dina kunder med en timme, två gånger om året, varje år. Lösningen på det här problemet är att använda UTC i koden och endast använda lokal tid när du interagerar med slutanvändare.
Stöd för tidszoner är aktiverat som standard. För att inaktivera det, ange USE_TZ = False
i din inställningsfil.
Tidszonsstöd använder zoneinfo
, som är en del av Pythons standardbibliotek från Python 3.9.
Om du brottas med ett särskilt problem kan du börja med :ref:time zone FAQ <time-zones-faq>
.
Koncept¶
Naiva och medvetna datetime-objekt¶
Pythons datetime.datetime
-objekt har ett tzinfo
-attribut som kan användas för att lagra tidszoninformation, representerad som en instans av en underklass av datetime.tzinfo
. När detta attribut är inställt och beskriver en förskjutning, är ett datetime-objekt medvetet. Annars är det naive.
Du kan använda is_aware()
och is_naive()
för att avgöra om datatider är medvetna eller naiva.
När stödet för tidszoner är avaktiverat använder Django naiva datetime-objekt i lokal tid. Detta är tillräckligt för många användningsfall. I det här läget skulle du skriva: för att få aktuell tid:
import datetime
now = datetime.datetime.now()
När stöd för tidszoner är aktiverat (USE_TZ=True
) använder Django tidszonsmedvetna datetime-objekt. Om din kod skapar datetime-objekt bör de också vara medvetna. I det här läget blir exemplet ovan:
from django.utils import timezone
now = timezone.now()
Varning
Att hantera medvetna datetime-objekt är inte alltid intuitivt. Till exempel fungerar inte tzinfo
-argumentet i standard datetime-konstruktören tillförlitligt för tidszoner med DST. Att använda UTC är i allmänhet säkert; om du använder andra tidszoner bör du granska zoneinfo
-dokumentationen noggrant.
Observera
Pythons datetime.time
-objekt har också ett tzinfo
-attribut, och PostgreSQL har en matchande tid med tidszon
-typ. Men som PostgreSQL: s dokument uttrycker det, den här typen ”uppvisar egenskaper som leder till tvivelaktig användbarhet”.
Django stöder endast naiva tidsobjekt och kommer att ge upphov till ett undantag om du försöker spara ett medvetet tidsobjekt, eftersom en tidszon för en tid utan något associerat datum inte är meningsfullt.
Tolkning av naiva datetime-objekt¶
När USE_TZ
är True
, accepterar Django fortfarande naiva datetime-objekt, för att bevara bakåtkompatibilitet. När databaslagret tar emot ett sådant försöker det göra det medvetet genom att tolka det i :ref:default time zone <default-current-time-zone>
och ger en varning.
Under DST-övergångar finns det tyvärr vissa datatider som inte existerar eller som är tvetydiga. Det är därför du alltid bör skapa medvetna datetime-objekt när stöd för tidszoner är aktiverat. (Se :mod:Using ZoneInfo section of the zoneinfo docs <zoneinfo>` för exempel på användning av attributet ``fold
för att ange den förskjutning som ska gälla för en datatid under en övergång till sommartid)
I praktiken är detta sällan ett problem. Django ger dig medvetna datetime-objekt i modellerna och formulären, och oftast skapas nya datetime-objekt från befintliga genom timedelta
-aritmetik. Den enda datatid som ofta skapas i applikationskoden är den aktuella tiden, och timezone.now()
gör automatiskt rätt sak.
Standardtidszon och aktuell tidszon¶
Standardtidszonen är den tidszon som definieras av inställningen TIME_ZONE
.
Den aktuella tidszonen är den tidszon som används för rendering.
Du bör ställa in den aktuella tidszonen till slutanvändarens faktiska tidszon med activate()
. Annars används standardtidszonen.
Observera
Som förklaras i dokumentationen av TIME_ZONE
, ställer Django in miljövariabler så att dess process körs i standardtidszonen. Detta sker oberoende av värdet på USE_TZ
och den aktuella tidszonen.
När USE_TZ
är True
, är detta användbart för att bevara bakåtkompatibilitet med applikationer som fortfarande förlitar sig på lokal tid. Men, :ref:som förklaras ovan <naive-datetime-objects>`, detta är inte helt tillförlitligt, och du bör alltid arbeta med medvetna datatider i UTC i din egen kod. Använd till exempel :meth:`~datetime.datetime.fromtimestamp` och sätt parametern ``tz
till utc
.
Välja aktuell tidszon¶
Den aktuella tidszonen är motsvarigheten till den aktuella locale för översättningar. Det finns dock ingen motsvarighet till HTTP-headern Accept-Language
som Django kan använda för att automatiskt bestämma användarens tidszon. Istället tillhandahåller Django funktioner för val av tidszon. Använd dem för att bygga den logik för val av tidszon som är vettig för dig.
De flesta webbplatser som bryr sig om tidszoner frågar användarna i vilken tidszon de bor och lagrar denna information i användarens profil. För anonyma användare använder de tidszonen för sin primära målgrupp eller UTC. zoneinfo.available_timezones()
ger en uppsättning tillgängliga tidszoner som du kan använda för att bygga en karta från sannolika platser till tidszoner.
Här är ett exempel som lagrar den aktuella tidszonen i sessionen. (Det hoppar över felhanteringen helt för enkelhetens skull)
Lägg till följande middleware i MIDDLEWARE
:
import zoneinfo
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = request.session.get("django_timezone")
if tzname:
timezone.activate(zoneinfo.ZoneInfo(tzname))
else:
timezone.deactivate()
return self.get_response(request)
Skapa en vy som kan ställa in aktuell tidszon:
from django.shortcuts import redirect, render
# Prepare a map of common locations to timezone choices you wish to offer.
common_timezones = {
"London": "Europe/London",
"Paris": "Europe/Paris",
"New York": "America/New_York",
}
def set_timezone(request):
if request.method == "POST":
request.session["django_timezone"] = request.POST["timezone"]
return redirect("/")
else:
return render(request, "template.html", {"timezones": common_timezones})
Inkludera ett formulär i template.html
som kommer att POST
till denna vy:
{% load tz %}
{% get_current_timezone as TIME_ZONE %}
<form action="{% url 'set_timezone' %}" method="POST">
{% csrf_token %}
<label for="timezone">Time zone:</label>
<select name="timezone">
{% for city, tz in timezones.items %}
<option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
{% endfor %}
</select>
<input type="submit" value="Set">
</form>
Tidszonmedveten inmatning i formulär¶
När du aktiverar stöd för tidszoner tolkar Django datatider som anges i formulär i aktuell tidszon och returnerar medvetna datatidsobjekt i cleaned_data
.
Konverterade datumtider som inte existerar eller som är tvetydiga eftersom de faller i en DST-övergång kommer att rapporteras som ogiltiga värden.
Tidszonmedveten utdata i mallar¶
När du aktiverar stöd för tidszoner konverterar Django medvetna datetime-objekt till aktuell tidszon när de återges i mallar. Detta beter sig väldigt mycket som format localization.
Varning
Django konverterar inte naiva datetime-objekt, eftersom de kan vara tvetydiga och eftersom din kod aldrig bör producera naiva datetimes när tidszonsstöd är aktiverat. Du kan dock tvinga fram konvertering med de mallfilter som beskrivs nedan.
Omvandling till lokal tid är inte alltid lämplig - du kanske genererar utdata för datorer snarare än för människor. Följande filter och taggar, som tillhandahålls av malltaggbiblioteket tz
, gör att du kan kontrollera tidszonskonverteringarna.
Mallfilter¶
Dessa filter accepterar både medvetna och naiva datatider. För konverteringsändamål antar de att naiva datatider är i standardtidszonen. De returnerar alltid medvetna datatider.
lokaltid
¶
Tvingar fram konvertering av ett enskilt värde till aktuell tidszon.
Till exempel:
{% load tz %}
{{ value|localtime }}
utc
¶
Tvingar fram konvertering av ett enskilt värde till UTC.
Till exempel:
{% load tz %}
{{ value|utc }}
tidszon
¶
Tvingar fram konvertering av ett enskilt värde till en godtycklig tidszon.
Argumentet måste vara en instans av en tzinfo
-underklass eller ett tidszonnamn.
Till exempel:
{% load tz %}
{{ value|timezone:"Europe/Paris" }}
Migrationsguide¶
Så här gör du för att migrera ett projekt som startades innan Django stödde tidszoner.
Databas¶
PostgreSQL¶
PostgreSQL-backend lagrar datatider som `` tidsstämpel med tidszon``. I praktiken betyder det att den konverterar datatider från anslutningens tidszon till UTC vid lagring och från UTC till anslutningens tidszon vid hämtning.
Som en följd av detta, om du använder PostgreSQL, kan du växla mellan USE_TZ = False
och USE_TZ = True
fritt. Databasanslutningens tidszon kommer att ställas in till DATABASE-TIME_ZONE
respektive UTC
, så att Django får korrekta datatider i alla fall. Du behöver inte utföra några datakonverteringar.
Övriga databaser¶
Andra backends lagrar datatider utan tidszoninformation. Om du byter från USE_TZ = False
till USE_TZ = True
, måste du konvertera dina data från lokal tid till UTC - vilket inte är deterministiskt om din lokala tid har DST.
Kod¶
Det första steget är att lägga till USE_TZ = True
i din inställningsfil. Vid denna punkt bör saker och ting mestadels fungera. Om du skapar naiva datetime-objekt i din kod, gör Django dem medvetna när det behövs.
Dessa konverteringar kan dock misslyckas vid DST-övergångar, vilket innebär att du inte får alla fördelar med tidszonsstöd ännu. Dessutom kommer du sannolikt att stöta på några problem eftersom det är omöjligt att jämföra en naiv datatid med en medveten datatid. Eftersom Django nu ger dig medvetna datatider kommer du att få undantag när du jämför en datatid som kommer från en modell eller ett formulär med en naiv datatid som du har skapat i din kod.
Så det andra steget är att omarbeta din kod varhelst du instansierar datetime-objekt för att göra dem medvetna. Detta kan göras stegvis. django.utils.timezone
definierar några praktiska hjälpmedel för kompatibilitetskod: now()
, is_aware()
, is_naive()
, make_aware()
, och make_naive()
.
Slutligen, för att hjälpa dig att hitta kod som behöver uppgraderas, ger Django en varning när du försöker spara en naiv datatid i databasen:
RuntimeWarning: DateTimeField ModelName.field_name received a naive
datetime (2012-01-01 00:00:00) while time zone support is active.
Under utvecklingsarbetet kan du omvandla sådana varningar till undantag och få en spårning genom att lägga till följande i din inställningsfil:
import warnings
warnings.filterwarnings(
"error",
r"DateTimeField .* received a naive datetime",
RuntimeWarning,
r"django\.db\.models\.fields",
)
Fixturer¶
Vid serialisering av en medveten datatid inkluderas UTC-offset, så här:
"2011-09-01T13:20:30+03:00"
Medan det för en naiv datatid inte är:
"2011-09-01T13:20:30"
För modeller med DateTimeField
gör denna skillnad det omöjligt att skriva en fixtur som fungerar både med och utan stöd för tidszoner.
Fixturer som genererats med USE_TZ = False
, eller före Django 1.4, använder det ”naiva” formatet. Om ditt projekt innehåller sådana fixturer kommer du att se RuntimeWarning
när du laddar dem efter att du har aktiverat stöd för tidszoner. För att bli av med varningarna måste du konvertera dina fixturer till ”aware”-formatet.
Du kan regenerera fixturer med loaddata
och sedan dumpdata
. Eller, om de är tillräckligt små, kan du redigera dem för att lägga till UTC-offset som matchar din TIME_ZONE
till varje serialiserad datatid.
FAQ¶
Setup¶
Jag behöver inte flera tidszoner. Ska jag aktivera stöd för tidszoner?
Ja, det stämmer. När stöd för tidszoner är aktiverat använder Django en mer exakt modell av lokal tid. Detta skyddar dig från subtila och icke reproducerbara buggar kring övergångar till sommartid (DST).
När du aktiverar stöd för tidszoner kommer du att stöta på vissa fel eftersom du använder naiva datatider där Django förväntar sig medvetna datatider. Sådana fel dyker upp när man kör tester. Du kommer snabbt att lära dig hur du undviker ogiltiga operationer.
Å andra sidan är buggar som orsakas av avsaknad av stöd för tidszoner mycket svårare att förebygga, diagnostisera och åtgärda. Allt som involverar schemalagda uppgifter eller datatidsaritmetik är en kandidat för subtila buggar som bara kommer att bita dig en eller två gånger om året.
Av dessa skäl är stöd för tidszoner aktiverat som standard i nya projekt, och du bör behålla det om du inte har en mycket god anledning att inte göra det.
Jag har aktiverat tidszonsstöd. Är jag säker?
Ja, kanske det. Du är bättre skyddad från DST-relaterade buggar, men du kan fortfarande skjuta dig själv i foten genom att slarvigt förvandla naiva datatider till medvetna datatider och vice versa.
Om din applikation ansluter till andra system - t.ex. om den frågar en webbtjänst - måste du se till att datatiderna är korrekt angivna. För att överföra datatider på ett säkert sätt bör deras representation inkludera UTC-offset, eller så bör deras värden vara i UTC (eller båda!).
Slutligen innehåller vårt kalendersystem intressanta undantagsfall. Till exempel kan man inte alltid subtrahera ett år direkt från ett givet datum:
>>> import datetime >>> def one_year_before(value): # Wrong example. ... return value.replace(year=value.year - 1) ... >>> one_year_before(datetime.datetime(2012, 3, 1, 10, 0)) datetime.datetime(2011, 3, 1, 10, 0) >>> one_year_before(datetime.datetime(2012, 2, 29, 10, 0)) Traceback (most recent call last): ... ValueError: day is out of range for month
För att implementera en sådan funktion korrekt måste du bestämma om 2012-02-29 minus ett år är 2011-02-28 eller 2011-03-01, vilket beror på dina affärskrav.
Hur interagerar jag med en databas som lagrar datatider i lokal tid?
Ställ in alternativet
TIME_ZONE
till lämplig tidszon för den här databasen i inställningenDATABASES
.Detta är användbart för att ansluta till en databas som inte stöder tidszoner och som inte hanteras av Django när
USE_TZ
ärTrue
.
Felsökning¶
Mitt program kraschar med
TypeError: can't compare offset-naive
and offset-aware datetimes
**– what’s wrong? **Låt oss återskapa detta fel genom att jämföra en naiv och en medveten datetime:
>>> from django.utils import timezone >>> aware = timezone.now() >>> naive = timezone.make_naive(aware) >>> naive == aware Traceback (most recent call last): ... TypeError: can't compare offset-naive and offset-aware datetimes
Om du stöter på detta fel är det troligt att din kod jämför dessa två saker:
en datatid som tillhandahålls av Django - till exempel ett värde som läses från ett formulär eller ett modellfält. Eftersom du aktiverade stöd för tidszoner är den medveten.
en datatid som genereras av din kod, vilket är naivt (annars skulle du inte läsa det här).
I allmänhet är den korrekta lösningen att ändra din kod så att den använder en medveten datetime istället.
Om du skriver en pluggbar applikation som förväntas fungera oberoende av värdet på
USE_TZ
, kan du tycka attdjango.utils.timezone.now()
är användbar. Denna funktion returnerar aktuellt datum och tid som en naiv datatid närUSE_TZ = False
och som en medveten datatid närUSE_TZ = True
. Du kan lägga till eller dra ifråndatetime.timedelta
efter behov.Jag ser massor av
RuntimeWarning: DateTimeField received a naive datetime
(YYYY-MM-DD HH:MM:SS)
while time zone support is active
**– är det dåligt? **När stöd för tidszoner är aktiverat förväntar sig databaslagret att endast medvetna datatider tas emot från din kod. Denna varning visas när den tar emot en naiv datetime. Detta indikerar att du inte har slutfört portningen av din kod för stöd för tidszoner. Se :ref:``migreringsguide <time-zones-migration-guide>` för tips om den här processen.
Under tiden, för bakåtkompatibilitet, anses datatiden vara i standardtidszonen, vilket i allmänhet är vad du förväntar dig.
now.date()
är igår! (eller imorgon)Om du alltid har använt naiva datetider tror du förmodligen att du kan konvertera en datetime till ett datum genom att anropa dess
date()
-metod. Du tror också att endate
är ungefär som endatetime
, förutom att den är mindre exakt.Inget av detta är sant i en miljö som är medveten om tidszoner:
>>> import datetime >>> import zoneinfo >>> paris_tz = zoneinfo.ZoneInfo("Europe/Paris") >>> new_york_tz = zoneinfo.ZoneInfo("America/New_York") >>> paris = datetime.datetime(2012, 3, 3, 1, 30, tzinfo=paris_tz) # This is the correct way to convert between time zones. >>> new_york = paris.astimezone(new_york_tz) >>> paris == new_york, paris.date() == new_york.date() (True, False) >>> paris - new_york, paris.date() - new_york.date() (datetime.timedelta(0), datetime.timedelta(1)) >>> paris datetime.datetime(2012, 3, 3, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris')) >>> new_york datetime.datetime(2012, 3, 2, 19, 30, tzinfo=zoneinfo.ZoneInfo(key='America/New_York'))
Som exemplet visar har samma datatid olika datum beroende på i vilken tidszon den representeras. Men det verkliga problemet är mer grundläggande.
En datatid representerar en punkt i tiden. Den är absolut: den är inte beroende av någonting. Tvärtom är ett datum ett kalendrerande begrepp. Det är en tidsperiod vars gränser beror på den tidszon i vilken datumet betraktas. Som du kan se är dessa två begrepp fundamentalt olika, och att konvertera en datetime till ett datum är inte en deterministisk operation.
Vad innebär detta i praktiken?
Generellt sett bör du undvika att konvertera en
datetime
tilldate
. Du kan t.ex. använda mallfiltretdate
för att bara visa datumdelen av en datatid. Det här filtret konverterar datatiden till aktuell tidszon innan den formateras, vilket säkerställer att resultaten visas korrekt.Om du verkligen behöver göra konverteringen själv måste du först se till att datatiden konverteras till rätt tidszon. Vanligtvis kommer detta att vara den aktuella tidszonen:
>>> from django.utils import timezone >>> timezone.activate(zoneinfo.ZoneInfo("Asia/Singapore")) # For this example, we set the time zone to Singapore, but here's how # you would obtain the current time zone in the general case. >>> current_tz = timezone.get_current_timezone() >>> local = paris.astimezone(current_tz) >>> local datetime.datetime(2012, 3, 3, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Singapore')) >>> local.date() datetime.date(2012, 3, 3)
** Jag får ett fel** ”``Är tidszondefinitioner för din databas installerade? ``”
Om du använder MySQL, se avsnittet Definitioner av tidszoner i MySQL Notes för instruktioner om hur du läser in tidszonsdefinitioner.
Användning¶
Jag har en sträng
"2012-02-21 10:28:45"
och jag vet att den är i"Europa/Helsingfors"
tidszonen. Hur förvandlar jag det till en medveten datatid?Här måste du skapa den nödvändiga
ZoneInfo
-instansen och koppla den till den naiva datatiden:>>> import zoneinfo >>> from django.utils.dateparse import parse_datetime >>> naive = parse_datetime("2012-02-21 10:28:45") >>> naive.replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki")) datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Europe/Helsinki'))
Hur får jag fram den lokala tiden i den aktuella tidszonen?
Den första frågan är om du verkligen behöver göra det?
Du bör bara använda lokal tid när du interagerar med människor, och mallskiktet tillhandahåller filter och taggar för att konvertera datatider till den tidszon du väljer.
Dessutom vet Python hur man jämför medvetna datatider och tar hänsyn till UTC-offset när det behövs. Det är mycket enklare (och möjligen snabbare) att skriva all din modell- och vykod i UTC. Så under de flesta omständigheter kommer datatiden i UTC som returneras av
django.utils.timezone.now()
att vara tillräcklig.Men om du verkligen vill ha den lokala tiden i den aktuella tidszonen kan du få den så här, för fullständighetens skull:
>>> from django.utils import timezone >>> timezone.localtime(timezone.now()) datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
I detta exempel är den aktuella tidszonen
"Europa/Paris"
.Hur kan jag se alla tillgängliga tidszoner?
zoneinfo.available_timezones()
ger en uppsättning av alla giltiga nycklar för IANA-tidszoner som är tillgängliga för ditt system. Se dokumentationen för användningsöverväganden.