# Django 3.2.11.dev 文档

# Time zones

## 概况

当启用了时区支持，Django 在数据库里以 UTC 存储日期信息，在内部使用时区感知日期对象，并且在模板和表单中转换为最终用户的时区。

如果用户居住在多个时区时，这会很方便。你要根据用户的时间来显示日期信息。

即便你的网站只在一个时区可用，在数据库中以 UTC 来存储数据是个好习惯。主要原因是夏令时。很多地方都有夏令时系统，时钟在春季向前调整，秋季向后调整。如果你以当地时间起居，那么时区转换时，每年都可能会遇到两次错误。也许这对你的博客没什么影响，但是每年两次，一次一小时多收或是少收用户的费用，这就是个问题。解决办法就是在代码里使用 UTC，并且在与最终用户交互时，使用当地时间。

Time zone support is disabled by default. To enable it, set `USE_TZ = True` in your settings file. By default, time zone support uses pytz, which is installed when you install Django; Django also supports the use of other time zone implementations like `zoneinfo` by passing `tzinfo` objects directly to functions in `django.utils.timezone`.

Changed in Django 3.2:

Support for non-`pytz` timezone implementations was added.

注解

方便起见，在执行 `django-admin startproject` 后创建默认的 `settings.py` 文件包含 `USE_TZ = True` 。

注解

还有一个有关 `USE_L10N` 的设置，用来控制 Django 是否激活本地格式化。查看 本地格式化 获取更多细节。

如果你在解决一个特定问题，从阅读 time zone FAQ 开始。

## 概念

### Naive 日期对象与 Aware 日期对象

Python 的 `datetime.datetime` 对象有一个 `tzinfo` 属性，它可以存储时区信息，表示为 `datetime.tzinfo` 子类的一个实例。当设置这个属性并描述了偏移量后，日期对象就是 aware 的，否则就是 naive 的。

你可以使用 `is_aware()` 和 `is_naive()` 来决定日期是 aware 还是 naive 的。

当关闭了时区支持，Django 会在本地时间里使用原生日期对象。这对很多用例来说足够了。在这个模式下，如果你想获取当前时间，你可以这么写：

    import datetime

    now = datetime.datetime.now()

当启用了时区支持 (`USE_TZ=True`) ，Django 使用 time-zone-aware 日期对象。如果你的代码创建了日期对象，她们应该也是 aware 的。在这个模式下，上面的例子变成：

    from django.utils import timezone

    now = timezone.now()

警告

处理 aware 日期对象并不一直都是直观的。比如，标准日期构造函数的``tzinfo`` 参数对于使用 DST 的时区并不可靠。使用 UTC 一般是安全的；如果你正在使用其他时区，你应该认真地看一下 pytz 文档。

注解

Python 的 `datetime.time` 对象还具有 `tzinfo` 属性，PostgreSQL 具有匹配 `带有时区时间` 的类型。但是，正如 PostgreSQL 所描述的，这个类型 "表现出导致问题的可用性问题"。

Django 只支持 naive 时间对象，如果打算保存 aware 时间对象会引发异常，因为没有关联日期的时区的时间是没有意义的。

### Naive 日期对象的说明

当 `USE_TZ` 为 `True` 时，Django 仍然接受 naive 日期对象，以保持向后兼容。当数据库层收到一个日期对象，会试着让日期对象在 default time zone 里进行解释将其变为 aware 日期对象，并发出警告。

不幸的是，在 DST 转换期间，一些日期时间不存在或不明确。在这些情况下，pytz 会引发一个异常。这就是为什么当启用时区支持的时候，需要始终创建 aware 时间对象的原因。

实际上，这种情况很罕见。Django 在模型和表单里为你提供了 aware 日期对象，并且在大部分时候，新的日期对象是通过 `timedelta` 算法从现有的对象创建的。常在应用代码里创建的日期时间是当前时间，`timezone.now()` 会自动完成这个操作。

