溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Django時區(qū)的示例分析

發(fā)布時間:2021-08-25 09:10:53 來源:億速云 閱讀:136 作者:小新 欄目:開發(fā)技術

這篇文章主要為大家展示了“Django時區(qū)的示例分析”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“Django時區(qū)的示例分析”這篇文章吧。

引言

相信使用Django的各位開發(fā)者在存儲時間的時候經常會遇到這樣子的錯誤:

RuntimeWarning: DateTimeField received a naive datetime while time zone support is active.

這個錯誤到底是什么意思呢?什么是naive datetime object?什么又是aware datetime object?

在Django配置中如果將settings.TIME_ZONE設置為中國時區(qū)(Asia/Shanghai),為什么以下時間函數會得到時間相差較大的結果?

# settings.py
TIME_ZONE = 'Asia/Shanghai'

# python manage.py shell
>>> from datetime import datetime
>>> datetime.now()
datetime.datetime(2016, 12, 7, 12, 41, 22, 729326)
>>> from django.utils import timezone
>>> timezone.now()
datetime.datetime(2016, 12, 7, 4, 41, 36, 685921, tzinfo=<UTC>)

接下來筆者將詳細揭秘在Django中關于時區(qū)的種種內幕,如有不對,敬請指教。

準備

UTC與DST

UTC可以視為一個世界統一的時間,以原子時為基礎,其他時區(qū)的時間都是在這個基礎上增加或減少的,比如中國的時區(qū)就為UTC+8。

DST(夏時制)則是為了充分利用夏天日照長的特點,充分利用光照節(jié)約能源而人為調整時間的一種機制。通過在夏天將時間向前加一小時,使人們早睡早起節(jié)約能源。雖然很多西方國家都采用了DST,但是中國不采用DST。(資料來源:DST 百度百科)

naive datetime object vs aware datetime object

當使用datetime.now()得到一個datetime對象的時候,此時該datetime對象沒有任何關于時區(qū)的信息,即datetime對象的tzinfo屬性為None(tzinfo屬性被用于存儲datetime object關于時區(qū)的信息),該datetime對象就被稱為naive datetime object。

>>> import datetime
>>> naive = datetime.datetime.now()
>>> naive.tzinfo
>>>

既然naive datetime object沒有關于時區(qū)的信息存儲,相對的aware datetime object就是指存儲了時區(qū)信息的datetime object。
在使用now函數的時候,可以指定時區(qū),但該時區(qū)參數必須是datetime.tzinfo的子類。(tzinfo是一個抽象類,必須有一個具體的子類才能使用,筆者在這里使用了pytz.utc,在Django中的timezone源碼中也實現了一個UTC類以防沒有pytz庫的時候timezone功能能正常使用)

>>> import datetime
>>> import pytz
>>> aware = datetime.datetime.now(pytz.utc)
>>> aware
datetime.datetime(2016, 12, 7, 8, 32, 7, 864077, tzinfo=<UTC>)
>>> aware.tzinfo
<UTC>

 在Django中提供了幾個簡單的函數如is_aware, is_naive, make_aware和make_naive用于辨別和轉換naive datetime object和aware datetime object。

datetime.now簡析

在調用datetime.now()的時候時間是如何返回的呢?在官方文檔里只是簡單地說明了now函數返回當前的具體時間,以及可以?指定時區(qū)參數,并沒有具體的說明now函數的實現。

classmethod datetime.now(tz=None)
Return the current local date and time. If optional argument tz is None or not specified, this is like today(), but, if possible, supplies more precision than can be gotten from going through a time.time() timestamp (for example, this may be possible on platforms supplying the C gettimeofday() function).

If tz is not None, it must be an instance of a tzinfo subclass, and the current date and time are converted to tz's time zone. In this case the result is equivalent to tz.fromutc(datetime.utcnow().replace(tzinfo=tz)). See also today(), utcnow().

OK,那么接下來直接從datetime.now()的源碼入手吧。

@classmethod
 def now(cls, tz=None):
  "Construct a datetime from time.time() and optional time zone info."
  t = _time.time()
  return cls.fromtimestamp(t, tz)

大家可以看到datetime.now函數通過time.time()返回了一個時間戳,然后調用了datetime.fromtimestamp()將一個時間戳轉化成一個datetime對象。

那么,不同時區(qū)的時間戳會不會不一樣呢?不,時間戳不會隨著時區(qū)的改變而改變,時間戳是唯一的,被定義為格林威治時間1970年01月01日00時00分00秒(北京時間1970年01月01日08時00分00秒)起至現在的總秒數。

datetime.fromtimestamp

既然時間戳不會隨時區(qū)改變,那么在fromtimestamp中應該對時間戳的轉換做了時區(qū)的處理。