### 默认时区和当前时区

默认时区 是通过 `TIME_ZONE` 定义的时区。

当前时区 是用来渲染的时区。

你应该使用 `activate()` 将当前时区设置为最终用户的实际时区。

注解

如 `TIME_ZONE` 文档描述，Django 设置了环境变量，因此它的进程在默认时区里运行。无论 `USE_TZ` 的值和当前时区如何，都会发生这种情况。

当 `USE_TZ` 设置为 `True` 时，这样有助于保持仍然需要依赖当地时间的应用程序的后端兼容性。然而，就像前面所说（as explained above），这样并不完全可靠，你应该始终在代码里使用 UTC 里的 aware 日期来工作。比如，使用 `fromtimestamp()` 并且将 `tz` 参数设置为 `utc` 。

### 选择当前时区

当前时区相当于转换 current locale 。但是，Django 没有可用于自动确定用户时区的 `Accept-Language` HTTP header 。相反，Django 提供了时区选择函数（ time zone selection functions ）。使用它们来建立对你有用的时区选择逻辑。

很多关心用户时区的站点会询问用户所在的时区，并且把这个信息保存在用户资料里。对于匿名用户，他们使用网站主要受众群体的时区或者 UTC。pytz 提供了帮助（ helpers ），例如每个国家时区的列表，这样你可以预先选择最有可能的选项。

这里有个在会话(session)里存储当前时区的例子。（为简单起见，它完全跳过了错误处理）

在 `MIDDLEWARE`: 里添加下面的中间件：

    import pytz

    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(pytz.timezone(tzname))
            else:
                timezone.deactivate()
            return self.get_response(request)

创建一个可以设置当前时区的视图：

    from django.shortcuts import redirect, render

    def set_timezone(request):
        if request.method == 'POST':
            request.session['django_timezone'] = request.POST['timezone']
            return redirect('/')
        else:
            return render(request, 'template.html', {'timezones': pytz.common_timezones})

在 `template.html` 包含了一个发送 `POST` 到视图的表单：

    {% 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 tz in timezones %}
            <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ tz }}</option>
            {% endfor %}
        </select>
        <input type="submit" value="Set">
    </form>

## 表单里的时区感知(aware)输入

当启用了时区支持，Django 会解释当前时区中以表格形式输入的日期时间，并且在 `cleaned_data` 中返回 aware 日期对象。

如果当前时区在夏令时转换（由 pytz 提供的时区）时因日期不存在或者不明确而引发了异常，此类日期时间将被报告为无效值。

## 模板中时区感知(aware)输出

当启用了时区支持，Django 会在模板中渲染 aware 日期时间时，将其转换为当前时区。这非常类似于本地化格式（ format localization ）。

警告

Django 不会转换 naive 日期对象，因为它们是不确定的，而且当开启了时区支持后，代码里绝不能生成 naive 日期。但是，你可以使用下面描述的模板过滤器那样强制转换。

转换为本地时间并不总是合适的——你或许是为计算机而不是为人类生成输出。下面的过滤器和标签，由 `tz` 模板标签库支持，允许你控制时区转换。

### 模板标签

#### `localtime`

在包含块里开启或关闭将 aware 日期时间对象的转换为当前时区。

就模板引擎而言，该标签与 `USE_TZ` 具有完全相同的效果。它可以更精细地控制转换。

要模板块激活或关闭转换，使用：

    {% load tz %}

    {% localtime on %}
        {{ value }}
    {% endlocaltime %}

    {% localtime off %}
        {{ value }}
    {% endlocaltime %}

注解

`USE_TZ` 的值在 `{% localtime %}` 块内被忽略。

#### `timezone`

在包含块内设置或取消当前时区。当没有设置当前时区时，会使用默认时区。

    {% load tz %}

    {% timezone "Europe/Paris" %}
        Paris time: {{ value }}
    {% endtimezone %}

    {% timezone None %}
        Server time: {{ value }}
    {% endtimezone %}