直接上源碼:

 @classmethod
 def _fromtimestamp(cls, t, utc, tz):
  """Construct a datetime from a POSIX timestamp (like time.time()).

  A timezone info object may be passed in as well.
  """
  frac, t = _math.modf(t)
  us = round(frac * 1e6)
  if us >= 1000000:
   t += 1
   us -= 1000000
  elif us < 0:
   t -= 1
   us += 1000000

  converter = _time.gmtime if utc else _time.localtime
  y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)
  ss = min(ss, 59) # clamp out leap seconds if the platform has them
  return cls(y, m, d, hh, mm, ss, us, tz)

 @classmethod
 def fromtimestamp(cls, t, tz=None):
  """Construct a datetime from a POSIX timestamp (like time.time()).

  A timezone info object may be passed in as well.
  """
  _check_tzinfo_arg(tz)

  result = cls._fromtimestamp(t, tz is not None, tz)
  if tz is not None:
   result = tz.fromutc(result)
  return result

當直接調用datetime.now()的時候,并沒有傳進tz的參數,因此_fromtimestamp中的utc參數為False,所以converter被賦值為time.localtime函數。

time.localtime

localtime函數的使用只需要知道它返回一個九元組表示當前的時區(qū)的具體時間即可:

def localtime(seconds=None): # real signature unknown; restored from __doc__
 """
 localtime([seconds]) -> (tm_year,tm_mon,tm_mday,tm_hour,tm_min,
        tm_sec,tm_wday,tm_yday,tm_isdst)

 Convert seconds since the Epoch to a time tuple expressing local time.
 When 'seconds' is not passed in, convert the current time instead.
 """
 pass

筆者覺得更需要注意的是什么因素影響了time.localtime返回的時區(qū)時間,那么,就需要談及time.tzset函數了。

在Python官方文檔中關于time.tzset函數解釋如下:

time.tzset()

Resets the time conversion rules used by the library routines. The environment variable TZ specifies how this is done.

Availability: Unix.

Note Although in many cases, changing the TZ environment variable may affect the output of functions like localtime() without calling tzset(), this behavior should not be relied on.
The TZ environment variable should contain no whitespace.

可以看到,一個名為TZ的環(huán)境變量的設置會影響localtime的時區(qū)時間的返回。(有興趣的同學可以去在Unix下執(zhí)行man tzset,就知道TZ變量是如何影響localtime了)

最后,筆者給出一些測試的例子,由于獲取的時間戳不隨時間改變,因此直接調用fromtimestamp即可:

>>> from datetime import datetime
>>> from time import time, tzset
>>> china = datetime.fromtimestamp(time())
>>> import os
>>> os.environ['TZ'] = 'UTC'
>>> tzset()
>>> utc = datetime.fromtimestamp(time())
>>> china
datetime.datetime(2016, 12, 7, 16, 3, 34, 453664)
>>> utc
datetime.datetime(2016, 12, 7, 8, 4, 30, 108349)

以及直接調用localtime的例子:

>>> from time import time, localtime, tzset
>>> import os
>>> china = localtime()
>>> china
time.struct_time(tm_year=2016, tm_mon=12, tm_mday=7, tm_hour=16, tm_min=7, tm_sec=5, tm_wday=2, tm_yday=342, tm_isdst=0)
>>> os.environ['TZ'] = 'UTC'
>>> tzset()
>>> utc = localtime()
>>> utc
time.struct_time(tm_year=2016, tm_mon=12, tm_mday=7, tm_hour=8, tm_min=7, tm_sec=34, tm_wday=2, tm_yday=342, tm_isdst=0)

(提前劇透:TZ這一個環(huán)境變量在Django的時區(qū)中發(fā)揮了重大的作用)

Django TimeZone

timezone.now() vs datetime.now()

筆者在前面花費了大量的篇幅來講datetime.now函數的原理,并且提及了TZ這一個環(huán)境變量,這是因為在Django導入settings的時候也設置了TZ環(huán)境變量。

當執(zhí)行以下語句的時候:

from django.conf import settings

毫無疑問,首先會訪問django.conf.__init__.py文件。

在這里settings是一個lazy object,但是這不是本章的重點,只需要知道當訪問settings的時候,真正實例化的是以下這一個Settings類。

class Settings(BaseSettings):
 def __init__(self, settings_module):
  # update this dict from global settings (but only for ALL_CAPS settings)
  for setting in dir(global_settings):
   if setting.isupper():
    setattr(self, setting, getattr(global_settings, setting))

  # store the settings module in case someone later cares
  self.SETTINGS_MODULE = settings_module

  mod = importlib.import_module(self.SETTINGS_MODULE)

  tuple_settings = (
   "INSTALLED_APPS",
   "TEMPLATE_DIRS",
   "LOCALE_PATHS",
  )
  self._explicit_settings = set()
  for setting in dir(mod):
   if setting.isupper():
    setting_value = getattr(mod, setting)

    if (setting in tuple_settings and
      not isinstance(setting_value, (list, tuple))):
     raise ImproperlyConfigured("The %s setting must be a list or a tuple. " % setting)
    setattr(self, setting, setting_value)
    self._explicit_settings.add(setting)

  if not self.SECRET_KEY:
   raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")

  if hasattr(time, 'tzset') and self.TIME_ZONE:
   # When we can, attempt to validate the timezone. If we can't find
   # this file, no check happens and it's harmless.
   zoneinfo_root = '/usr/share/zoneinfo'
   if (os.path.exists(zoneinfo_root) and not
     os.path.exists(os.path.join(zoneinfo_root, *(self.TIME_ZONE.split('/'))))):
    raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
   # Move the time zone info into os.environ. See ticket #2315 for why
   # we don't do this unconditionally (breaks Windows).
   os.environ['TZ'] = self.TIME_ZONE
   time.tzset()

 def is_overridden(self, setting):
  return setting in self._explicit_settings

 def __repr__(self):
  return '<%(cls)s "%(settings_module)s">' % {
   'cls': self.__class__.__name__,
   'settings_module': self.SETTINGS_MODULE,
  }