#### `get_current_timezone`

使用 `get_current_timezone` 标签来获取当前时区的名称：

    {% get_current_timezone as TIME_ZONE %}

另外，你可以激活 `tz()` 上下文处理器并使用 `TIME_ZONE` 上下文变量。

### 模板过滤器

这些过滤器接受 aware 时间和 naive 时间。处于转换目的，它们假设 naive 时间在默认时区中。它们始终返回 aware 时间。

#### `localtime`

单一值强制转换为当前时区。

例如:

    {% load tz %}

    {{ value|localtime }}

#### `utc`

单一值强制转换为 UTC 。

例如:

    {% load tz %}

    {{ value|utc }}

#### `timezone`

单一值强制转换为任意时区。

参数必须是 `tzinfo` 子类实例或时区名。

例如:

    {% load tz %}

    {{ value|timezone:"Europe/Paris" }}

## 迁移指南

以下是迁移在 Django 支持时区之前已有项目的方法。

### 数据库

#### PostgreSQL

PostgreSQL 后端存储将日期时间存储为 `带时区的时间戳` 。事实上，这意味着它在存储时会将日期从连接的时区转换为UTC，并在检索时将UTC转换为连接的时区。

因此，如果你正在使用 PostgreSQL，你可以在 `USE_TZ = False` 和 `USE_TZ = True` 之间自由选择，数据库连接的时区将分别设置为 `TIME_ZONE` 或 `UTC` ，以便 Django 在所有情况下将得到正确的日期。你不需要执行任何数据转换。

#### 其他数据库

其他后端存储没有时区信息的日期。如果你的选择从 `USE_TZ = False` 变为 `USE_TZ = True` ，你必须将你的数据从本地时间转换为UTC —— 如果你的当地时间有夏令时时，则不确定。

### 邮政编码

第一步是将 `USE_TZ = True` 添加到你的配置文件里。在这点上，大多数情况下都应该起作用。如果你在代码里创建 naive 日期时间对象，Django 会在必要时将它们转换为 aware 日期时间对象。

然而，这些转换有可能在夏令时转换时失败，这意味着你并有获得时区支持的所有好处。而且，在运行的时候很可能会遇到一些问题，因为它无法将 naive 日期时间和 aware 日期时间进行比较。由于 Django 现在为你提供了 aware 时间，你在比较来自模型或表单的日期与代码里创建的naive日期时间时，会遇到一些异常。

第二步是重构你在任何地方实例化的日期时间，将它们转换为 aware 日期时间。这可以逐步完成。`django.utils.timezone` 为了代码兼容性而定义了一些方法：`now()`, `is_aware()`, `is_naive()`, `make_aware()`, 和 `make_naive()` 。

最后，为了帮助你定位需要升级的代码，Django 会在你试图保存 naive 日期代码到数据库的时候，引发一个告警。

    RuntimeWarning: DateTimeField ModelName.field_name received a naive
    datetime (2012-01-01 00:00:00) while time zone support is active.

在开发期间，你可以通过在配置文件中添加下面的代码，使得此类警告变成异常，方便追踪：

    import warnings
    warnings.filterwarnings(
        'error', r"DateTimeField .* received a naive datetime",
        RuntimeWarning, r'django\.db\.models\.fields',
    )

### 辅助工具

当序列化 aware 日期时间时，会包含 UTC 偏移，如下：

    "2011-09-01T13:20:30+03:00"

对于 naive 时间，它不能这样：

    "2011-09-01T13:20:30"

对于带有 `DateTimeField` 的模型，这种差异使得编写一个支持和不支持时区的辅助工具变得不可能。

使用 `USE_TZ = False` 生成辅助工具，或者 Django 1.4 版本之前，使用 "naive" 格式化。如果你的项目包含这些辅助工具，则在开始时区支持后，你将会在加载它们时，看到 `RuntimeWarning` 。为了避免这些告警，你必须将辅助工具转换为 "aware" 格式。

你可以先使用 `loaddata` 然后 `dumpdata` 重新生成辅助工具。或者，如果它们足够小，你可以对它们编辑，与将匹配到 `TIME_ZONE` 的 UTC 偏移量添加到每个序列化日期时间。

## FAQ

### 安装

1. 我不需要多时区服务。我应该开启时区支持吗？

   是的。开启时区支持后，Django 会使用更准确的本地时间模型，这样使得你在夏令时转换时避免一些微秒、不可复制的错误。

   开启时区支持后，会遇到一些错误，因为你使用 naive 日期时间，而 Django 期望使用 aware 日期时间。这些错误在测试的时候会显示出来。

   另一方面，由于缺乏时区支持导致的bug很难预防、诊断和修复。任何涉及计划任务或日期计算的事务都有可能是潜在的bug。

   基于这些原因，在新项目时要启用时区支持，除非有很好的理由，否则应该保留它。

2. 我已经开启了时区支持。我安全了吗？

   也许吧。你可以更好地避免夏令时相关错误，但你仍然可以把 naive 日期时间转换为 aware 日期时间，反之亦然。

   如果你的应用连接了其他系统——比如，查询一个web服务——请确保正确指定日期时间。为了便于安全地传输时间，它们的描述应该包含 UTC 偏移量，或者它们的值应该在UTC中（或者二者都包括）。

   最后，我们的日历系统包含了有趣的边缘情况。例如，你不能总是直接从给定日期中减去一年：

       >>> 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

   为了正确实现这样的功能，你必须决定 2012-02-29 减去一年是 2011-02-28 还是 2011-03-01，这取决于你的业务需求。

3. 我改如何与存储本地日期时间的数据库进行交互？

   在 `DATABASES` 里将:setting:TIME_ZONE <DATABASE-TIME_ZONE> 选项设置为适合该数据库的时区。

   当 `USE_TZ` 为 `True` 时，这对连接到不支持时区以及不受 Django 管理的数据库很有用。

### 错误调试

1. 我的程序因不能比较带有偏移的 naive 日期时间与带有偏移的 aware 日期时间而崩溃——怎么回事？

   我们通过比较 naive 时间和 aware 时间来重现一下这个错误：

       >>> 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

   如果遇到这个错误，很可能你的代码正在比较这两件事：

   - Django 支持的日期——比如，读取来自表单或模型字段的值。因为你开启了时区支持，所以它是 aware 的。
   - 通过代码生成了日期，它是 naive 的（或者你不会读取它）。

   一般来说，正确的解决办法是修改代码，用 aware 日期时间代替它。

   如果你正在编写可插拔的应用独立于 `USE_TZ` 的值运行，你会发现 `django.utils.timezone.now()` 很有用。这个函数会在 `USE_TZ = False` 时将当前日期和时间作为 naive 返回，并且在 `USE_TZ = True` 时将当前日期和时间作为 aware 返回。你可以在需要时添加或减少 `datetime.timedelta` 。

2. 我发现了很多告警：在时区支持激活后，DateTimeField 收到了 naive 日期时间((YYYY-MM-DD HH:MM:SS)) ——这样不好吗？

   当启用时区支持后，数据库层会期望仅从你的代码里收到 aware 日期。这个告警发生在数据库收到 naive 日期时间时。这表明你没有为时区支持而完成代码移植。请参考 migration guide 来获取此过程的提示。

   在这期间，为了向后兼容，日期时间被认为处于默认时区内，通常这是你期望的。

3. `now.date()` 方法得到的结果 为什么是昨天（或明天）？

   如果你一直在使用 naive 日期时间，你或许相信可以通过调用 `date()` 方法来将日期时间转换为日期。你也认为 `date` 和 `datetime` 很像，除了它的准确性较差。

   在 aware 日期的环境里，这些都不是正确的：

       >>> import datetime
       >>> import pytz
       >>> paris_tz = pytz.timezone("Europe/Paris")
       >>> new_york_tz = pytz.timezone("America/New_York")
       >>> paris = paris_tz.localize(datetime.datetime(2012, 3, 3, 1, 30))
       # This is the correct way to convert between time zones with pytz.
       >>> new_york = new_york_tz.normalize(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=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
       >>> new_york
       datetime.datetime(2012, 3, 2, 19, 30, tzinfo=<DstTzInfo 'America/New_York' EST-1 day, 19:00:00 STD>)

   如这个例子所示，相同 datetime 有不同的 date，这取决于它所代表的时区。但真实问题更重要。

   一个 datetime 代表一个 时间点。它是客观存在的，不依赖任何事物。另一方面，一个 date 是一个 日历概念 。它是一个时间段，其范围取决于 date 所在的时区。如你所见，这两个概念在根本上是不同的，将 datetime 转换为 date 并不是确定性的操作。

   这在实践中意味着什么？

   通常，你应该避免将 `datetime` 转换为 `date` 。比如，你可以使用 `date` 模板过滤器来只展示 datetime 的 date 部分。这个过滤器在格式化之前将 datetime 转换为为 当前时间，确保显示正确的结果。

   如果你确实需要自己进行转换，你必须首先确保 datetime 转换为合适的时区。通常，这将是当前时区：

       >>> from django.utils import timezone
       >>> timezone.activate(pytz.timezone("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()
       # Again, this is the correct way to convert between time zones with pytz.
       >>> local = current_tz.normalize(paris.astimezone(current_tz))
       >>> local
       datetime.datetime(2012, 3, 3, 8, 30, tzinfo=<DstTzInfo 'Asia/Singapore' SGT+8:00:00 STD>)
       >>> local.date()
       datetime.date(2012, 3, 3)

4. 收到“你的数据库安装时区定义了吗？”的错误

   如果你正在使用 MySQL，查看 时区定义 部分，以获取有关加载时区定义的说明。

### 用法

1. 有一个字符串 `"2012-02-21 10:28:45"` 并且时区是 `"Europe/Helsinki"` 。我该如何将其转换为 aware 日期时间？

   这正是 pytz 的用途。

       >>> from django.utils.dateparse import parse_datetime
       >>> naive = parse_datetime("2012-02-21 10:28:45")
       >>> import pytz
       >>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None)
       datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=<DstTzInfo 'Europe/Helsinki' EET+2:00:00 STD>)

   注意 `localize` 是 `tzinfo` 的 pytz 扩展。另外，你或许想捕捉 `pytz.InvalidTimeError`。pytz 的文档包含了很多例子（ more examples ）。你应该在尝试操作 aware 日期时间之前看一下它。

2. 如何在当前时区里获取当地时间？

   好吧，首先要问下自己，你真的需要这么做吗？

   当与用户进行交互的时候，你只能使用当地时间，并且在模板层提供过滤器和标签，将日期时间转换为你所选择的时区。

   而且，Python 知道如何去比较 aware 日期时间，并在必要时考虑 UTC 偏移量。使用 UTC 编写所有模型和视图代码里很容易（并且可能很快）。因此，在大部分情况下，由 `django.utils.timezone.now()` 返回的 UTC 日期时间足够了。

   但是，为了完整性，如果你真的想在当前时区里获取当地时间，你可以这么做：

       >>> from django.utils import timezone
       >>> timezone.localtime(timezone.now())
       datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)

   在这个例子里，当前时区是 `"Europe/Paris"` 。

3. 如何查看所有可用时区？

   pytz provides helpers, including a list of current time zones and a list of all available time zones -- some of which are only of historical interest. `zoneinfo` also provides similar functionality via `zoneinfo.available_timezones()`.