在該類的初始化函數的最后,可以看到當USE_TZ=True的時候(即開啟Django的時區(qū)功能),設置了TZ變量為settings.TIME_ZONE。

OK,知道了TZ變量被設置為TIME_ZONE之后,就能解釋一些很奇怪的事情了。

比如,新建一個Django項目,保留默認的時區(qū)設置,并啟動django shell:

# settings.py
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

# python3 manage.py shell
>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2016, 12, 7, 9, 19, 34, 741124)
>>> datetime.datetime.utcnow()
datetime.datetime(2016, 12, 7, 9, 19, 45, 753843)

默認的Python Shell通過datetime.now返回的應該是當地時間,在這里即中國時區(qū),但是當settings.TIME_ZONE設置為UTC的時候,通過datetime.now返回的就是UTC時間。

可以試試將TIME_ZONE設置成中國時區(qū):

# settings.py
TIME_ZONE = 'Asia/Shanghai'

# python3 manage.py shell
>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2016, 12, 7, 17, 22, 21, 172761)
>>> datetime.datetime.utcnow()
datetime.datetime(2016, 12, 7, 9, 22, 26, 373080)

此時datetime.now返回的就是中國時區(qū)了。

當使用timezone.now函數的時候,情況則不一樣,在支持時區(qū)功能的時候,該函數返回的是一個帶有UTC時區(qū)信息的aware datetime obeject,即它不受TIME_ZONE變量的影響。

直接看它的源碼實現:

def now():
 """
 Returns an aware or naive datetime.datetime, depending on settings.USE_TZ.
 """
 if settings.USE_TZ:
  # timeit shows that datetime.now(tz=utc) is 24% slower
  return datetime.utcnow().replace(tzinfo=utc)
 else:
  return datetime.now()

不支持時區(qū)功能,就返回一個受TIME_ZONE影響的naive datetime object。

實踐場景

假設現在有這樣一個場景,前端通過固定格式提交一個時間字符串供后端的form驗證,后端解析得到datetime object之后再通過django orm存儲到DatetimeField里面。

Form.DateTimeField

在django關于timezone的官方文檔中,已經說明了經過form.DatetimeField返回的在cleaned_data中的時間都是當前時區(qū)的aware datetime object。

Time zone aware input in forms&para;

When you enable time zone support, Django interprets datetimes entered in forms in the current time zone and returns aware datetime objects in cleaned_data.

If the current time zone raises an exception for datetimes that don't exist or are ambiguous because they fall in a DST transition (the timezones provided by pytz do this), such datetimes will be reported as invalid values.

Models.DatetimeField

在存儲時間到MySQL的時候,首先需要知道在Models里面的DatetimeField通過ORM映射到MySQL的時候是什么類型。
筆者首先建立了一個Model作為測試:

# models.py
class Time(models.Model):

 now = models.DateTimeField()

# MySQL Tables Schema
+-------+-------------+------+-----+---------+----------------+
| Field | Type  | Null | Key | Default | Extra   |
+-------+-------------+------+-----+---------+----------------+
| id | int(11)  | NO | PRI | NULL | auto_increment |
| now | datetime(6) | NO |  | NULL |    |
+-------+-------------+------+-----+---------+----------------+

可以看到,在MySQL中是通過datetime類型存儲Django ORM中的DateTimeField類型,其中datetime類型是不受MySQL的時區(qū)設置影響,與timestamp類型不同。

關于datetime和timestamp類型可以參考這篇文章。

因此,如果筆者關閉了時區(qū)功能,卻向MySQL中存儲了一個aware datetime object,就會得到以下報錯:

"ValueError: MySQL backend does not support timezone-aware datetimes. "

關于對時區(qū)在業(yè)務開發(fā)中的一些看法

后端應該在數據庫統一存儲UTC時間并返回UTC時間給前端,前端在發(fā)送時間和接收時間的時候要把時間分別從當前時區(qū)轉換成UTC發(fā)送給后端,以及接收后端的UTC時間轉換成當地時區(qū)。

以上是“Django時區(qū)的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI