Django-高级教程-全-

Django 高级教程(全)

原文:Pro Django

协议:CC BY-NC-SA 4.0

一、了解 Django

Abstract

仅有代码是不够的。当然,它是计算机运行的,但是代码必须来自某个地方。程序员必须坐下来决定包括什么特性,它们应该如何实现,利用什么其他软件,以及如何为将来添加的增强功能提供挂钩。直接跳到代码很容易,忽略了产生代码的认知过程,但是伟大的程序员总是有他们做出决定的理由。

仅有代码是不够的。当然,它是计算机运行的,但是代码必须来自某个地方。程序员必须坐下来决定包括什么特性,它们应该如何实现,利用什么其他软件,以及如何为将来添加的增强功能提供挂钩。直接跳到代码很容易,忽略了产生代码的认知过程,但是伟大的程序员总是有他们做出决定的理由。

有了像 Django 这样的框架,许多这样的决定已经做出,提供的工具由这些决定以及做出这些决定的程序员来塑造。通过在您自己的代码中采用这些理念,您不仅将与 Django 和其他应用保持一致,而且您甚至会对您所能完成的事情感到惊讶。

即使是最简单的代码,其背后也隐藏着创造它的思维过程。决定了它应该做什么和如何做。这种思维过程是书籍和手册中经常忽略的一步,这导致了一大批技术人员埋头苦干,编写代码来设法完成手头的任务,但却没有对未来的愿景。

本书的其余部分将详细解释 Django 为最复杂的项目提供的许多基本构建模块,而本章将关注框架的更基本的方面。对于那些来自其他背景的读者来说,本章介绍的观点可能看起来相当陌生,但这并没有降低它们的重要性。所有使用 Python 和 Django 的程序员都会很好地理解 Django 工作方式的原因,以及如何将这些原则应用到其他项目中。

你可能希望不止一次地阅读这一章,并且在你和 Django 一起工作时经常参考它。许多主题都是 Django 社区中的常识,所以如果你打算和其他程序员交流,仔细阅读这一章是非常必要的。

哲学

Django 在很大程度上依赖于哲学,无论是它的代码是如何编写的,还是如何决定什么进入框架。这在编程中并不是独一无二的,但这是新手经常遇到的问题。保持一致性和质量是至关重要的,制定决策时有一套通用的参考原则有助于保持一致性和质量。因为这些概念对于单个应用,甚至是应用的集合也很重要,所以牢牢掌握这些理念将会产生类似的好处。

也许 Python 哲学中最著名和被引用最多的一段话来自 Tim Peters,他是一位长期的 Python 大师,写下了许多指导 Python 自身开发过程的原则。他想出的 19 行代码被称为 Python 的禅,随着时间的推移,它们对 Python 程序员的影响如此之大,以至于被称为 Python 增强提案(PEP) 20 1 并在 Python 发行版本身中被称为“复活节彩蛋”模块this

>>> import this

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren't special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you're Dutch.

Now is better than never.

Although never is often better than *right* now.

If the implementation is hard to explain, it's a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let's do more of those!

虽然其中一些显然是为了幽默,但它很好地总结了常见的 Python 态度。本章的剩余部分强调了 Django 社区中经常引用的一些具体原则,但是所有专业的 Python 程序员都应该记住这些内容并经常引用。

需要记住的一件重要事情是,Python 中的许多内容都是主观的。例如,“美丽”可能比“丑陋”更好,但“美丽”的定义是丰富的,可以根据提供它们的人的不同而变化。同样,考虑简单和复杂、实用和纯洁的概念;每个开发人员对于某段代码应该放在生产线的哪一边都有不同的看法。

Django 对 MVC 模式的解释

爱好者和公司都采用的最常见的应用架构之一是模型-视图-控制器(MVC)模式,因为它在应用的主要方面之间提供了清晰的任务和责任分离。Django 只是松散地遵循这种方法。适当的讨论应该从对其组件的快速概述开始。

  • 该模型通常负责管理数据和核心业务逻辑。
  • 查看器向用户显示该数据。

控制器接受用户输入并执行特定于应用的逻辑。尽管这种模式在许多领域被证明是非常有效的,但是 Django 的作者在一开始并没有寻求符合任何一种模式。他们只是对寻找开发 Web 软件的最有效方法感兴趣。毕竟,Django 是为一份工作报纸的日常需求而建立的,如果事情要发生,就必须非常迅速地发生。最终,将任务分成独立的组有几个不同的目的。

  • 为特定任务集设计的代码更易于维护,因为它不需要对应用中完全不相关的部分做出假设。一般来说,这个概念被称为关注点分离,并且适用于整个软件开发。
  • 应用开发变得更加灵活,因为多个明显不同的视图和控制器层可以连接到单个模型层。这使得各种应用能够共享相同的业务逻辑和数据,为不同的受众以不同的方式呈现业务逻辑和数据并与之交互。
  • 开发人员能够学习系统中与正在执行的工作相关的那些部分。这种专业化有助于抑制沮丧和疲劳,同时在每个开发人员的专业领域内培养创造力和卓越。

当然还有其他较小的好处,但这些通常是使用 MVC 实现的主要目标。然而,有趣的是,这些好处中适用于 MVC 模式中任何特定部分的唯一部分是将多个应用插入单个模型层的能力。剩下的只是基于共同发展计划的任意划分。

Django 的开发人员寻求同样的好处,但是强调快速开发,在获得一套对他们的工作流有意义的工具后,他们最终采用了一些人所说的模型-模板-视图(MTV)模式。然而,Django 应用中实际上有四个主要的代码部分,下面将对其进行概述。

模型

考虑到将模型与应用的其他部分分开的好处,Django 严格遵循了 MVC 的这一部分。Django 模型提供了对底层数据存储机制的简单访问,并且还可以封装任何核心业务逻辑,这些逻辑必须始终有效,不管哪个应用正在使用它。

模型独立于系统的其余部分而存在,并且被设计为由任何可以访问它们的应用使用。事实上,模型实例上可用的数据库操作方法甚至可以从交互式解释器中使用,而无需加载 Web 服务器或任何特定于应用的逻辑。

第三章更详细地介绍了 Django 模型,包括如何定义和利用它们,如何包含你自己的业务逻辑,等等。

尽管 Django 视图与最初的 MVC 定义有着相同的名字,但是它们与传统的范例没有什么共同之处。相反,它们将一些传统视图的责任与管制员的全部任务结合起来。视图接受用户输入(包括简单的信息请求),根据应用的交互逻辑进行操作,并返回一个适合用户访问由模型表示的数据的显示。

视图通常被定义为标准的 Python 函数,当用户请求特定的 URL 时会调用这些函数。就 Web 而言,即使是简单的信息请求也被认为是一个动作,因此视图旨在处理数据修改和其他提交。视图可以访问模型,根据需要检索和更新信息,以完成用户请求的任务。

由于视图被简单地称为函数,不需要任何特定的结构,因此可以用多种方式来指定它们。除了简单的函数之外,视图还可以采用任何 Python 可调用的形式,包括类、实例方法、可调用对象以及 curried 或修饰函数。

模板

虽然视图在技术上负责向用户呈现数据,但如何呈现数据的任务通常委托给模板,模板是 Django 开发中足够重要的一部分,可以完全视为一个独立的层。许多人将 Django 模板和传统视图层相提并论,因为模板处理用户将看到的所有表示细节。

Django 为此提供了一种简单的模板语言,这样模板设计者不需要仅仅为了使用模板而学习 Python。Django 的模板语言不依赖于任何特定的表示语言。它主要用于 HTML,但也可以用于生成任何基于文本的格式。

但是,请记住,这个模板引擎只是视图可以用来为用户呈现显示的一个工具。许多视图可能使用 HTTP 重定向到其他 URL、第三方可移植文档格式(PDF)库或其他任何东西来生成它们的输出。

URL 配置

作为一个 Web 框架,Django 提供了一个单独的粘合层,使视图在特定的 URL 上对外界可用。通过提供一个正则表达式作为 URL 组件,单个声明可以以高度可读和高度可维护的方式容纳各种各样的特定 URL。

这个配置与视图本身分开定义,以允许在多个 URL 上配置一个视图,每个位置可能有不同的选项。事实上,Django 的核心特性之一就是通用视图的概念。这些视图旨在满足一般需求,具有允许在任何应用中使用的配置选项,只需要一个 URL 配置就可以启用它们。

也许最重要的是,将 URL 作为过程的一个独立部分鼓励开发人员将 URL 视为应用整体设计的一部分。因为它们必须在书签、博客文章和营销活动中使用,所以 URL 有时比你的应用更容易被看到。毕竟,那些在浏览网页时全神贯注的用户会在决定访问你的网站之前就看到你的网址。当使用印刷媒体进行广告宣传时,URL 变得更加重要。

第四章更详细地介绍了 URL 配置,包括一些正确的 URL 设计指南。

松耦合

MVC 架构的一个关键特性,以及 Django 稍微修改后的形式,是执行明显不同功能的代码部分不应该依赖于其他部分如何操作的概念。这被称为松耦合。这与紧耦合形成对比,在紧耦合中,模块通常严重依赖于其他模块实现的内部细节。

紧密耦合会导致长期代码维护的一大堆问题,因为对一个部分的重大更改总是会影响到其他部分。这给程序员带来了堆积如山的额外工作,他们不得不修改与需要完成的工作没有什么关系的代码。这些额外的工作不仅仅影响程序员;对雇主来说,这通常也相当昂贵。紧耦合也使得测试更加困难,因为隔离个体行为更加困难。

看起来松散耦合主张任何代码都不应该知道任何其他代码,但事实并非如此,因为这样编写的程序根本不能做任何事情。代码的某些部分总是需要引用其他部分;那是不可避免的。关键是尽可能少地依赖实现细节。

在 Python 中,松散耦合通常以多种方式提供,下面的列表显示了其中的一些方式。还有数不清的其他方法,它们本身可以写满一本书,但是这里展示的技术在第二章中有详细的描述。

  • 鸭子打字
  • 运算符重载
  • 信号和调度
  • 插件

不要重复自己(干)

如果你在这个街区呆过几次,你就会非常清楚编写“样板”代码是多么容易。你为一个目的编码一次,然后为另一个目的编码,一次又一次,一次又一次。过了一段时间,你会意识到有多少代码被复制了,如果你幸运的话,你有时间、精力和头脑清醒来看看有什么是共同的,并把这些部分移到一个共同的位置。

这个过程是框架存在的主要原因之一。框架提供了大部分这种公共代码,同时试图使其更容易避免在将来重复您自己的代码。这代表了一种常见的编程实践:不要重复自己。

通常缩写为 DRY,这个术语在对话中经常出现,可以用作

  • 一个名词——“这个代码违反了 DRY。”
  • 一个形容词——“我喜欢这种方式,它非常干燥。”
  • 一个动词——“让我们试着把它弄干一点。”

基本思想是你应该只写一次。这降低了在两个应该匹配的代码之间意外引入不一致的风险。它还应该尽可能地可重用,如果其他代码需要了解您已经编写的内容,您应该能够使用 Python 自动获取必要的信息,而不需要程序员重复这些信息。

为了实现这一点,Python 提供了丰富的资源来查看代码内部,这一过程称为内省。第二章中提到的许多资源在你的代码中支持 DRY 时非常有用。

注重可读性

"可读性很重要。"正如前面提到的,在 Python 的禅宗中特别提到了这一点,这可能是 Python 最重要的特性之一。事实上,许多 Python 程序员为他们编写的语言和代码的可读性感到自豪。这个想法是,代码被阅读的次数远远多于它被编写的次数,尤其是在开源的世界里。

为此,Python 提供了许多旨在提高可读性的特性。例如,它最少使用标点符号和强制缩进,使得语言本身有助于保持代码的可读性。然而,当您在现实世界中处理代码时,需要考虑的事情要多得多。

在现实生活中,Python 社区开发了一套编写代码的指南,旨在提高可读性。在 PEP-8, 2 中阐述了这些指导方针,它们的设计目的不仅是为了保持单个程序的可读性,也是为了保持跨多个程序的一致性。一旦你对一个写得好的程序有了感觉,你就能很容易地理解其他程序。

PEP-8 的具体细节太多了,无法在此一一列出,所以请务必通读一遍,以便更好地了解如何编写好的代码。另外,请注意,如果您阅读 Django 自己的源代码,PEP-8 中规定的一些规则并没有得到遵守。具有讽刺意味的是,这仍然是为了可读性,因为严格遵循每一条规则有时会导致其他问题。毕竟,再次引用 Python 的禅,“实用性胜过纯粹性。”本书中的例子将遵循 Django 自己的源代码所使用的风格。

大声失败

“错误永远不会无声无息地过去。/除非明确消音。”这似乎是一个简单的观点,但是在两行中,它包含了超过 10%的 Python 禅,这是有道理的。处理异常是编程的一个重要部分,在 Python 中尤其如此。所有的编程语言都会产生错误,而且大多数都有一种优雅地处理错误的方法,但是每种语言都有自己处理错误的最佳实践。

需要记住的一个关键点是,尽管大多数 Python 异常的名称以Error结尾,但是基类被称为Exception。为了理解应该如何使用和处理它们,从学习为什么使用这个特定的单词开始是有用的。查看“exception”一词的一些字典定义,很容易发现一个主题的变体。

  • 例外的东西;不符合一般规则的例子或情况
  • 例外被排除的事物,尤指不符合规则或一般规律的情况
  • 不符合规则或概括的例子

异常不是描述发生问题的情况的错误,而是简单地描述意外发生的情况。这似乎是一个微妙的区别,但有些人将异常视为错误,只保留它们用于不可恢复的问题,如文件损坏或网络故障。在某些语言中,引发异常的代价极其昂贵,因此为了防止性能问题,尽可能避免出现异常。

然而,在 Python 中,异常并不比简单的返回值更昂贵,这允许它们更精确地符合它们的字典定义。如果我们将异常定义为对规则的违反,那么显而易见,我们必须首先定义规则。

定义规则这是理解异常的最重要的方面,所以有必要非常清楚:没有定义规则的 Python 语法。这根本不是这种语言的特征。其他一些语言明确支持契约式设计, 3 并且许多语言可以通过框架级代码支持它,但是 Python 本身不支持任何形式的契约式设计。

相反,规则是由程序员按照他们希望代码做什么来定义的。这似乎过于简化了,但事实并非如此。一段代码完全按照作者的意图去做,仅此而已。程序员意图之外的任何事情都可以——也应该——被视为例外。为了说明这一点,下面是 Python 和 Django 使用的一些规则:

  • 使用括号语法(my_list[3])访问列表中的项目会返回指定位置的项目。
  • 集合的discard()方法确保指定的项目不再是集合的成员。
  • QuerySet 的get()方法只返回一个与提供的参数匹配的对象。

诸如此类的例子很重要,因为即使这些规则很简单,它们也准确地描述了给定特性在各种情况下的行为。为了进一步说明,考虑以下场景以及规则如何影响行为。

  • 如果作为列表项引用提供的索引确实存在,将返回适当的值。如果没有,就会引发一个异常(IndexError)。如果用作索引的值不是整数,则会引发一个不同的异常(TypeError)。
  • 如果使用discard()从器械包中移除的物品已经是该器械包的一员,那么它将被简单地移除。如果它不是集合的成员,discard()返回而不引发异常,因为discard()只确保该项目不在集合中。
  • 如果传递给 QuerySet 的get()方法的参数与数据库中的一个记录匹配,则该记录作为适当模型的实例返回。如果没有匹配的记录,则引发异常(DoesNotExist),但是如果有多条记录匹配,则引发不同的异常(MultipleObjectsReturned)。最后,如果参数不能用于查询数据库(由于不正确的类型、未知的属性名或各种其他条件),就会引发另一个异常(TypeError)。

显然,只要明确定义,即使是简单的规则也会产生深远的影响。虽然唯一的要求是规则必须在作者的头脑中定义,但是如果规则没有传达给其他人,那么规则就没有什么用处。对于 Django 这样的框架来说,这一点变得尤为重要,因为它是为向大众分发而构建的。

记录规则

有许多适当的方法来记录编写一段代码所遵循的特定规则。用多种方式,以不同的复杂程度来指定它们甚至是非常有用的。人们在四个主要的地方寻找这些信息,所以在任何一个或所有这些地方提供这些信息都会很好地达到目的。

  • 文档——因为这应该是关于应用的完整信息集合,所以理所当然应该包括这些规则。
  • 文档字符串——不管独立的文档,开发人员经常会偷看代码本身,看看它是如何工作的。Docstrings 允许您在实现这些规则的代码旁边提供这些规则的纯文本解释。
  • 测试——除了为人类理解这些规则提供解释之外,以 Python 可以理解的方式提供它们是一个好主意。这允许定期验证您的规则。此外,doctests——嵌入在 docstrings 中的测试——也是人类可读的,并且可以同时达到这两个目的。
  • 注释——有时,一个函数可能非常复杂,以至于一个宽泛的概述,比如可以在完整的文档或者甚至是 docstring 中找到的概述,并不能给出关于特定代码块应该做什么的足够信息。Python 对可读性的强调使得这种情况相当少见,但还是会发生。当出现这种情况时,注释是向其他人解释代码意图的有用方式,因此什么应该被视为例外。特别是,注释应该解释代码的目的,而不仅仅是概述每一行实际上在做什么。思考为什么,而不是如何。

不管你选择如何描述你的规则,有一个教训必须永远优先:明确。请记住,规则中没有列出的任何内容都应被视为异常,因此明确定义规则将有助于您决定代码在不同情况下的行为,包括何时引发异常。

还有,要始终如一。许多类和函数在名称或接口上看起来很相似,只要有可能,它们的行为应该相似。习惯于某一特定行为的程序员会期望类似组件的类似行为,最好满足这些期望。当编写模仿 Python 或 Django 提供的类型的代码时尤其如此,因为它们已经被很多程序员很好地记录和理解了。

社区

自从 2005 年向公众发布以来,Django 在技术和文化上都取得了巨大的成功。它在 Python Web 开发领域积累了大量的追随者,包括业余爱好者和专业人士。这个社区是该框架及其用户最大的资产之一,它肯定值得详细讨论。

AN EVOLVING COMMUNITY

重要的是要认识到,像任何社会结构一样,Django 社区将随着时间的推移而发展和变化。因此,本节中的信息可能并不总是准确地反映当前的实践和期望。

但是,没有理由让这阻止你。我不期望改变的一件事是社区接受新成员的意愿。如果你愿意出去,你总能接触到各种各样的人。

框架的管理

关于 Django 和 Python 的开发,首先要理解的一件事是,尽管任何人都可以查看和操作框架的代码(毕竟它是开源的),但核心发行版的整体管理是由一小部分人监督的。这些“核心开发人员”包括那些有权更新主要代码库的人。

WHAT IS “CORE”?

因为 Django 是开源的,任何用户都可以修改 Django 的代码并分发修改后的副本。许多开发人员已经这样做了,添加了重要的特性和增强功能,并提供他们的工作给其他人使用。高级用户可以对核心代码进行相当大的修改,而不会影响那些不需要副本提供的功能的人。

此外,开发人员被允许——也受到鼓励——将他们的应用通用化并分发给其他人。这些有时变得如此普遍,以至于许多开发人员在他们开始的任何新项目中都默认包含它们。

相比之下,Django 的核心只是通过 Django 主网站发布的代码,要么作为官方版本,要么作为主干开发代码。因此,当讨论包括关于某个东西是否应该“在核心中”的辩论时,两难的问题是它应该进入官方发行版还是某种第三方格式,比如分支或分布式应用。

一个有趣的灰色区域是django.contrib包。它是在 Django 主发行版中发布的,因此有资格成为 core 的一部分,但是它们被设计成第三方应用。我们的目标是,如果一个第三方应用写得足够好,在社区中获得足够的吸引力,并有持续支持的承诺,它最终会被拉入 core。然而在实践中,更常见的是反其道而行之,将django.contrib包从核心中移除,作为第三方应用来维护。

这种结构有助于确保那些对框架及其历史最有经验的人在将所有补丁提交到存储库之前,负责检查并经常调整它们。他们还定期讨论有关框架的最新发展、需要进行的重大改革、可以做出的重大改进等问题。

仍然有人在管理链的顶端。这个职位被称为终身仁慈独裁者,通常缩写为 BDFL,是为那些在需要打破僵局或推翻多数决定时拥有最终决定权的人保留的。值得庆幸的是,他们是真正仁慈的独裁者,这种区别不容忽视。

事实上,BDFL 的想法比其他任何东西都更幽默。虽然他们拥有最终的权力,但这种权力很少被行使,因为他们倾向于支持群体意见。当他们确实需要介入并仲裁一个决定时,他们的裁决是基于多年的经验,知道什么对框架和它的受众最好。事实上,他们通常会将自己的想法提交给整个团队进行讨论,如果有合适的反驳,甚至可能会遵从团队的意见。

对于那些来自企业背景的读者来说,BDFL 的概念可能显得陌生,在企业背景下,设计决策通常由委员会做出,多数规则和变更需要经过详尽的官僚程序。相反,不太直接的监督通常会导致不同领域的小专家小组,他们完全有能力独立行动,产生高质量的代码。这个简单的结构允许流程在需要时更快地运行,更重要的是,有助于在框架内保持更大的一致性。

在 Python 的世界里,Python 本身的创造者吉多·范·罗苏姆拥有 BDFL 的地位。对于 Django 来说,它是由两个人举办的,每个人的官方头衔都是“共同 BDFL ”: Adrian Holovaty,该框架的共同创建者,和 Jacob Kaplan-Moss,Django 当前工作的主要开发者。贯穿本章的原则和哲学通常反映了 BDFLs 的观点和理想。

新闻与资源

像 Django 这样一个充满激情和活力的社区,及时了解其他人在做什么、他们对常见问题找到了什么解决方案、可用的新应用以及许多其他事情是非常重要的。考虑到这个社区的规模和多样性,保持下去可能看起来是一项艰巨的任务,但实际上非常简单。

首先要关注的是 Django 博客4——官方新闻渠道,其中包含关于框架本身、其开发及其在主要工作中的使用的新闻和更新。例如,Django 博客会发布新的版本、即将到来的开发冲刺和项目网站的更新。

也许更重要的是 Django 社区新闻聚合器, 5 ,它从世界各地的开发者那里收集文章,将它们全部展示在一个地方。这里可用的信息种类更加多样,因为它是由社区成员生成的,这使它成为一个非常有价值的资源。示例内容可能包括新的和更新的应用、解决常见问题的技巧和诀窍以及新的 Django 支持的网站。

可重用的应用

Django 最有价值的方面之一是它专注于基于应用的开发。开发人员应该为特定的目的编写应用,然后将它们组合起来构建一个站点,而不是从头开始构建每个站点。这种理念鼓励许多社区成员以开源的方式向公众发布他们的应用,这样其他人就可以从他们的特性中受益。开发者可以自由地在任何他们希望的地方托管他们的应用,但是许多人选择 GitHub, 6 是因为它丰富的特性和非常活跃的开发者社区。事实上,Django 本身就在这里。GitHub 整合了自己的问题跟踪系统,使得在一个地方维护所有东西变得容易。许多应用 7 都托管在那里,所以花几分钟四处看看是否有人已经写了你需要的东西绝对是个好主意。您还可以在 Django 包中找到并比较第三方应用。 8

毕竟,这是开源软件的主要目标之一:一个更大的社区可以比一个更小的专门程序员小组产生更好、更干净、更有功能的代码。Django 社区既展示了这种行为,也鼓励其他人利用这种行为。

获得帮助

尽管这本书和其他书中包含了所有的知识,但假装每种潜在的情况都可以提前记录下来是愚蠢的。此外,可用的文档并不总是容易找到或理解。在上述任何一种情况下,你可能会发现自己需要向活生生的人展示自己的情况,以真实世界的经验,希望有人能发现问题并提出解决方案。

首先要知道这不是问题。任何人都可能遇到意想不到的情况,即使是我们中最优秀、最聪明的人也会被最简单的语法错误弄糊涂。如果你遇到了这种情况,要知道 Django 的社区是很温柔的,有需要的时候一定要寻求帮助。

阅读文档

试图解决任何问题的第一步总是阅读官方文档。随着新功能的增加和现有行为的改变,它非常全面,并且定期更新。当遇到错误时,文档将有助于确保您按照预期的方式使用 Django。

一旦您的代码与文档显示的相匹配,就该看看其他常见问题了。

检查您的版本

如前所述,官方文档与 Django 的主干开发保持同步,因此文档中的特性很可能与您正在使用的代码中的特性不匹配。如果您使用的是官方版本,这种情况更有可能发生,但是如果您跟踪 trunk,这种情况仍然会发生,这取决于您更新本地副本的频率。

当您跟踪主干时,关于向后不兼容的 9 变更的文章应该被认为是官方文档的重要部分。如果您在更新后遇到问题,请确保您正在使用的功能没有发生变化。

常见问题解答

在用下面的方法回答问题几年后,Django 社区听到了各种各样定期出现的问题。为了帮助更容易地回答这些问题,有两篇文章。虽然官方 FAQ 10 包括了很多与故障排除问题无关的问题,但还是有几个常见问题列在那里。

互联网中继聊天(IRC)频道有自己的一套问题和答案以及自己的 FAQ。 11

邮件表

获得帮助的最简单的方法之一是在 django-users 邮件列表上提出你的问题。因为它通过标准的电子邮件运行,所以每个人都可以访问,不需要任何特殊的软件。只要加入这个列表,你就可以把你的问题发布给成千上万的其他用户。没有保证,但大多数问题会很快得到回答。

邮件列表的一个主要优点是所有的对话都被存档以备将来参考。除了 FAQ 之外,django-users 邮件列表档案也是一个非常有价值的资源,当你试图追踪一个以前可能发生在某人身上的问题时。不过,在问问题之前,一定要搜索一下档案,因为很有可能其他人也遇到过。

互联网中继聊天(IRC)

如果你需要更快的答案,最好的选择是 Django IRC 频道, 13 ,在那里,Django 社区的许多知识渊博的成员可以直接对话。这是一个非常有用的环境,但是你应该准备好提供关于问题的具体细节。这可能包括精确的错误追溯、模型片段、视图和其他可能与问题有关的代码。

这些代码通常是通过在线剪贴板共享的——一个临时放一些代码给其他人看的地方。代码可以在有限的时间内粘贴到公共网站上,允许与其他人共享。GitHub 为此提供了一个工具,名为 gist, 14 ,这是一个在 IRC 和其他地方与用户共享代码的简单工具。

现在怎么办?

当然,学习哲学和社区并不会写出任何代码。知道如何很好地使用工具会有所帮助,但是如果没有一套工具来使用,那就什么都不是了。下一章概述了 Python 本身提供的许多不常用的工具,而剩余的章节探索了 Django 自己的工具集。

Footnotes 1

1 http://prodjango.com/pep-20/

2

2 http://prodjango.com/pep-8/

3

3 http://prodjango.com/design-by-contract/

4

4 http://prodjango.com/django-weblog/

5

5 http://prodjango.com/community/

6

6 http://prodjango.com/github/

7

7 http://prodjango.com/github-projects/

8

8 http://prodjango.com/djangopackages/

9

9 http://prodjango.com/backwards-incompatible-changes/

10

10 http://prodjango.com/faq/

11

11 http://prodjango.com/irc-faq/

12

12 http://prodjango.com/django-users/

13

13 http://prodjango.com/irc/

14

14 http://prodjango.com/gist/

二、Django 就是 Python

Abstract

Django 和其他框架一样,是建立在底层编程语言上的——在这里是 Python——来完成它的工作。许多不熟悉 Django 的人也不熟悉 Python,Python 的自然感觉语法结合 Django 的节能特性可以使 Django 看起来像是使用了某种元语言,但事实并非如此。

Django 和其他框架一样,是建立在底层编程语言上的——在这里是 Python——来完成它的工作。许多不熟悉 Django 的人也不熟悉 Python,Python 的自然感觉语法结合 Django 的节能特性可以使 Django 看起来像是使用了某种元语言,但事实并非如此。

要想正确理解 Django 能做什么,首先必须知道 Django 就是 Python,就像所有的应用一样。在 Python 中可以完成的任何事情在 Django 中都可以完成,这使得可能性几乎是无限的。

这也意味着 Django 应用不仅可以访问整个 Python 标准库,还可以访问大量第三方库和实用程序。其中一些接口是随 Django 本身一起提供的,所以在很多情况下,现有的代码和文档足以快速启动并运行应用。

在本书的后面,将会介绍一些额外的实用程序,以及一些关于如何将它们集成到 Django 应用中的技巧。可能性不仅限于本书中概述的选项,所以请随意寻找有助于支持您的商业计划的 Python 实用程序,并使用本书中列出的技术将它们集成到您的应用中。

虽然学习 Python 超出了本书的范围,但是 Django 使用了它的一些高级特性。在这一章中,我将讨论其中的许多特性,以帮助你理解 Python 是如何帮助实现让每个人都更容易的目标的。

Python 如何构建类

Django 所依赖的一些最先进的 Python 技术与 Python 如何构造其类有关。这个过程通常被大多数开发人员认为是理所当然的——也应该如此——但是因为它是 Django 的核心,所以它构成了这个探索的基础。

当 Python 解释器遇到一个类定义时,它会像读取任何其他代码一样读取它的内容。然后,Python 为该类创建了一个新的名称空间,并执行其中的所有代码,将所有变量赋值写入这个新的名称空间。类定义通常包含变量、方法和其他类,所有这些基本上都是类的命名空间的赋值。然而,这里几乎允许任何有效的代码,包括打印到控制台输出、写入日志文件甚至触发 GUI 交互。

一旦内容执行完毕,Python 就会有一个类对象,通常放在定义它的名称空间(通常是模块的全局名称空间)中,然后在这里传递或调用它来创建该类的实例。

类正常类:

...打印(“加载正常类”)

...垃圾邮件= '鸡蛋'

...打印(“完成加载”)

...

正在加载普通类

完成加载

普通类

NormalClass.spam

鸡蛋

如您所见,代码在类定义中执行,一旦类准备就绪,任何赋值变量都显示为类属性。

以编程方式构建类

上一节描述的过程适用于任何源代码声明的类,但是 Python 处理它的方式提供了更有趣的可能性。在幕后,关于类声明的细节被发送到内置的type对象,该对象负责为类创建适当的 Python 对象。对于每个类,当它完成对类声明内容的解析时,这将自动发生。

type的构造函数接受三个参数,它们代表整个类声明。

  • name—以字符串形式提供给类的名称
  • bases—类的继承链中的类元组;可能是空的
  • attrs—类名称空间的字典

COMPATIBILITY: NEW-STYLE CLASSES IN PYTHON 2

本节描述的过程适用于新型 Python 类,Python 2.2 1 中引入的区别在 Python 3 中已经完全去除,但是如果您正在使用 Python 2,您将需要确保强制使用新型类。为此,只需确保该类继承自其继承链中某处的内置object类型。

Django 提供的所有子类都已经从object派生出来了,所以任何进一步的派生都将自动成为新类型的类,而不需要您做任何额外的工作。尽管如此,记住这种区别是很重要的,这样你的应用可能需要的任何自定义类都将表现出本章概述的行为。

像任何 Python 对象一样,新的type可以在任何时间从任何代码块实例化。这意味着您的代码可以基于运行时收集的数据构造一个新的类。下面的代码演示了一种在运行时声明类的方法,它在功能上等同于上一节中提供的示例。

dynamic class = type(' dynamic class ',(),{'spam': 'eggs'})

动态类

DynamicClass.spam

鸡蛋

A WARNING ABOUT TYPE()

手动使用type()可以很容易地创建重名的类,甚至可以通过在字典的attrs参数中提供一个__module__键来定制模块的位置。尽管这些特性可能是有用的,正如本书后面将要展示的,但是它们会导致自省的问题。

您可以合理地拥有两个具有相同名称和模块的不同类,但是您的代码无法区分它们之间的区别。在某些情况下,这可能不是问题,但是需要注意。

元类改变了它

实际上是一个元类——一个创建其他类的类——我们所从事的被称为元编程。 2 本质上,元编程是在运行时而不是编程时创建或修改代码。Python 允许一个类定义一个不同的元类来执行它的工作,从而允许您定制这个过程。

如果一个类定义包含一个单独的类作为它的metaclass选项,那么这个元类将被调用来创建这个类,而不是内置的type对象。这允许您的代码读取、修改甚至完全替换已声明的类,以进一步定制其功能。从技术上讲,metaclass选项可以被赋予任何有效的 Python callable,但是大多数元类都是type的子类。元类接收新类作为它的第一个参数,并提供对类对象及其声明细节的访问。

为了帮助说明元类参数是如何从类定义中派生出来的,以下面的代码为例。

类元类(类型):

-我...。def _ _ init _(cls、名称、基础、属性):

...打印('定义%s' % cls ')

...打印('名称:%s' % name ')

...print(' base:% s ' %(base,))

...打印('属性:')

...对于 attrs.items()中的(名称,值):

...打印(' %s: %r' %(名称,值))

...

class RealClass(object,元类=元类):

...垃圾邮件= '鸡蛋'

...

定义

名称:RealClass

碱基:(,)

属性:

垃圾邮件:“鸡蛋”

__ 模块 __: 'main '

qualname: 'RealClass '

RealClass

注意,这个类在任何时候都没有被实例化;创建类的简单动作触发了元类的执行。注意属性列表中的__module__:这个属性是所有 Python 类的标准部分。

虽然这个例子使用了__init__方法来对新创建的类执行特殊处理,但是还有另一个更强大的方法叫做__new__,它有一组不同的可能性。如后面章节所述,Django 在配置它的许多类时使用了__new__

COMPATIBILITY: METACLASSES IN PYTHON 2

Python 3 引入了将参数传递给类定义的能力,如这里的metaclass选项所示。在 Python 2 中,元类被分配给一个名为__metaclass__的类变量。两个版本的效果是相同的;这只是语法上的变化。

将基类与元类一起使用

元类可能非常有用,但是metaclass选项是一个实现细节,在定义类时不应该成为过程的一部分。另一个问题是,虽然每个类都由元类处理,但它们并不从任何具体的类继承。这意味着任何额外的功能,比如公共方法或属性,都必须在元类处理过程中提供才能发挥作用。

稍加小心,具体的 Python 类可以使用元类来解决这两个问题。因为子类从其父类继承属性,所以metaclass选项自动提供给定义它的类的所有子类。这是为任意类提供元类处理的一种简单有效的方法,不需要每个类都定义metaclass选项。按照上一节的例子,看看当我们子类化RealClass时会发生什么。

class 子类(RealClass): #注意这里没有元类。

...及格

...

定义

名称:子类

碱基:(,)

属性:

__ 模块 __: 'main '

注意这里的子类不必担心在幕后有一个元类在使用。通过指定一个基类,它继承了所有的好处。Django 使用这个行为来实现它最突出的特性之一,将在下一节中描述。

声明语法

Django 的一些更突出的工具具有“声明性语法”,读、写和理解都很简单。该语法旨在最大限度地减少“样板”重复语法,并提供优雅、可读的代码。例如,下面是一个典型的 Django 模型和更多可能的样子:

班级联系(模型。型号):

"""

向网站所有者发送消息时提供的联系信息。

"""

名称=型号。CharField(max_length=255)

邮件=模特。电子邮件字段()

这种声明性语法已经成为 Django 代码的一个识别特征,所以许多提供额外框架的第三方应用都使用类似于 Django 本身的语法。这有助于开发人员更容易理解和利用新代码,因为它让所有代码感觉更有凝聚力。一旦您理解了如何使用声明性语法创建一个类,您将能够很容易地使用 Django 的许多特性来创建类,包括官方和社区提供的特性。

单独看一下声明性语法,就会发现为 Django 创建一个符合这种模式的全新框架是多么容易。在您自己的代码中使用声明性语法将有助于您和您的同事更容易地适应代码,从而确保更高的生产率。毕竟,开发效率是 Django 和 Python 本身的主要目标。

虽然接下来的几节描述了一般的声明性语法,但是给出的例子是针对 Django 的对象关系映射器(ORM)的,详见第三章。

集中访问

通常,一个包会提供一个单独的模块,应用可以通过这个模块访问所有必要的实用程序。这个模块可以从树中的其他地方提取单独的类和函数,所以它们仍然可以使用可维护的名称空间,但是它们都将被收集到一个中心位置。

从 django.db 导入模型

一旦导入,该模块至少提供一个类,作为基于框架的子类的基类。提供了附加的类作为新子类的属性。这些对象将共同控制新类的工作方式。

基类

每个特性都以至少一个基类开始。根据框架的需要,可能会有更多,但是为了使这种语法成为可能,至少需要一个。如果没有它,您要求用户定义的每个类都必须显式地包含一个元类,这是大多数用户不需要知道的实现细节。

班级联系(模型。型号):

除了检查已定义的属性,这个基类还将提供一组子类将自动继承的方法和属性。像任何其他类一样,它可以根据需要简单或复杂,以提供框架所需的任何特性。

属性类别

提供基类的模块还将提供一组要实例化的类,通常带有可选的参数来定制它们的行为,并被指定为新类的属性。

班级联系(模型。型号):

名称=型号。CharField(max_length=255)

邮件=模特。电子邮件字段()

这些对象提供的特性在不同的框架中会有很大的不同,有些特性的行为可能与标准属性大相径庭。通常,它们会与元类相结合,提供一些额外的幕后功能,而不仅仅是简单地分配一个属性。这些属性类的选项通常由元类在创建这个额外功能时读取。

例如,Django 的Model使用字段属性的名称和选项来描述底层数据库表,然后可以在数据库中自动创建该表。字段名称用于访问该表中的各个列,而属性类和选项会自动将原生 Python 数据类型转换为适当的数据库值。下一章将提供更多关于 Django 如何处理模型类和字段的信息。

分类属性排序

使用声明性语法时,一个潜在的混淆点是 Python 字典是无序的,而不是遵守它们的值被赋值的顺序。通常这不是问题,但是在检查名称空间字典时,不可能确定键的声明顺序。如果一个框架需要遍历它的特殊属性,或者向用户或程序员显示这些属性,那么按照定义的顺序访问这些属性通常是很有用的。这给了程序员对属性顺序的最终控制权,而不是由编程语言决定的任意顺序。

一个简单的解决方案是让属性自己跟踪实例化序列;然后元类可以相应地对它们进行排序。这个过程的工作原理是让所有的属性类都从一个特定的基类继承,这个基类可以计算类被实例化的次数,并为每个实例分配一个数字。

class BaseAttribute(对象):

creation_counter = 1

def init(self):

self . creation _ counter = base attribute . creation _ counter

base attribute . creation _ counter+= 1

对象实例与类有不同的名称空间,所以这个类的所有实例都有一个creation_counter,它可以用来根据实例化的顺序对对象进行排序。这不是这个问题的唯一解决方案,但是 Django 是这样对模型和表单的字段进行排序的。

类别声明

有了模块中的所有这些类,创建一个应用类就像定义一个子类和一些属性一样简单。不同的框架对于属性类会有不同的名称,并且对于哪些类是必需的或者它们可能被应用的组合会有不同的需求。它们甚至可能有保留名称,如果您使用该名称定义属性,这将导致冲突,但这种问题很少发生,并且在开发使用该语法的新框架时,通常不鼓励保留名称。一般的规则是允许开发者尽可能的灵活,没有框架的阻碍。

从 django.db 导入模型

班级联系(模型。型号):

"""

向网站所有者发送消息时提供的联系信息。

"""

名称=型号。CharField(max_length=255)

邮件=模特。电子邮件字段()

仅仅这些简单的代码就足以让框架给新类注入大量的附加功能,而不需要程序员手动处理这个过程。还要注意所有的属性类是如何从同一个基本模块中提供的,以及在分配给模型时是如何被实例化的。

类声明决不仅限于框架提供的那些特性。因为允许任何有效的 Python 代码,所以您的类可能包含各种方法和其他属性,与框架提供的特性混合在一起。

常见的鸭分型方案

你可能听过这句古老的谚语,“如果它走路像鸭子,说话像鸭子,那它就是鸭子。”莎士比亚在《罗密欧与朱丽叶》中更浪漫地发挥了这一思想,“就算我们把玫瑰叫做别的名字,它闻起来还是一样香。”这里反复出现的主题是,给一个物体起的名字与它的真实性质没有关系。这个想法是,不管标签是什么,你可以通过观察它的行为来合理地确定它是什么。

在 Python 和其他一些语言中,这个概念被扩展到对象类型。它不依赖于某个基类或接口来定义一个对象能做什么,而是简单地实现了按预期行为所必需的属性和方法。Python 中一个常见的例子是类似文件的对象,它是实现了至少一些与 Python 文件对象相同的方法的任何对象。以这种方式,许多库可以返回它们自己的对象,这些对象可以被传递给期望文件对象的其他函数,但同时保留特殊能力,例如只读、压缩、加密、从互联网连接的源拉取或任何数量的其他可能性。

此外,像其他语言中的接口一样,Python 对象一次可以是多种类型的 duck。例如,一个对象在某些方面表现为字典,而在其他方面表现为列表,这种情况并不少见。Django 的HttpResponse对象展示了这两种行为,并模仿了一个打开的文件对象。

在 Django 中,许多特性通过不提供特定的基类来利用 duck 类型。相反,每个特性都定义了一种协议,即一个对象为了正常工作而必须提供的一组方法和属性。这些协议中的许多都在 Django 的官方文档中有所介绍,本书将涵盖更多内容。您还将看到使用这种技术可以提供的一些特殊能力。

接下来的几节描述了一些常见的 Python 协议,这些协议贯穿于整个 Django,实际上贯穿于任何大型 Python 库。

可召回商品

Python 允许从许多来源执行代码,任何能够以与典型函数相同的方式执行的代码都被指定为可调用的。正如所料,所有的函数、类和方法都是可自动调用的,但是通过提供单个方法,任意对象类的实例也可以被指定为可调用的。

call(self[,…])

当实例化的对象被作为函数调用时,将执行该方法。它的工作方式和其他成员函数一样,只是调用方式不同。

类乘数(对象):

...def init(self,factor):

...自身因素=因素

...def call(self,value):

...返回值*自身因素

...

times2 =乘数(2)

时代 2(5)

Ten

times2(10)

Twenty

乘以 3 =乘数(3)

times3(10)

Thirty

Python 还提供了一个内置函数来帮助识别可调用对象。callable()函数采用单个参数,返回TrueFalse,表示该对象是否可以作为函数调用。

类基本(对象):

...及格

...

类可调用(对象):

...def call(self):

...返回“已执行!”

...

b =基本()

可调用(b)

错误的

c = Callable()

可调用(c)

真实的

字典

字典是单个对象中键和值之间的映射。大多数编程语言都有某种形式的字典;其他语言称它们为“散列”、“映射”或“关联数组”除了通过指定一个键来简单地访问值之外,Python 中的字典还提供了许多方法来对底层映射进行更细粒度的操作。为了表现得更像一个真正的字典,一个对象可能会提供其他方法,在 Python 库参考中有记录。 3

__ 包含 _ _(自身,密钥)

in操作符使用,如果指定的键出现在底层映射中,则返回True,否则返回False。这不应该引发异常。

getitem(自我,钥匙)

这将返回指定键引用的值(如果存在)。如果这个键不在底层映射中,它应该引发一个KeyError

setitem(自身,关键字,值)

这将存储指定的值,以便以后由指定的键引用。如果这样的映射已经存在,这将覆盖由同一键引用的任何现有值。

class CaseInsensitiveDict(dict):

...    def init(self, **kwargs):

...对于 key,kwargs.items()中的值:

...self[key.lower()] =值

...def contains(self,key):

...return super(CaseInsensitiveDict,self)。__ 包含 __(key.lower())

...    防御 getitem(自我,密钥):

...return super(CaseInsensitiveDict,self)。getitem(key.lower())

...def setitem(self,key,value):

...super(CaseInsensitiveDict,self)。setitem(key.lower(),value)

...

d = CaseInsensitiveDict(SpAm = ' eggs ')

d 中的“垃圾邮件”

真实的

d[“垃圾邮件”]

鸡蛋

d['垃圾邮件'] = 'burger '

d[“垃圾邮件”]

汉堡

字典也应该是可迭代的,当代码在字典的内容上循环时使用键的列表。有关更多信息,请参考即将到来的“Iterables”部分。

文件

如前所述,文件是访问信息的一种常见方式,许多 Python 库提供了类似文件的对象,用于其他与文件相关的函数。一个类似文件的对象不需要提供下面所有的方法,只需要提供那些正常工作所必需的方法。在文件协议的情况下,对象可以自由地实现读访问和/或写访问。这里没有列出所有的方法,只列出了最常用的方法。Python 标准库文档中提供了文件方法的完整列表,因此请务必查看该文档以了解更多详细信息。 4

阅读(自我,[大小])

这将从对象或其信息源中检索数据。可选的size参数包含要检索的字节数。如果没有此参数,该方法应返回尽可能多的字节(通常是整个文件,如果可用的话,或者可能是网络接口上可用的所有字节)。

写(自己,字符串)

这将指定的str写入对象或其信息源。

关闭(自己)

这将关闭该文件,使其无法再被访问。这可以用来释放任何已分配的内存资源,将对象的内容提交到磁盘,或者只是满足协议。即使此方法不提供特殊功能,也应该提供它以避免不必要的错误。

A VERY LOOSE PROTOCOL

类似文件的对象有很多种,因为这个协议是所有 Python 中定义最宽松的协议之一。从缓冲输出到允许随机访问数据,有相当多的特性在某些情况下是不合适的,因此为这些情况设计的对象通常不会实现相应的方法。例如,Django 的HttpResponse对象,在第七章中描述,只允许顺序写入,所以它不实现read()seek()tell(),当与某些文件操作库一起使用时会导致错误。

在这种情况下,常见的方法是不实现任何不合适的方法,这样试图访问它们就会引发一个AttributeError。在其他情况下,程序员可能认为实现它们更有用,但是简单地抛出一个NotImplementedError来显示更具描述性的消息。只要确保总是记录你的对象遵守了多少协议,这样当用户试图将它们用作标准文件时,尤其是在第三方库中,如果出现这些错误就不会感到惊讶。

可重复的

如果将一个对象传递给内置的iter()返回一个迭代器,那么这个对象被认为是可迭代的。iter()通常被隐式调用,比如在for循环中。所有的列表、元组和字典都是可迭代的,通过定义下面的方法,任何新样式的类都可以成为可迭代的。

iter(self)

这个方法由iter()隐式调用,负责返回一个迭代器,Python 可以用它从对象中检索条目。通过将这个方法定义为一个生成器函数,返回的迭代器通常是隐含的,这将在接下来的“生成器”一节中描述。

斐波那契类(对象):

...def init(self,count):

...self.count =计数

-我...。def _iter(自我):

...a,b = 0,1

...对于范围内的 x(自计数):

...如果 x < 2:

...产量 x

...否则:

...c = a + b

...产量 c

...a,b = b,c

...

对于斐波那契中的 x(5):

...打印(x)

...

Zero

one

one

Two

three

对于斐波那契中的 x(10):

...打印(x)

...

Zero

one

one

Two

three

five

eight

Thirteen

Twenty-one

Thirty-four

迭代程序

当用一个对象调用iter()时,它应该返回一个迭代器,然后这个迭代器可以用来按顺序检索该对象的项目。迭代器是一种简单的单向遍历可用条目的方法,一次只返回一个条目,直到没有更多条目可用。对于大型集合,逐个访问项目比首先将它们全部收集到一个列表中要高效得多。

下一个(自己)

迭代器唯一需要的方法是返回一个条目。如何检索该项将取决于迭代器的设计目的,但它必须只返回一项。在那个项目被称为迭代器的代码处理之后,将再次调用next()来检索下一个项目。

一旦没有更多的项要返回,next()还负责告诉 Python 停止使用迭代器,并在循环后继续前进。这是通过引发StopIteration异常来完成的。Python 将继续调用next(),直到出现异常,导致无限循环。要么使用StopIteration优雅地停止循环,要么使用另一个异常来指示更严重的问题。

FibonacciIterator 类(对象):

def init(self,count):

self.a = 0

self.b = 1

self.count =计数

自电流= 0

def next(自己):

自电流+= 1

如果自身电流>自身计数:

提升停止迭代

如果自电流< 3:

返回自电流- 1

c =自我 a +自我 b

self.a = self

self.b = c

返回 c

下一个= __ 下一个 _ _

def _iter(自我):

因为它已经是一个迭代器,所以它可以返回自身。

回归自我

斐波纳契类(对象):

def init(self,count):

self.count =计数

def _iter(自我):

返回斐波那契迭代器(self.count)

注意迭代器不需要显式定义__iter__()才能正确使用,但是包含该方法允许迭代器直接在循环中使用。

COMPATIBILITY: ITERATORS IN PYTHON 2

Python 3 中的迭代器只有一个非常小的变化。这里显示的__next__()方法以前叫做next()。注意缺失的下划线。这是为了尊重 Python 的习惯,即在方法名的前后使用双下划线来标识像这样的神奇方法。

如果您需要同时支持 Python 2 和 3,解决方案相当简单。在我们的 Fibonacci 示例中定义了__next__()之后,您可以直接将__next__()方法赋给该方法的下一个:next = __next__。这可以在类定义中的任何地方完成,但是最好是在__next__()方法结束之后,以保持整洁。

发电机

如 Fibonacci 示例所示,生成器是创建简单迭代器的便捷方式,无需定义单独的类。Python 使用yield语句将函数标识为生成器,这使得它的行为与其他函数略有不同。

当调用生成器函数时,Python 不会立即执行任何代码。相反,它返回一个迭代器,迭代器的next()方法将调用函数体,直到第一个yield语句出现的地方。给予yield语句的表达式被用作next()方法的返回值,允许任何调用生成器的代码获得一个值来处理。

下一次在迭代器上调用next()时,Python 继续执行生成器函数,所有变量保持不变。只要 Python 遇到yield语句,这个过程就会重复,通常函数会使用一个循环来不断产生值。每当函数结束而没有产生值时,迭代器自动引发StopIteration来指示循环应该结束,剩下的代码可以继续。

顺序

虽然 iterables 只是描述一个一次检索一个值的对象,但是这些值通常都是预先知道的,并被收集在一个对象上。这是一个序列。最常见的类型是列表和元组。作为可迭代的,序列也使用__iter__()方法逐个返回它们的值,但是由于这些值也是预先知道的,所以一些额外的特性是可用的。

len(self)

有了所有可用的值,序列就有了特定的长度,这可以使用内置的len()函数来确定。在幕后,len()检查给定的对象是否有一个__len__()方法,并使用它来获得序列的长度。为了实现这一点,__len__()应该返回一个包含序列中项目数量的整数。

从技术上来说,__len__()并不要求预先知道所有的值,只需要知道有多少。因为不能有部分项目——一个项目要么存在,要么不存在——__len__()应该总是返回一个整数。如果没有,len()无论如何都会将它强制为一个整数。

FibonacciLength 类(Fibonacci):

...def len(self):

...返回自我计数

...

len(fibonacclength(10))

Ten

len(fibonacclength(2048))

Two thousand and forty-eight

getitem(self)和 setitem(self,value)

序列中的所有值都已经排序,因此可以通过序列中的索引来访问单个值。因为用于这种类型访问的语法与字典键的语法相同,所以 Python 重用了前面针对字典描述的两种方法。这允许序列自定义如何访问单个值,或者可能限制为序列设置新值,使其成为只读的。

增强功能

除了标准的声明和调用,Python 还提供了允许您以有趣的方式调用函数的选项。Django 使用这些技术来帮助实现高效的代码重用。您也可以在您的应用中使用这些相同的技术;它们是 Python 的标准部分。

多余的论据

并不总是可能知道在运行时将向函数提供什么参数。在 Django 中经常是这种情况,类方法甚至在子类本身被适当定制之前就在源代码中定义了。另一种常见的情况是函数可以作用于任意数量的对象。在其他情况下,函数调用本身可以成为一种 API,供其他应用使用。

对于这些情况,Python 提供了两种特殊的方法来定义函数参数,这两种方法允许函数接受未由显式声明的参数处理的多余参数。这些“额外的”参数将在下面解释。

注意名字argskwargs仅仅是 Python 的约定。与任何函数参数一样,您可以随意命名它们,但是与标准 Python 习惯用法的一致性使您的代码更容易被其他程序员访问。

位置参数

在参数名称前使用单个星号允许函数接受任意数量的位置参数。

def multiple(* args):

...总计= 1

...对于 args 中的 arg:

...总计*=参数

...退货总额

...

乘法(2,3)

six

乘法(2,3,4,5,6)

Seven hundred and twenty

Python 将参数收集到一个元组中,然后可以作为变量args访问这个元组。如果除了那些显式声明的参数之外,没有提供任何位置参数,则该参数将用一个空元组填充。

关键字参数

Python 在参数名称前使用两个星号来支持任意关键字参数。

def accept(**kwargs):

...对于关键字,kwargs.items()中的值:

...打印(" % s--> % r " %(关键字,值))

...

接受(foo='bar ',spam='eggs ')

foo ->

垃圾邮件->“鸡蛋”

注意kwargs是一个普通的 Python 字典,包含参数名和值。如果没有提供额外的关键字参数,kwargs将是一个空字典。

混合参数类型

任意位置和关键字参数可以与其他标准参数声明一起使用。混合它们需要一些小心,因为它们的顺序对 Python 很重要。参数可以分为四类,虽然不是所有的类别都是必需的,但是它们必须按照下面的顺序定义,跳过任何不使用的类别。

  • 必需的参数
  • 可选参数
  • 多余的位置参数
  • 多余的关键字参数

def complex_function(a,b =无,*c,**d):

这个顺序是必需的,因为*args**kwargs只接收那些不能放在任何其他参数中的值。如果没有这种顺序,当您调用带有位置参数的函数时,Python 将无法确定哪些值用于声明的参数,哪些值应被视为多余的位置参数。

还要注意,虽然函数可以接受任意数量的必需和可选参数,但它们只能定义一种多余的参数类型。

传递参数集合

除了能够接收任意参数集合的函数之外,Python 代码还可以使用前面描述的星号标记来调用具有任意数量参数的函数。以这种方式传递的参数被 Python 扩展成一个普通的参数列表,这样被调用的函数就不需要为了像这样被调用而计划过多的参数。任何 Python 可调用函数都可以使用这种符号来调用,并且可以使用相同的排序规则与标准参数结合使用。

def add(a,b,c):

...返回 a + b + c

...

添加(1,2,3)

six

相加(a=4,b=5,c=6)

Fifteen

args = (2,3)

添加(1,*args)

six

总部,这里是总部

add(a=7, **kwargs)

Twenty-four

add(a=7,*args)

追溯(最近一次通话持续时间):

...

TypeError: add()获得了关键字参数“a”的多个值

相加(1,2,a=7)

追溯(最近一次通话持续时间):

...

TypeError: add()获得了关键字参数“a”的多个值

如本例最后几行所示,如果显式传递任何关键字参数,同时还传递一个元组作为多余的位置参数,要特别小心。因为 Python 将使用排序规则扩展多余的参数,所以位置参数将首先出现。在这个例子中,最后两个调用是相同的,Python 不能确定哪个值用于a

装饰者

另一种改变函数行为方式的常见方法是用另一个函数“装饰”它。这也经常被称为“包装”一个函数,因为 decorators 被设计成在原始函数被调用之前或之后执行额外的代码。

decorators 背后的关键原则是,它们接受可调用内容并返回新的可调用内容。装饰器返回的函数是稍后调用被装饰的函数时要执行的函数。必须小心确保原始函数不会在这个过程中丢失,因为没有任何方法可以在不重新加载模块的情况下恢复它。

装饰器可以以多种方式应用,要么应用于您直接定义的函数,要么应用于在其他地方定义的函数。从 Python 2.4 开始,新定义的函数上的 decorators 可以使用特殊的语法。在 Python 的以前版本中,需要稍微不同的语法,但是在两种情况下可以使用相同的代码;唯一的区别是用于将装饰器应用到预期函数的语法。

def 装修(func):

...打印('装饰%s ... '% func。name)

...def wrapped(*args,**kwargs):

...print("调用带参数的包装函数:",args)

-我...。return func(*args、**kwargs)

...打印('完成!')

...包装退货

...

Python 2.4 及更高版本的语法

@装修

...定义测试(a,b):

...返回 a + b

...

装饰试验...

搞定了。

测试(13,72)

调用带参数的包装函数:(13,72)

eighty-five

Python 2.3 的语法

def 测试(a,b):

...返回 a + b

...

测试=装修(测试)

装饰试验...

搞定了。

测试(13,72)

调用带参数的包装函数:(13,72)

eighty-five

本例中较老的语法是修饰函数的另一种技术,可以在@语法不可用的情况下使用。考虑一个已经在其他地方声明过的函数,但它会从修饰中受益。这样一个函数可以传递给一个装饰器,装饰器然后返回一个新的函数,所有的东西都包装好了。使用这种技术,任何可调用的程序,不管它来自哪里或做什么,都可以被包装在任何装饰器中。

用额外的参数装饰

有时候,装饰器需要额外的信息来决定它应该对收到的函数做什么。使用旧的修饰语法,或者在修饰任意函数时,这个任务相当容易执行。只需声明装饰器来接受所需信息的附加参数,这样它们就可以和要包装的函数一起提供。

def 测试(a,b):

...返回 a + b

...

def 装饰(func,prefix='Decorated '):

...def wrapped(*args,**kwargs):

...返回' %s: %s' %(前缀,func(*args,**kwargs))

...包装退货

...

简单=装修(测试)

定制=装饰(测试,前缀= '定制')

简单(30,5)

装饰:35 英尺

定制(27,15)

定制:42 英尺

然而,Python 2.4 装饰器语法使事情变得复杂了。当使用这种新语法时,装饰器总是只接收一个参数:要包装的函数。有一种方法可以将额外的参数引入 decorators,但是首先我们需要离题一点,谈谈“部分”

函数的部分应用

通常,在执行函数时,调用函数时会使用所有必需的参数。然而,有时参数可能在函数被调用之前很久就已经知道了。在这些情况下,一个函数可以预先应用一个或多个参数,这样就可以用较少的参数调用该函数。

为此,Python 2.5 将partial对象作为其functools模块的一部分。它接受一个 callable 和任意数量的附加参数,并返回一个新的 callable,它的行为就像原来的 callable 一样,只是不需要在以后指定那些预先加载的参数。

导入功能工具

def add(a,b):

...返回 a + b

...

添加(4,2)

six

plus3 = functools.partial(add,3)

plus5 = functools.partial(add,5)

plus3(4)

seven

plus3(7)

Ten

plus5(10)

Fifteen

对于 than 2.5 之前的版本,Django 在位于django.utils.functionalcurry函数中提供了自己的partial实现。该函数适用于 Python 2.3 及更高版本。

回到装饰者的问题

如前所述,如果使用 Python 2.4 语法的 decorators 接受额外的参数,就会出现问题,因为该语法本身只提供一个参数。使用局部应用技术,甚至可以在装饰器上预加载参数。给定前面描述的装饰器,下面的例子使用curry(在第九章中描述)为使用较新的 Python 2.4 语法的装饰器提供参数。

来自 django . utils . functional import curry

@curry(装饰,前缀='Curried ')

...定义测试(a,b):

...返回 a + b

...

测试(30,5)

咖喱:35 英尺

测试(27,15)

咖喱:42 英尺

这仍然很不方便,因为每次使用这个函数来修饰另一个函数时,都需要运行这个函数。更好的方法是在装饰器本身中直接提供这个功能。这需要装饰者编写一些额外的代码,但是包含这些代码会使它更容易使用。

诀窍是在另一个函数中定义装饰器,该函数将接受参数。这个新的外部函数然后返回装饰器,Python 的标准装饰器处理使用这个装饰器。装饰器反过来返回一个函数,该函数将在装饰过程完成后被程序的其余部分使用。

由于这些都相当抽象,考虑下面的例子,它提供了与前面的例子相同的功能,但是不依赖于curry,使得处理起来更容易。

def decorate(前缀='Decorated '):

...#此处传入的前缀将是

...#可用于所有内部函数

...定义装饰器(函数):

...#这是用 func 作为

...#被修饰的实际函数

-我...。def wrapper(*args、**kwargs):

...#每次都会调用这个函数

...#执行真正的功能

...返回' %s: %s' %(前缀,func(*args,**kwargs))

...#发送包装的函数

...返回包装

...#提供供 Python 使用的装饰器

...返回装饰者

...

@装饰('轻松')

...定义测试(a,b):

...返回 a + b

...

测试(13,17)

简单:30 分钟

测试(89,121)

简单:210 英尺

这种技术在需要参数的情况下最有意义。如果在没有任何参数的情况下应用装饰器,为了让它正常工作,括号仍然是必需的。

@装饰()

...定义测试(a,b):

...返回 a + b

...

测试(13,17)

装饰:30 英尺

测试(89,121)

装饰:210 英尺

@装修

...定义测试(a,b):

...返回 a + b

...

测试(13,17)

追溯(最近一次通话持续时间):

...

TypeError: decorator()只接受 1 个参数(给定了 2 个)

第二个例子失败了,因为我们没有首先调用decorate。因此,所有对test的后续调用都将其参数发送到decorator而不是test。由于这是一个不匹配,Python 抛出一个错误。这种情况可能有点难以调试,因为引发的确切异常将取决于被包装的函数。

有或没有争论的装饰者

装饰器的另一个选择是提供一个单一的装饰器,它可以在前面两种情况下工作:有参数和没有参数。这个比较复杂,但是值得探讨。

目标是允许带参数或不带参数调用装饰器,所以假设所有参数都是可选的是安全的;任何带有必需参数的装饰器都不能使用这种技术。记住这一点,基本思想是在列表的开头添加一个额外的可选参数,它将接收要修饰的函数。然后,装饰器结构包括必要的逻辑来确定它是被调用来添加参数还是装饰目标函数。

def 装饰(func=None,prefix='Decorated '):

...装饰定义(功能):

...#这将返回最终的修饰

...#函数,不管它是如何被调用的

-我...。def wrapper(*args、**kwargs):

...返回' %s: %s' %(前缀,func(*args,**kwargs))

...返回包装

...如果 func 为 None:

...#用参数调用了装饰器

...定义装饰器(函数):

...返回装饰(功能)

...返回装饰者

...#没有参数就调用了装饰器

...返回装饰(功能)

...

@装修

...定义测试(a,b):

...返回 a + b

...

测试(13,17)

装饰:30 英尺

@修饰(前缀=“参数”)

...定义测试(a,b):

...返回 a + b

...

测试(13,17)

参数:30 '

这要求传递给装饰器的所有参数都作为关键字参数传递,这通常有助于提高代码的可读性。一个缺点是,对于使用这种方法的每个装饰者来说,需要重复多少样板文件。

幸运的是,像 Python 中的大多数样板文件一样,可以将它分解成可重用的形式,因此可以使用另一个装饰器更容易地定义新的装饰器。以下函数可用于修饰其他函数,提供接受参数所需的所有功能,也可以不使用参数。

def optional _ arguments _ decorator(real _ decorator):

...def decorator(func=None,**kwargs):

...#这是未来的装潢师

...#暴露于程序的其余部分

...装饰定义(功能):

...#这将返回最终的修饰

...#函数,不管它是如何被调用的

...定义包装(*a,**kw):

...return real_decorator(func,a,kw,**kwargs)

...返回包装

...如果 func 为 None:

...#用参数调用了装饰器

...定义装饰器(函数):

...返回装饰(功能)

...返回装饰者

...#没有参数就调用了装饰器

...返回装饰(功能)

...返回装饰者

...

@optional_arguments_decorator

...def decorate(func,args,kwargs,prefix='Decorated '):

...返回' %s: %s' %(前缀,func(*args,**kwargs))

...

@装修

...定义测试(a,b):

...返回 a + b

...

测试(13,17)

装饰:30 英尺

test = decorate(test,prefix='Decorated again ')

测试(13,17)

再次装饰:装饰:30 '

这使得单个装饰者的定义更加简单明了。结果装饰器的行为与上一个例子完全一样,但是它可以带参数使用,也可以不带参数使用。这项新技术需要的最显著的变化是,被定义的真正的装饰器将接收以下三个值:

  • func—使用新生成的装饰器装饰的函数
  • args—包含传递给函数的位置参数的元组
  • kwargs—包含传递给函数的关键字参数的字典

然而,要认识到的一件重要的事情是,装饰器接收的argskwargs是作为位置参数传递的,没有通常的星号符号。然后,当将它们传递给包装的函数时,必须使用星号符号来确保函数接收到它们,而不必知道装饰器是如何工作的。

描述符

通常,引用对象上的属性会直接访问属性值,没有任何复杂性。获取和设置属性会直接影响对象的实例命名空间中的值。有时,在访问这些值时必须做额外的工作。

  • 从复杂的源中检索数据,如数据库或配置文件
  • 将简单的值转换为复杂的对象或数据结构
  • 自定义它所附着的对象的值
  • 在保存到数据库之前,将值转换为可存储的格式

在一些编程语言中,通过创建额外的实例方法来访问那些需要它的属性,这种类型的行为成为可能。虽然这种方法很实用,但会导致一些问题。首先,这些行为通常更多地与属性中存储的数据类型相关联,而不是与它所附加到的实例的某些方面相关联。通过要求对象提供访问此数据的附加方法,包含此行为的每个对象都必须在其实例方法中提供必要的代码。

另一个重要问题是,当一个曾经简单的属性突然需要这种更高级的行为时会发生什么。当从简单属性更改为方法时,对该属性的所有引用也需要更改。为了避免这种情况,这些语言的程序员采用了一种标准的做法,即总是为属性访问创建方法,这样对底层实现的任何更改都不会影响任何现有的代码。

为了改变一个属性的访问方式而修改那么多代码并不有趣,所以 Python 提供了一种不同的方法来解决这个问题。与其要求对象负责对其属性的特殊访问,不如由属性本身来提供这种行为。描述符是一种特殊类型的对象,当它附加到一个类时,可以在访问属性时进行干预,提供任何必要的附加行为。

导入日期时间

class CurrentDate(object):

...def get(自身,实例,所有者):

...返回 datetime.date.today()

...def set(自身,实例,值):

...引发 NotImplementedError("不能更改当前日期。")

...

类示例(对象):

-我...。date = CurrentDate()

...

e = Example()

电子日期

datetime.date(2008, 11, 24)

e.date = datetime.date.today()

回溯(最近一次呼叫):

...

notimplemontederror:无法更改当前日期。

创建描述符就像创建一个标准的新型类一样简单(通过继承 Python 2.x 下的object),并至少指定以下方法之一。descriptor 类可以包含执行其负责的任务所需的任何其他属性或方法,而下面的方法构成了一种支持这种特殊行为的协议。

get(自身,实例,所有者)

当检索一个属性的值(value = obj.attr)时,这个方法将被调用,允许描述符在返回值之前做一些额外的工作。除了通常的代表描述符对象的self之外,这个 getter 方法还接收两个参数。

  • instance—包含被引用属性的实例对象。如果属性被作为一个类的属性而不是一个实例引用,这将是None
  • owner—分配了描述符的类。这将始终是一个类对象。

instance参数可以用来确定描述符是从一个对象还是它的类中访问的。如果instanceNone,那么属性是从类而不是从实例中访问的。如果描述符被以不应该的方式访问,这可以用来引发一个异常。

此外,通过定义此方法,您可以让描述符负责检索一个值并将其返回给请求它的代码。否则将迫使 Python 返回其默认返回值None

注意,默认情况下,描述符不知道在声明为属性时它们被赋予了什么名称。Django 模型提供了一种绕过这个问题的方法,这在第三章中有描述,但是除此之外,描述符只知道它们的数据,不知道它们的名字。

set(自身,实例,值)

当给描述符(obj.attr = value)设置一个值时,这个方法被调用,这样一个更专门化的过程可以发生。像__get__一样,除了标准的self之外,这个方法还接收两个参数。

  • instance—包含被引用属性的实例对象。这绝对不会是None
  • value—正在分配的值。

还要注意的是,描述符的__set__方法只有在对象上分配属性时才会被调用,而在第一次分配描述符的类上分配属性时永远不会被调用。这种行为是有意设计的,它禁止描述符完全控制其访问。外部代码仍然可以通过向首次赋值的类赋值来替换描述符。

还要注意来自__set__的返回值是不相关的。该方法本身只负责适当地存储所提供的值。

跟踪实例数据

因为描述符会缩短属性访问,所以在附加对象上设置值时需要小心。不能简单的用setattr在对象上设置值;尝试这样做将再次调用描述符,导致无限递归。

Python 提供了另一种访问对象名称空间的方法:__dict__属性。在所有 Python 对象上都可用,__dict__是一个表示对象名称空间中所有值的字典。直接访问这个字典绕过了 Python 关于属性的所有标准处理,包括描述符。利用这一点,描述符可以在不触发自身的情况下设置对象的值。考虑下面的例子。

类描述符(对象):

...def init(self,name):

...self.name = name

...def get(自身,实例,所有者):

...返回实例。dict[self.name]

...def set(自身,实例,值):

...实例。dict[self.name] = value

...

类 TestObject(对象):

...attr =描述符(' attr ')

...

检验 = 测试对象()

test.attr = 6

test.attr

six

不幸的是,这种技术需要给描述符显式地给出属性的名称。您可以用一些元类技巧来解决这个问题;Django 的模型系统(在第三章中讨论)展示了一种可能的解决方法。

反省

许多 Python 对象在它们执行的代码之外携带元数据。这些信息在使用框架或编写自己的框架时非常有用。

当试图开发可重用的应用时,Python 的自省工具可以提供很大的帮助,因为它们允许 Python 代码检索关于程序员所写内容的信息,而不需要程序员重新编写。

本节描述的一些特性依赖于一个强大的标准库模块inspectinspect模块提供了方便的功能来执行高级自检。

这里只详细介绍inspect的一些用途,因为它们对使用 Django 编写的应用最有价值。有关本模块中许多其他可用选项的完整详细信息,请参考 Python 标准库文档。 5

MORE ON OLD-STYLE CLASSES

本节中展示的例子都是针对新型类的,正如本章前面所描述的,新型类的行为不同于旧式类,特别是在内省方面。确切的区别超出了本书的范围,因为通常的建议是简单地使用新型类。

如果您的任何代码看起来与这里描述的行为不同,请确保您的所有类都从object继承,这将使它们成为适当的新型类。

常见的类和函数属性

所有的类和函数都提供了一些可以用来识别它们的公共属性。

  • __name__—用于声明类或函数的名称
  • __doc__—为函数声明的文档字符串
  • __module__—声明类或函数的模块的导入路径

此外,所有对象都包含一个特殊属性__class__,它是用于创建对象的实际类对象。该属性可用于多种目的,例如测试该类是否提供了特定的属性,或者是否在对象本身上设置了该属性。

class ValueClass(object):

...source = ' class '

...

value_instance = ValueClass()

value_instance.source = '实例'

值 _ 实例。class

值实例.来源

'实例'

值 _ 实例。__ 类 _ _。来源

“班级”

识别对象类型

因为 Python 使用动态类型,所以任何变量都可以是任何可用类型的对象。虽然 duck typing 的一般原则建议简单地测试对象对特定协议的支持,但是识别您正在处理的对象的类型通常是有用的。有几种方法可以解决这个问题。

获取任意对象类型

使用前面描述的内置type很容易确定任何 Python 对象的类型。用单个参数调用type将返回一个类型对象,通常是一个类,它被实例化以产生对象。

类型('这是一个字符串')

类型(42)

类 TestClass(object):

...及格

...

型(测试类)

obj = TestClass()

类型(对象)

这种方法通常不是确定对象类型的最佳方式,尤其是当您试图根据对象的类型来决定执行的分支时。它只告诉你正在使用的一个特定的类,即使子类可能被考虑用于同一个执行分支。相反,这种方法应该用在对象的类型不是决策所必需的,而是输出到某个地方的情况下,也许是输出到用户的日志文件中。

例如,在报告异常时,包含异常的类型及其值非常有用。在这些情况下,type可以用来返回类对象,然后它的__name__属性可以包含在日志中,很容易识别异常的类型。

检查特定类型

更常见的是,您需要检查特定类型的影响,一个类是否是它的后代,或者一个对象是否是它的实例。这是一个比使用type更健壮的解决方案,因为它在决定成功或失败时考虑了类继承。

Python 为此提供了两个内置函数。

  • issubclass(cls, base)—如果clsbase相同,或者如果cls继承了base的某个祖先,则返回True
  • isinstance(obj, base)—测试对象是否是base的实例或其任何祖先

class CustomDict(dict):

...pass #假装这里有更有用的东西

...

issubclass(CustomDict,Dict)

真实的

issubclass(CustomDict,CustomDict)

真实的

my_dict = CustomDict()

实例(my_dict,dict)

真实的

事件(my_dict,CustomDict)

真实的

issubclassisinstance有明确的关系:isinstance(obj, SomeClass)相当于issubclass(obj.__class__, SomeClass)

功能签名

正如本章前面所描述的,Python 函数可以用多种方式声明,直接在代码中访问关于它们声明的信息是非常有用的。

在检查函数时特别重要的是inspect.getargspec(),这个函数返回关于函数接受什么参数的信息。它接受单个参数,即要检查的函数对象,并返回以下值的元组:

  • args—为函数指定的所有参数名称的列表。如果函数不接受任何参数,这将是一个空列表。
  • varargs—用于多余位置参数的变量的名称,如前所述。如果函数不接受多余的位置参数,这将是None
  • varkwargs—用于多余关键字参数的变量的名称,如前所述。如果函数不接受多余的关键字参数,这将是None
  • defaults—为函数参数指定的所有默认值的元组。如果没有参数指定默认值,那么这个值将是None而不是一个空元组。

总之,这些值代表了知道如何以任何可能的方式调用函数所必需的一切。当接收一个函数并用适合它的参数调用它时,这是很有用的。

def 测试(a,b,c =真,d =假,*e,**f):

...及格

...

进口检验

inspect.getargspec(测试)

ArgSpec(args=['a ',' b ',' c ',' d'],varargs='e ',keywords='f ',defaults=(True,False))

处理默认值

正如前面的例子所说明的,默认值是在一个独立于参数名的列表中返回的,因此如何区分哪些参数指定了哪些默认值似乎并不明显。然而,有一种相对简单的方法来处理这种情况,这是基于前面关于多余参数的讨论中的一个小细节:必需参数必须总是在可选参数之前声明。

这很关键,因为这意味着参数及其默认值是按照它们在函数中声明的顺序指定的。所以在前面的例子中,有两个默认值的事实意味着最后两个参数是可选的,默认值按顺序排列。下面的代码可以用来创建一个字典,将可选参数名映射到为它们声明的默认值。

def get_defaults(func):

-我...。args、varargs、varkwargs、defaults = inspection . getagspec(func)

...index = len(args) - len(defaults) #第一个可选参数的索引

...return dict(zip(args[index:],默认值))

...

get_defaults(测试)

{'c ':真,' d ':假}

文档字符串

如前所述,类和函数都有一个特殊的__doc__属性,它包含被指定为代码的 docstring 的实际字符串。不幸的是,它的格式与原始源文件完全一样,包括额外的换行符和不必要的缩进。

为了以更可读的方式格式化文档字符串,Python 的inspect模块提供了另一个有用的函数getdoc()。它删除了不必要的换行符,以及任何额外的缩进,这是编写 docstring 的副作用。

缩进的去除值得稍微解释一下。实际上,getdoc()查找字符串中最左边的非空白字符,计算该字符和它所在的行首之间的所有空白,并从 docstring 中的所有其他行中删除该数量的空白。这样,得到的字符串是左对齐的,但保留了为了格式化文档而存在的任何附加缩进。

定义 func(arg):

..."""

...对参数执行函数并返回结果。

...

...银

...要处理的参数

..."""

...及格

...

打印(func。doc)

对参数执行函数并返回结果。

要处理的参数

print(inspect.getdoc(func))

对参数执行函数并返回结果。

要处理的参数

在需要向用户显示文档字符串的情况下,比如自动化文档或帮助系统,getdoc()为原始文档字符串提供了一个有用的替代品。

应用技术

有无数种 Python 特性的组合可以用来完成大量的任务,因此这里展示的几种决不能被认为是通过组合 Python 的许多特性可以完成的工作的详尽列表。然而,就 Django 而言,这些都是有用的策略,并为本书中列出的其他技术提供了坚实的基础。

跟踪子类

考虑一个应用,它必须在任何给定的时间访问一个特定类的所有子类的列表。元类是一种很好的方式,但是它们有一个问题。记住,每个带有metaclass选项的类都会被处理,包括这个新的基类,它不需要注册(只需要注册它的子类)。这需要一些额外的处理,但是很简单:

class SubclassTracker(类型):

-我...。def _ _ init _(cls、名称、基础、属性):

...尝试:

...如果 TrackedClass 不在基中:

...返回

...除了名称错误:

...返回

-我...。TrackedClass(跟踪类)。_registry.append(cls)

...

class tracked class(meta class = subclass tracker)

..._registry = []

...

类类跟踪类:

...及格

...

trackedclass。_ registry-登录

[ ]

类 2:

...及格

...

trackedclass。_ registry-登录

元类执行两个功能。首先,try块确保父类TrackedClass已经被定义。如果还没有,就会引发一个NameError,表明元类当前正在处理TrackedClass本身。这里,可以为TrackedClass做更多的处理,但是这个例子简单地忽略了它,允许它绕过注册。

此外,if子句确保另一个类没有将SubclassTracker明确指定为它的metaclass选项。应用只想注册TrackedClass的子类,而不是其他可能不符合应用要求的类。

任何想要使用类似 Django 的声明性语法的应用作者都可以使用这种技术来提供一个公共基类,从这个基类可以创建特定的类。Django 的模型和表单都使用了这个过程,因此它的声明性语法在整个框架中是相当一致的。

如果 Python 通过了这些测试而没有提前退出,那么该类将被添加到注册表中,在注册表中可以随时检索到TrackedClass的所有子类。任何TrackedClass的子类都会出现在这个注册表中,不管子类是在哪里定义的。执行类定义就足以注册它;这样,应用可以导入任何可能包含必要类的模块,元类完成剩下的工作。

虽然它的注册表提供了比简单列表更多的特性,但是 Django 使用了这种技术的扩展来注册模型,因为它们必须各自扩展一个公共基类。

一个简单的插件架构

在可重用的应用中,通常希望有一组定义良好的核心特性,并能够通过使用插件来扩展这些特性。虽然这可能看起来是一个可能需要大量插件架构库的高要求,但它可以非常简单地完全在您自己的代码中完成。毕竟,一个成功的、松散耦合的插件架构可以归结为提供三样东西:

  • 一种清晰、易读的方式来声明一个插件,并使它对需要使用它的代码可用
  • 访问所有已声明插件的简单方法
  • 一种在插件和使用它们的代码之间定义一个中间点的方法,在这里插件应该被注册和访问

有了这个简单的需求列表和对 Python 必须提供的内容的正确理解,几行简单的代码就可以满足这些需求。

类别插件数量(类型):

def _ _ init _(cls、名称、基础、属性):

如果没有 hasattr(cls,' plugins '):

此分支仅在处理挂载点本身时执行。

因此,由于这是一个新的插件类型,而不是一个实现,这

class 不应注册为插件。相反,它设置了一个

列出以后可以注册插件的地方。

cls 插件= []

否则:

这必须是一个插件实现,应该注册。

只需将它添加到列表中即可

以后再追踪它。

cls.plugins.append(cls)

这就是让整个事情工作的全部,跟踪注册的插件并将它们存储在一个列表中的plugins属性上。剩下的工作就是找出如何实现前面列出的每一点。对于下面的例子,我们将创建一个应用来验证用户密码的强度。

第一步是中间访问点,我称之为挂载点,等式的每一端都可以从这里访问另一端。如前所述,这依赖于元类,所以这是一个很好的起点。

类 password validator(meta class = plugin mount):

"""

扩展这个类的插件将被用来验证密码。

有效的插件必须提供以下方法。

验证(自我、密码)

接收要测试的密码,然后静默完成或引发

如果密码无效,则返回 ValueError。可能会显示异常

所以要确保它充分地描述了问题所在。

"""

如果您愿意,您可以添加更多的内容,但是这里的内容是使流程正常工作所必需的唯一部分。当你想给它添加更多的东西时,只需要知道单个插件会继承它的子类,从而继承你在这个类上定义的任何东西。这是一种提供附加属性或辅助方法的便捷方式,对所有插件都有用。无论如何,单独的插件可以覆盖它们,所以没有什么是一成不变的。

还要注意,插件挂载点应该包含与插件预期行为相关的文档。虽然这不是明确要求的,但这是一个很好的实践,因为这样做会让其他人更容易实现插件。只有当所有注册的插件都符合指定的协议时,系统才能工作;确保它被指定。

接下来,设置您的代码来访问任何已注册的插件,以对应用有意义的任何方式使用它们。因为挂载点已经维护了它自己的已知插件列表,所以它所要做的就是遍历插件,并使用任何适合手头任务的属性或方法。

def is_valid_password(密码):

"""

如果密码正确,则返回 True,如果有问题,则返回 False。

"""

对于 PasswordValidator.plugins 中的插件:

尝试:

插件()。验证(密码)

除了值错误:

返回 False

返回 True

def get_password_errors(密码):

"""

返回一个消息列表,指出发现的任何问题

有了密码。如果没问题,这将返回一个空列表。

"""

错误= []

对于 PasswordValidator.plugins 中的插件:

尝试:

插件()。验证(密码)

除了值错误为 e:

errors.append(str(e))

返回错误

这些例子比大多数要复杂一些,因为它们需要错误处理,但是这仍然是一个非常简单的过程。简单地遍历列表将提供每个插件供使用。剩下的就是构建一些插件来提供这种验证行为。

class minimum length(password validator):

定义验证(自我,密码):

"如果密码太短,将引发 ValueError . "

如果 len(密码)< 6:

提高值错误('密码必须至少有 6 个字符。)

class special characters(password validator):

定义验证(自我,密码):

"如果密码不包含任何特殊字符,将引发 ValueError . "

if password.isalnum():

raise ValueError('密码必须至少包含一个特殊字符。)

是的,真的很简单!下面是这些插件在实践中的样子。

for password in ('pass ',' password ',' p@ssword!)):

...打印(('正在检查%r ... '% password),end= ' ')

...如果是 _ 有效 _ 密码(password):

...打印('有效!')

...否则:

...print() #强制换行

...对于 get_password_errors(密码)中的错误:

...打印(' %s' %错误)

...

检查“通过”...

密码必须至少包含 6 个字符。

密码必须包含至少一个特殊字符。

正在检查“密码”...

密码必须包含至少一个特殊字符。

正在检查“p@ssword!”...有效!

现在怎么办?

对 Python 必须提供的东西有了坚实的理解之后,您就可以深入了解 Django 如何使用这些工具来实现它的许多特性,以及如何在自己的代码中应用相同的技术。模型构成了大多数 Django 应用的基础,利用了许多这些高级 Python 特性。

Footnotes 1

1 http://prodjango.com/new-style-classes/

2

2 http://prodjango.com/metaprogramming/

3

3 http://prodjango.com/dict-methods/

4

4 http://prodjango.com/file-methods/

5

5 http://prodjango.com/inspect-module/

三、模型

Abstract

数据是大多数现代 Web 应用的中心,Django 旨在为各种数据结构和持久性选项提供支持。模型是 Django 使用的传统 MVC 模型的主要方面。对于任何需要跨多个请求、会话甚至服务器实例持久化数据的应用来说,模型都是必不可少的一部分。

数据是大多数现代 Web 应用的中心,Django 旨在为各种数据结构和持久性选项提供支持。模型是 Django 使用的传统 MVC 模型的主要方面。对于任何需要跨多个请求、会话甚至服务器实例持久化数据的应用来说,模型都是必不可少的一部分。

Django 模型被定义为标准的 Python 类,并自动添加了大量的附加特性。在幕后,对象关系映射器(ORM)允许这些类及其实例访问数据库。如果没有这个 ORM,开发人员将需要使用结构化查询语言(SQL)直接处理数据库,这是访问数据库内容的标准方式。

SQL 的主要目标是描述和访问存储在关系数据库中的关系。SQL 一般不为应用提供高层关系,因此大多数应用都包含用于数据活动的手写 SQL。这肯定是可能的,但是它倾向于导致大量的重复,这本身就违反了第一章概述的 DRY 原则。

散布在应用代码中的这些 SQL 很快变得难以管理,尤其是因为必须管理代码的程序员通常不是关系数据库方面的专家。这也意味着这些数据库很容易出现错误,通常很难追踪和修复。

这仍然没有考虑到最大的问题:安全性。SQL 注入 1 攻击是恶意攻击者访问甚至修改他们不应该访问的数据的常见方式。当手写的 SQL 没有对传递到数据库中的值采取适当的预防措施时,就会发生这种情况。手工编写的 SQL 语句越多,就越容易受到这种类型的攻击。

不管使用何种语言,所有这些问题在 Web 开发中都非常普遍,ORM 是框架减轻这些问题的一种常见方式。有其他方法可以避免这些问题,比如 SQL 注入,但是 Django 的 ORM 是在考虑到这些问题的情况下编写的,并且在幕后处理了大部分问题。通过使用标准 Python 对象访问数据,SQL 的数量被最小化,从而减少了出现问题的机会。

Django 如何处理模型类

在第二章中描述过,Django 最著名的特性之一是其模型定义的声明性语法。这样,模型定义可以简单明了,同时仍然提供大量的功能。在第二章中详细描述了将元类用于声明性语法的基本过程,但是在处理模型时还有更多具体的步骤,值得特别注意。

负责处理模型定义的元类是ModelBase,住在django.db.models.base。这提供了一些关键功能,这里按照执行操作的顺序列出。

A new class is generated to be used for the actual model, preserving the module location where the original model was defined.   If a custom app_label wasn’t provided for the model, it’s determined based on the module where it was declared.   Meta options are pulled out of the model and placed in a special Options object, which is described in more detail later in this chapter.   Two special exception classes, DoesNotExist and MultipleObjectsReturned, are created and customized for the new model.   A default manager is assigned to the model if one wasn’t already provided.   If the model was already defined—which can happen because of differences in how the module was imported at different stages—the existing model is retrieved from the application cache and returned, making sure that the same class object is always used.   Attributes and methods defined on the original model are added to the newly-created model class.   Settings from inherited parent models are set on the new model.   The new model is registered with the application cache for future reference.   The newly-created model is returned to be used in place of the class that was defined in the source file.

抽象模型和继承模型是特例,在这种情况下,并非所有这些行为都会发生。这些情况的具体区别将在本章后面介绍。

在模型上设置属性

Python 提供了有用的工具来获取和设置对象的属性,而不需要事先知道名称,但是虽然getattr()setattr()代表了访问对象属性的标准方式,但是 Django 的一个模型字段钩子需要一些额外的处理。Django 在其所有模型上提供了一个类方法add_to_class(),它应该被用作setattr()的替代品。

add_to_class()的语法和语义与传统函数略有不同。它实际上是一个类方法,而不是内置的甚至是模块级的函数,这意味着类是隐式提供的,而不是显式的第一个参数。这个方法检查提供的值是否存在一个contribute_to_class()方法,如果存在就调用它。否则,将使用标准的setattr()函数向模型添加值。这些行为是相互排斥的;在给定的add_to_class()调用中只会发生一次。认识到这不仅仅是 Django 自己的内部代码,这一点很重要。如果应用需要添加任意对象作为模型的属性,它们必须调用add_to_class()。这样,使用该应用的开发人员可以传入任何对象,并确保它会像直接应用于模型的类定义一样被处理。

当使用第二章中描述的自省技术时,整个过程改变了类的外观。为了确定声明的字段、使用的数据库表或模型的显示名称,需要一些额外的知识。

获取关于模型的信息

一旦模型和 Django 的ModelBase元类一起被 Python 处理,它的原始结构仍然可以通过使用存在于每个 Django 模型及其实例上的属性_meta来确定。

_meta上有许多可用的属性,它们组合在一起描述模型,它是如何被定义的,以及提供了什么值来定制它的行为。这些也可以分为两个独立的组:通过查看原始类的实际结构来确定的属性,以及那些被直接指定为模型中定义的Meta类的一部分的属性。

REGARDING THE STABILITY OF _META

以下划线开头的名字通常是指不应该直接使用的私有属性。它们通常由本质上更加公开的函数和方法在内部使用,并且通常伴随着关于可能的更改和未记录的行为的警告。在大多数情况下,这些警告是有效的;程序员通常编写供他们自己使用的工具,发现没有必要记录他们的行为或保证他们的寿命。

然而,_meta是这个规则的一个例外。虽然它确实是私有 API 的一部分,但在大多数情况下并不是必需的,它与本书中描述的许多工具有一些共同之处;如果理解和正确使用,它可以证明是非常有用的。事实上,_meta做得更好,因为它非常稳定,如果不花大力气保持向后兼容,就很难改变。它是 Django 许多内部代码的基础,并且已经被许多第三方应用直接访问。

因此,虽然以下划线开头的名字通常意味着危险、潜在的不兼容性和缺乏支持,但你可以非常安全地依赖_meta。只要确保跟上 Django 的向后不兼容变更列表就行了。任何会打破_meta的新东西都会列在那里。

班级信息

虽然第二章中涉及的大多数基本内省技术都适用于 Django 模型,但是还有一些细节也可以在_meta属性中找到。其中大部分是 Django 本身需要的信息,以便正确地处理模型,但是和许多其他特性一样,它对其他应用也非常有用。

模型的一个重要区别是它们是否被“安装”。这意味着检查包含它们的应用是否在站点的INSTALLED_APPS设置中列出。Django 的许多特性,比如syncdb和内置的管理界面,需要在INSTALLED_APPS中列出一个应用才能被定位和使用。

如果一个应用被设计成直接接受任何 Django 模型,而不是通过INSTALLED_APPS迭代,它通常需要某种方法来确定模型是否被正确安装。如果应用需要不同地处理模型,这是必要的,例如,取决于数据库操作是否应该在表上执行。为此,Django 提供了installed属性,只有当模型属于INSTALLED_APPS中列出的应用时,该属性才是True,否则就是False

模型级信息的另外两个属性通常对应用开发人员很有用。正如在《??》第二章中所描述的,所有的 Python 类都提供了一种简单的方法来获得类名和定义它的模块名,分别使用__name____module__属性。但是,在某些情况下,这些信息可能会产生误导。

考虑这样一种情况,一个模型可能被子类化而没有继承所有特定于 Django 的模型继承处理。这需要对元类进行一些调整,但是对于解决某些类型的问题来说是有用的。当这样做时,__name____module__属性将引用子类,而不是位于其下的实际模型。

通常,这是所期望的行为,因为这就是标准 Python 的工作方式,但是当试图与 Django 模型或者可能需要使用它的 Django 的其他领域进行交互时,可能有必要了解模型本身的细节,而不是子类。实现这一点的一种方法是使用类自省来获取正在使用的各种父类,检查每个父类,看它是否是 Django 模型。

这是一个相当难看的过程,需要时间编码,时间执行,使维护和可读性更加困难,如果需要经常做,还会增加样板文件。幸运的是,Django 在_meta上提供了两个额外的属性来大大简化这个过程。module_name属性包含来自底层模型的__module__属性,而object_name属于模型的__name__属性。

字段定义

使用和操作 Django 模型的一个主要挑战是定位和使用为它们定义的字段的过程。Django 使用在第二章的中描述的creation_counter技术来跟踪字段的顺序,因此它们可以被放在一个列表中以备将来参考。这个列表存储在模型的_meta属性的fields属性中。

作为一个列表,它可以被迭代以按顺序检索所有的字段对象,这在处理一般模型时非常有用。如本章后面所述,字段对象的属性包含为其指定的所有选项,因此列表中的每一项都可以提供丰富的信息。

这样,我们就可以创建一个定制的表单或模板输出,或者任何其他需要在任意模型上处理字段的特性。考虑下面的例子,它打印出给定对象中每个字段的显示名称和当前值,而不必事先知道使用的是什么模型。

from django.utils.text import capfirst

def get_values(instance):

for field in instance._meta.fields:

name = capfirst(field.verbose_name)

value = getattr(instance, field.name)

print('%s: %s' % (name, value))

这样做允许函数忽略对象背后的模型细节。只要它是一个适当的 Django 模型的实例,_meta属性就可用,所有的字段都可以通过这种方式访问。由于 Django 会自动为任何没有声明主键的模型添加一个AutoField,所以创建的AutoField也会包含在fields列表中。

虽然能够遍历列表对于那些需要考虑所有字段的情况来说是很好的,但是有时只需要一个字段,并且该字段的名称是预先知道的。由于fields是一个列表而不是一个字典,通过名称获取字段的唯一方法是循环遍历字段,检查每个字段看其名称是否匹配。

为了迎合这种需求,Django 提供了一个实用方法,_meta.get_field()。通过向_meta.get_field()提供字段名,很容易只检索指定的字段。如果不存在具有该名称的字段,它将引发一个FieldDoesNotExist异常,该异常位于django.db.models.fields

为了更好地理解这些方法如何协同工作来标识模型上声明的字段,请考虑下面的模型声明。

class Product(models.Model):

sku = models.CharField(max_length=8, verbose_name='SKU')

name = models.CharField(max_length=255)

price = models.DecimalField(max_digits=5, decimal_places=2)

def __unicode__(self):

return self.name

然后,可以检查模型以获得关于该声明的更多信息,而不必事先知道它看起来像什么。

>>> from django.utils.text import capfirst

>>> for field in Product._meta.fields:

...     print('%s: %s' % (capfirst(field.verbose_name), field.__class__))

...

ID: <class 'django.db.models.fields.AutoField'>

SKU: <class 'django.db.models.fields.CharField'>

Name: <class 'django.db.models.fields.CharField'>

Price: <class 'django.db.models.fields.DecimalField'>

>>> Product._meta.get_field('name').__class__

<class 'django.db.models.fields.CharField'>

主键字段

通过在字段定义中设置primary_key=True,可以将任何字段指定为主键。这意味着,如果代码要处理模型或模型实例,而事先不知道它的定义,那么通常有必要识别哪个字段被定义为主键。

与通过名称获取字段非常相似,可以遍历所有字段,查找属性设置为True的字段。毕竟,Django 只允许将一个字段指定为主键。不幸的是,这又引入了大量的样板文件,减慢了速度,并使维护变得更加困难。

为了简化这个任务,Django 提供了另一个_meta属性pk,它包含将被用作模型主键的 field 对象。这也比遍历所有的字段要快,因为在模型第一次被处理的时候,pk被填充了一次。毕竟,Django 需要确定是否需要提供隐式主键。_meta.pk属性还用于启用模型实例上的pk快捷方式属性,该属性返回实例的主键值,而不管哪个字段是主键。

通常,模型不需要声明显式主键,而是可以让 Django 自动创建一个。这是避免重复这种公共声明的有效方法,同时仍然允许在必要时覆盖它。然而,这样做的一个潜在问题是确定一个模型是否被赋予了一个自动字段,以及这个字段看起来像什么。

基于 Django 如何提供这个自动字段,以及它通常看起来是什么样子,可以对模型做出某些假设。然而,创建一个看起来很像隐式字段的自定义字段是很容易的,如果您的代码只看它的结构和选项,就很难区分这两者。

相反,Django 在_meta属性上提供了两个属性来帮助解决这种情况。如果模型让 Django 隐式地提供一个id字段,那么第一个字段_meta.has_auto_field就是True。如果是False,模型有一个明确的主键,所以 Django 不需要干预。

与自动主键字段相关的第二个属性是_meta.auto_field,它将是作为主键提供的实际字段对象 Django。如果_meta.has_auto_fieldTrue,这将是一个AutoField,并且将总是以相同的方式配置给所有使用它的型号。重要的是查看这个属性,而不是对字段的结构做出假设,以防 Django 在未来做出任何改变。这是一种简单的方法,有助于确保您的应用在未来继续正常工作。如果一个模型提供了自己的主键字段,那么_meta.has_auto_field就是False,那么_meta.auto_field将被设置为None

配置选项

除了提供对模型上声明的字段的访问,_meta还充当所有各种选项的容器,这些选项可以使用Meta内部类在模型上设置。这些选项允许模型控制各种事情,例如模型的名称、应该使用的数据库表、记录应该如何排序,以及其他一些事情。

这些选项都有默认值,所以即使那些没有在模型上指定的属性仍然可以通过_meta属性获得。以下是以这种方式可用的许多选项的列表,以及它们的默认值和该选项用途的简要描述。

  • abstract—一个布尔值,表示模型是否被定义为抽象,这个过程在 Django 的模型继承文档中有更详细的描述。 2 默认值为False
  • app_label—包含 Django 用来识别定义模型的应用的名称的字符串。通过查看默认值,最容易理解这意味着什么,默认值是包含指定模型的models.py的模块的名称。对于位于corporate.accounts.models.Account的模型,app_label将是"accounts"
  • db_table—Django 将用来存储和检索模型数据的数据库表的名称。如果没有明确定义,它将被确定为模型名称和位置的函数。也就是说,一个叫做Account的型号的db_table和一个accountsapp_label将会是"accounts_account"
  • db_tablespace—对于 Oracle 以及将来可能出现的其他数据库后端,表可以放在磁盘的不同部分,或者完全放在不同的磁盘上。默认情况下,这只是一个空字符串,它告诉数据库将表存储在默认位置。对于不支持该选项的后端,该选项将被忽略。
  • get_latest_by—基于日期的字段的名称,例如DateFieldDateTimeField,它应该用于确定模型的最新实例。如果未提供,这将是一个空字符串。
  • order_with_respect_to—与另一个模型相关的字段的实例,在对该模型的实例进行排序时使用。这默认为None,这意味着模型的排序完全由模型本身的字段决定,而不是由任何相关的模型决定。
  • ordering—包含在对模型的实例进行排序时要使用的字段名称的元组。默认情况下,这是一个空元组,它依赖于数据库来确定模型实例的排序。
  • permissions—要添加到模型中的附加权限的元组序列。序列中的每个元组包含两个值,第一个值是要在代码和数据库中使用的权限的名称,第二个值是为用户或组选择权限时要在管理界面中显示的文本。
  • unique_together—指示任何字段组的元组序列,当组合时,必须仅在数据库中的一个记录中使用。序列中的每个元组都包含对于特定索引必须是唯一的字段名称。多个元组彼此之间没有任何关系;它们各自代表数据库级别的一个单独的索引。
  • verbose_name-模型的单个实例的显示名称。默认情况下,这是由类本身的名称决定的,通过将每个大写部分拆分成一个单独的非大写单词;Article会变成"article",而AddressBook会变成"address book"
  • verbose_name_plural-模型的多个实例的显示名称。默认情况下,这只是结尾带有“s”的verbose_nameArticle会是"articles"AddressBook会是"address books"
  • verbose_name_rawverbose_name的原始未翻译版本。偶尔,有必要对每个人使用相同的显示名称,而不需要 Django 应用翻译。这在将它存储在缓存或数据库中以供以后访问时特别有用,尤其是如果它将在以后的某个时间点被翻译。

访问模型缓存

一旦模型被ModelBase元类处理,它们就被放入位于django.db.models.loading的名为AppCache的全局注册中心。当模块被导入时,它立即被自动实例化,并使用名称cache进行访问。这个特殊的缓存提供了对 Django 已知的各种模型的访问,并在必要时安装新的模型。

因为每当 Python 处理类时,ModelBase处理新模型的注册,所以它包含的模型不能保证是应用的一部分,出现在INSTALLED_APPS设置中。这个事实使得记住模型上的_meta属性包含一个installed属性变得更加重要,这个属性表明模型是否属于一个已安装的应用。

每当代码访问本节中的某个特性时,AppCache将自动加载INSTALLED_APPS中列出的应用,确保每当访问某些特性时,缓存中包含所有应该可用的应用和模型。如果没有这一点,这些方法的结果将是完全不可预测的,仅仅基于应用的加载顺序。

显而易见,只有当所有应用都被加载后,应用缓存才能被完全填充。因此,如果应用的models.py调用AppCache作为加载过程的一部分,缓存可能还没有完全填充。

为了防止这个问题,AppCache提供了一种方法来确定缓存本身是否已经被填充并准备好被访问。调用cache.app_cache_ready()将返回TrueFalse,这取决于是否所有已安装的应用都已被正确处理。利用这一点,可以从拥有自己的已知模型缓存中受益的应用可以检查该缓存是否可用于该目的。如果是,它可以直接使用这个缓存,如果不是,它可以手动确定它需要知道什么。

检索所有应用

当想要反思一个站点的内容时,看看应用本身的结构也是非常有用的。毕竟,只有在有模型可看的情况下,查看模型才是有用的,有时需要收集当前使用的所有模型。按照声明它们的应用来排列它们也很有用。Django 已经需要将这些信息放在手边,所以AppCache被设计成专门管理这些信息。

HOW DOES DJANGO SEE APPLICATIONS?

需要记住的一件重要事情是,Django 需要一个对象作为应用的引用。Django 应用本质上是一个标准的 Python 包,它只是包含在单个文件夹中的模块集合。虽然 Python 提供了一个对象作为单个模块的引用,但它没有提供任何东西来引用一个包。

因此,Django 可以拥有的最接近应用对象的概念是 Python 用来将其识别为包的__init__.py模块。在这种情况下,Django 将使用模块对象作为应用引用。

不幸的是,很少有项目在__init__.py中存储任何有用的东西,所以 Django 不太可能从中找到任何感兴趣的东西。为了获得任何真正有用的东西,它必须执行一些额外的工作来遍历包结构,以获得包含一些相关信息的模块。

相反,因为 Django 无论如何都必须使用一个模块对象,所以使用一个包含有用信息的模块更有意义。对于大多数应用来说,包中最有用的模块是models.py,在这里定义了所有的 Django 模型。因此,Django 使用这个模块来识别应用。下面的一些方法返回一个应用,在每种情况下,它都返回应用包中的models模块。

站点范围内自检的第一步是确定安装了什么应用。调用cache.get_apps()将返回这样一个列表,其中包含包含一个models模块的INSTALLED_APPS设置中每个应用的应用模块。这并不是说它只返回有模型的应用。它实际上检查一个models模块的存在,所以即使一个空的models.py也会导致一个应用被包含在这个列表中。

以下面的INSTALLED_APPS设置为例,展示了 Django 自己贡献的几个应用,以及一些内部应用和第七章中描述的signedcookies应用。

INSTALLED_APPS = (

'django.contrib.admin'

'django.contrib.auth'

'django.contrib.contenttypes'

'django.contrib.sessions'

'django.contrib.sites'

'news'

'customers'

'callcenter'

'signedcookies'

)

这些应用中的大多数必然会包含各种模型。第七章的signedcookies,然而,只与网站的 HTTP 流量交互,所以它对数据库没有用处。因此,当查看cache.get_apps()的结果时,signedcookies应用不会出现。

>>> from django.conf import settings

>>> from django.db.models.loading import cache

>>> len(settings.INSTALLED_APPS)

9

>>> len(cache.get_apps())

8

>>> for app in cache.get_apps():

...     print(app.__name__)

...

django.contrib.admin.models

django.contrib.auth.models

django.contrib.contenttypes.models

django.contrib.sessions.models

django.contrib.sites.models

news.models

customers.models

callcenter.models

检索单个应用

有了应用列表,就可以直接从每个应用中获取模型,这样就可以对它们进行适当的处理。下一节将更详细地描述这个过程。然而,查看所有模型并不总是最好的方法;有时,一个应用可能被赋予一个特定应用的标签,因此它可以只处理该应用中的模型。

虽然可以循环遍历来自cache.get_apps()的结果,对照应用模块的__name__属性检查模块名称,但是这种技术很快会遇到一些问题。首先,应用的标签和它的__name__属性不同,所以试图比较这两个结果需要很多额外的代码,其中大部分已经由 Django 完成了。此外,代码必须经过测试和维护,这增加了将错误引入应用的风险。

相反,Django 提供了一个实用程序来处理这种情况。通过将已知的标签传递给cache.get_app(),应用可以检索与该特定标签匹配的应用的应用模块。此处引用的标签被确定为应用导入路径的特定部分。

通常称为app_label,应用的标签通常由应用模块导入路径的最后一部分组成,在models部分之前。为了举例说明,考虑下面的应用标签,对应于INSTALLED_APPS设置中的条目。

admin

auth

contenttypes

sessions

sites

news

customers

callcenter

signedcookies

这里有一点需要注意。作为官方文档中描述的Meta选项的一部分,并且在本章前面简单地提到过,任何模型都可以覆盖它自己的app_label设置,就像它是在不同的应用中声明的一样。这个选项不会以任何方式影响cache.get_app()的行为。get_app()方法只是将app_label映射到一个应用模块,而不考虑其中的模块可能声明了什么选项。

正如前面的cache.get_apps()所展示的,没有模型的应用在 Django 内部被认为与其他应用略有不同。默认情况下,如果应用不包含models.py文件,cache.get_app()将引发一个ImproperlyConfigured异常。有时,在没有模型的情况下处理应用可能仍然有用,所以cache.get_app()接受一个可选的第二个参数来控制如何处理这样的应用。

第二个参数叫做emptyOK,它采用一个布尔值来表示应用是否允许不包含任何模型。这默认为False,这将引发ImproperlyConfigured异常,但是如果改为给出Truecache.get_app()将简单地返回None,允许调用代码继续管理应用。

>>> from django.db.models.loading import cache

>>> print(cache.get_app('admin'))

<module 'django.contrib.admin.models' from ...>

>>> print(cache.get_app('signedcookies'))

Traceback (most recent call last):

...

django.core.exceptions.ImproperlyConfigured: App with label signedcookies could not be found

>>> print(cache.get_app('signedcookies', emptyOK=True))

None

处理单个模型

一旦知道了应用,下一步就是处理该应用中的各个模型。再一次,AppCache提出了一些处理这种情况的方法。从缓存中检索模型通常采用两种形式中的一种,这取决于事先对模型的了解程度。

第一种情况,考虑纯内省。记住上一节,AppCache通过对返回应用模块的get_apps()方法的一次调用来提供对所有已知应用的访问。因为这些模块实际上是每个应用中的models模块,所以使用dir(app_module)或迭代app_module.__dict__来获得已定义的模型似乎很容易。

不幸的是,像简单迭代的许多用法一样,这需要循环检查模块中的每个单独的对象,看看它实际上是一个模型,还是完全是别的什么东西。毕竟,Python 模块可以包含任何内容,并且许多模型利用元组和模块级常量来帮助完成工作,所以不能保证模块名称空间中的每一项实际上都是 Django 模型。

相反,cache.get_models()检索特定于给定应用模块的适当 Django 模型的列表。cache.get_apps()cache.get_app()都返回应用模块,这不是巧合;cache.get_models()适用于这两种方法。这意味着即使没有应用也可以检索模型列表,但是提前知道应用可以减少检索到的模型数量。

下面的代码演示了如何结合使用这些技术来检索站点上使用的每个已知应用的模型列表。

>>> from django.db.models.loading import cache

>>> for app in cache.get_apps():

...     app_label = app.__name__.split('.')[-2]

...     for model in cache.get_models(app):

...         print('%s.%s' % (app_label, model.__name__))

...

admin.LogEntry

auth.Message

auth.Group

auth.User

auth.Permission

contenttypes.ContentType

sessions.Session

sites.Site

news.News

customers.Customer

callcenter.Agent

callcenter.Call

callcenter.Case

作为一个附加选项,get_models()也可以不带参数调用,这将导致它返回AppCache已知的所有模型。这是一个有用的捷径,可以避免与本例中的额外循环相关的一些开销,作为获取所有模型的快速方法。

然而,有一个问题。

直接使用get_models()时,不带参数,返回所有注册的模型。这听起来可能是个好主意,有时确实如此,但是请记住AppCache会在遇到模型时注册所有模型,不管它们是在哪里发现的。完整列表可能包括不属于已安装应用的型号。与之形成对比的是get_apps() / get_models()组合,它只检索在INSTALLED_APPS设置中找到的应用的模型。

实际上,如果不带参数调用,get_models()可能会返回与调用从get_apps()返回的每个应用不同的结果。通常,这可能意味着应用可以访问它不想知道的额外模型。有时候这确实是我们想要的行为,但是理解其中的区别总是很重要的。

一个模型可能在AppCache中,但没有被安装,如果应用是从一个单独的、已安装的应用中导入的,这将导致它的模型类被 Django 处理并注册,不管它是否在INSTALLED_APPS中。同样,如果任何模型在它的Meta类中指定了一个app_label,而这个应用标签与任何已安装的应用都不匹配,也会出现同样的情况。如果应用确实希望访问所有的模型,不管它们是否被安装,记住它可以使用_meta.installed属性来识别哪些模型被正确安装。

有时,会提供应用和模型的名称,可能作为 URL 或其他配置的一部分。在这些情况下,迭代给定应用的所有模型没有多大意义。对于这种情况,AppCache提供了另一种方法,get_model(),它根据应用标签和模型名称检索模型类。应用名称区分大小写,但模型名称不区分大小写。

>>> from django.db.models.loading import cache

>>> cache.get_model('auth', 'user')

<class 'django.contrib.auth.models.User'>

使用模型字段

模型最重要的方面之一是可用于保存数据的字段集。没有字段,模型就只是一个空容器,没有办法做任何有用的事情。字段提供了一种组织模型值并根据特定数据类型进行验证的方法,在数据库和原生 Python 数据类型之间架起了一座桥梁。

通常,当访问作为模型实例属性的字段时,该值将是一个标准 Python 对象,表示数据库中的值。本章的前几节描述了访问实际字段对象本身的各种方法,而不是这个转换后的值。使用字段对象可以做许多有用的事情。

公共字段属性

根据需要,不同的字段类型会有不同的属性,但是有几个属性是大多数内置 Django 字段共有的。这些可用于一般访问字段的各种细节,并通过关联访问它们要与之交互的值和行为。请注意,内部使用的属性比这里列出的更多,但这些是最有用和最稳定的,并将为希望使用字段的应用提供最大的价值。

这里列出的描述是 Django 本身如何使用这些属性,以及开发人员期望它们如何表现。其他应用也可能使用它们来控制某些类型的行为,因此下面的描述将有助于说明它们的预期用途。

一些应用可能会找到与 Django 本身预期用途略有不同的用途,但是值的一般语义应该保持不变。请记住,开发人员将根据 Django 本身的行为来构建他们对这些值的期望,第三方应用应该避免违反这些期望。

  • attname—存储数据库相关值的模型实例的属性名称。对于数据库中的值直接存储在模型中的简单情况,这通常与name属性相同。在其他情况下,当实际的字段名被访问时,向其他代码公开一个更复杂的对象,比如另一个模型实例,会更合适。对于这些情况,attnamename会有所不同,name引用的属性是复杂对象,而attname引用的属性包含创建它所需的原始数据。
  • blank—一个布尔值,指示当使用基于模型自动生成的表单时,该字段是否必须提供一个值。这纯粹是与验证相关的行为;null属性控制一个模型是否可以在没有给定字段值的情况下保存在数据库中。
  • choices—指示字段有效选项的二元组序列。每个元组中的第一项是实际值,如果被选中,它将存储在数据库中,而第二项是将为该值向用户显示的文本。
  • column—将用于保存字段值的数据库列的名称。如果字段明确声明了它的数据库列,那么它将匹配db_column,或者根据字段的名称自动生成。通常可以忽略这一点,因为 Django 直接管理数据库交互,但是有些应用可能需要直接与数据库通信,或者与需要这一信息的其他数据库适配器接口。
  • db_column—明确作为字段值的数据库列名提供的名称。这与column不同,因为db_column指的是模型本身声明的内容,而不是实际将要使用的内容。只有当模型字段明确指定了它的db_column参数时,它才会有值;否则就是None
  • db_index—一个布尔值,指示字段是否被声明为在数据库中为其创建了索引。这仅表明该字段是否被配置为指示 Django 创建索引。其他索引可能已经直接添加到数据库本身中,这不一定会反映在该属性的值中。
  • db_tablespace—表示字段数据存储位置的表空间指令。目前仅支持 Oracle 后端,其内容的格式将取决于数据库后端。它总是有一个字符串值,如果没有明确设置,默认为DEFAULT_INDEX_TABLESPACE设置的值。
  • default—字段的默认值,如果尚未向字段本身提供值,则使用该值。在这种情况下,除了插入数据库之外,该值还将用作基于模型生成的任何表单的字段初始值。存储在该属性中的值的类型将是该字段要与之交互的任何本机 Python 数据类型,例如字符串或整数。
  • description—字段或其用途的简单文本描述。docstring 通常也很有用,但是这种描述可以在显示应用内部的字段信息时使用,比如admindocs
  • editable—一个布尔值,指示在基于模型生成表单时,是否应向用户显示该字段以供编辑。这并没有使字段本身在 Python 中成为只读的,因此也不能保证字段不会被编辑。它只是一个控制表单默认行为的指令,尽管其他应用也可以——也应该——使用它来控制其他行为,如果它们提供编辑功能的话。
  • empty_strings_allowed—一个布尔值,指示字段是否允许空字符串作为可能的值。这不是作为特定字段实例的配置指定的选项,而是在字段的类本身中定义的。许多字段,如CharFieldEmailField,将空字符串与None分开处理,因此该属性允许后端决定如何为数据库(如 Oracle)处理空字符串,否则这些数据库可能会失去这种区别。
  • help_text—在字段定义中提供的信息性文本,当字段呈现以供编辑时向用户显示。这将被传递给基于模型生成的表单,比如提供的管理界面。
  • max_length—字段值可以包含的最大长度。大多数基于字符串的字段,比如CharFieldEmailField,都使用这个来限制字符串内容的长度,无论是在表单字段还是底层数据库列中。其他字段类型,比如IntegerFieldDateField,可以忽略它,因为它在这些情况下没有任何意义。
  • name—字段的名称,在将字段分配给模型时定义。这被设置为contribute_to_class()过程的一部分,通过避免两次输入名字来保持干燥。这将是属性的名称,将在该属性中分配和检索字段的本地 Python 值。与此形成对比的是attname,它存储了填充name所需的原始数据。通常,这两个值是相同的,但是对于它们不同的情况,理解它们的区别是很重要的。
  • null—一个布尔值,指示字段是否可以在没有赋值的情况下提交到数据库。这主要控制如何创建底层数据库列,但是只要语义保持不变,一些应用可能会找到其他用途。
  • primary_key—一个布尔值,指示该字段是否应该用作数据库表的主键。除了指示数据库生成主键索引之外,Django 还使用这个指示器来确定在查找特定实例时使用哪个字段的值,比如通过外键关系查找相关对象。参见本章前面的“主键”一节,详细了解用于确定哪个字段的值设置为True_meta.pk快捷键。
  • rel—对于将一个模型与另一个模型相关联的字段,这将是一个描述该关系的各个方面的特殊对象。对于所有非关系字段类型,这将被设置为None
  • serialize—一个布尔值,指示当使用序列化框架序列化模型实例时是否应包含该字段。3
  • unique-一个布尔值,表示该字段在模型的所有实例中必须是唯一的。这主要用于在数据库中创建适当的约束来强制执行该条件,但它也可以由应用使用。例如,提供关于用户输入的值对于模型是否有效的详细反馈的内容编辑应用在做出该决定时也可以考虑这一点。
  • unique_for_date—与日期相关的字段的名称,例如DateFieldDateTimeField,其值应该是唯一的。这在本质上类似于unique,除了根据该属性引用的字段,约束被限制为发生在同一天的记录。这不能在数据库级别强制执行,所以 Django 手动管理约束,就像任何其他需要提供给定对象是否可以提交到数据库的详细信息的应用一样。
  • unique_for_month—与unique_for_date类似,不同的是只有发生在同一个月的对象才需要唯一性,根据该属性包含的名称所引用的日期相关字段。
  • unique_for_year—与unique_for_date类似,不同的是只有发生在同一年的对象才需要唯一性,根据该属性包含的名称所引用的日期相关字段。
  • verbose_name—用简单的英语显示给用户的字段的全名。Django 的文档建议以小写字母开头,这样应用就可以在必要时使用大写字母。如果一个应用需要将这个值资本化,一定要使用capfirst()实用程序方法,在第九章中有描述。

常见的现场方法

与上一节描述的属性一样,这些方法对于大多数字段类型来说都是通用的,并且提供了大量的功能,否则很难获得这些功能。并非所有的字段类型都会实现所有这些方法,它们的确切行为可能会根据所涉及的字段类型而变化,但是这里描述的一般语义将保持不变。

还有更多在内部使用的方法,这里没有列出,因为它们主要负责简单地填充上一节描述的属性。因此,通常最好是简单地引用生成的属性,而不是试图在事后手动重新创建它们。

  • clean(value, instance)—验证给定的value是否适合模型,以及它被分配到的instance。在内部,这遵从于to_python()validate(),以及处理字段实例化时定义的验证器列表。如果一切都有效,它将返回一个正确的值,否则将引发django.core.exceptions.ValidationError
  • contribute_to_class(cls, name)—为其附加的类配置字段。字段上最重要的方法之一,当ModelBase处理分配给模型的类定义的属性时,就会调用这个方法。cls参数是它被分配到的模型类,name是它被分配到那里时的名字。这使得现场人员有机会根据这些信息执行任何额外的设置或配置。通常不需要直接调用它,但是它是将字段应用到先前处理的模型的一种有用的方法。
  • db_type(connection)—返回该字段存储其数据所需的特定于数据库的列定义。通常,这仅在内部使用,但是与列出的一些其他属性一样,如果应用需要使用一些其他工具直接访问数据库,这可能是确定基础列的有用方法。
  • formfield()—根据字段的数据类型和详细名称返回表单字段,适合包含在任何标准表单中。它可以选择一个显式参数form_class,这是一个要实例化的表单字段类,默认为最合适的表单字段,由模型字段本身定义。它还接受任意数量的附加关键字参数,这些参数只是在返回实例化的表单域之前通过表单域的构造函数传递。这通常由 Django 在基于模型构造表单时自动调用,但是在其他情况下也可以手动使用。更多信息可以在第五章中找到。
  • get_attname()—返回应该用于attname属性的名称。这仅在为类配置字段时调用一次。
  • get_attname_column()—返回包含用于attname属性和column属性的值的两项元组。
  • get_cache_name()—如果需要缓存,返回适合用作字段缓存的名称。这通常只适用于生成复杂 Python 数据类型的字段,如果每次访问都必须生成这样一个复杂的对象,或者在不使用它的情况下,性能会受到严重影响。有关如何在这种情况下使用这种方法的详细信息,请参见本章末尾的应用技术。
  • get_choices()—返回一个二元组序列,该序列应用于向希望在该字段中输入数据的用户显示选项。与choices属性不同,这可能还包括一个空选项,表示没有做出选择。这种行为由两个可选参数控制:include_blank,一个布尔值,表示是否应该包括它;以及blank_choice,一个元组列表,包含应该用于空选项的值和显示文本。默认情况下,这些参数被配置为包含对("", "---------")的单一选择。
  • get_db_prep_lookup(value, lookup_type, connection, prepared=False)—返回所提供值的表示形式,适合与数据库中的现有值进行比较。
  • get_db_prep_save(value, connection)—返回适合存储在数据库中的所提供值的表示形式。
  • get_db_prep_value(value, connection, prepared=False)—返回所提供值的表示形式,可用于数据库的一般用途。这由get_db_prep_lookup()get_db_prep_save()内部调用。
  • get_default()—返回将用于字段的默认值。这处理了所有必要的逻辑,检查是否提供了默认值,如果缺省情况下提供了一个 callable,则执行它,并为需要该行为的数据库后端区分空字符串和None
  • get_internal_type()—返回一个字符串,表示字段包含的数据类型的高级概念。这主要用于与每个数据库后端提供的映射一起确定要使用的实际数据库列。
  • get_prep_lookup(lookup_type, value)—类似于get_db_prep_lookup(),除了这个方法用于简单的转换,不需要知道使用哪种类型的数据库。
  • get_prep_value(value)—类似于get_db_prep_value(),除了这个方法用于简单的转换,不需要知道使用哪种类型的数据库。
  • has_default()—如果字段有关联的默认值,则返回True,如果默认行为将留给数据库后端,则返回False
  • pre_save(model_instance, add)—返回保存到数据库之前的字段值。默认情况下,这只是返回已经在所提供的model_instance上设置的值,但是它也可以返回从其他字段派生的值,或者可能与实例完全无关的值,比如当前时间。add参数是一个布尔值,表明所提供的实例是否是第一次被添加。
  • save_form_data(instance, data)—将提供的数据存储到提供的实例的适当属性中。这是表单能够基于表单数据充分填充模型实例的快捷方式。
  • set_attributes_from_name(name)—使用提供的name参数根据需要设置字段的nameattnamecolumnverbose_name属性。对于attnamecolumn值,该方法遵从get_attname_column(),而verbose_name只有在实例化字段时没有显式定义时才在这里设置。
  • to_python(value)-将提供的值强制转换为本地 Python 数据类型,以便在访问模型实例上的字段值时使用。详见本章后面对它的描述。
  • validate(value, instance)-如果字段的值适合于字段的配置和模型实例上的其他数据,则无错误返回,否则引发django.core.exceptions.ValidationError。这是clean()内部的叫法。
  • value_from_object(obj)—返回字段在所提供对象上显示的值。

子类化字段

使用 Django 模型可以做的一件更有用的事情,特别是对于可重用的应用,是与模型以通用方式处理单个类型的字段的能力结合起来。这使得字段本身能够很好地控制它们与数据库的交互方式、使用何种原生 Python 数据类型来访问它们的内容,以及如何将它们应用到使用它们的模型类。

本节的大部分内容假设自定义字段需要保留现有字段的许多相同功能,例如与数据库和生成的表单交互。还有许多其他的应用,比如在第十一章中描述的历史记录应用,它们使用本节中描述的钩子来提供比简单字段更多的功能。

这里的术语“字段”是用来描述任何使用这些技术向 Django 开发人员呈现类似于标准 Django 模型字段的对象。实际上,这种对象可以封装复杂的关系,比如标记应用,甚至可以根据它们被分配到的模型,动态地控制整个新 Django 模型的创建。可能性几乎是无限的。

要记住的关键是 Django 在字段方面使用了 duck 类型化原则。它只是在每种情况下访问它期望的任何属性和方法,而不考虑这些属性和方法在幕后实际做了什么。事实上,使用这些钩子甚至不需要对象是django.db.models.fields.Field的子类。从Field继承只是提供了一种简单的方法来重用现有的大部分功能,如果需要这种行为的话。

决定是发明还是扩展

当编写一个新的字段时,首先要考虑的事情之一是,是尝试创造一个全新的字段类型,也许是在完全没有Field帮助的情况下从零开始,还是扩展一些现有的字段类型并继承它的大部分行为。每种方法都有优点和缺点,哪种方法最合适在很大程度上取决于新领域的需求。

通过继承Field或它的一个子类,下面部分中的大多数行为将被继承,潜在地减少了定制字段必须包含的新代码的数量。如果它的行为类似于一个现有的字段类型,这将是一个非常有用的方法,不仅可以减少新代码,有助于减少 bug,还可以自动接收 Django 本身在未来版本中提供的任何新的或更新的功能。毕竟,通过依赖 Django 本身来实现这种行为,对代码的更新将自动反映在定制字段的行为中。

另一方面,如果新的字段与任何现有的字段类型有很大的不同,那么无论如何都需要重写标准行为以供自己使用,从而否定从父类继承的任何值。如果这些行为中的大部分——或者全部——都必须从头开始编写,那么从现有字段继承只会在 Python 用来管理类的过程中增加一个额外的步骤,即使这个额外的步骤没有带来什么好处。因此,在这些情况下,最好从零开始,只实现那些对定制字段有意义的行为,由于使用了 duck 类型,Django 仍然会正确地处理它。

当然,这两种方法之间有一些中间地带。例如,一个定制字段可能与一个完全唯一的数据类型交互,与任何现有的字段类型几乎没有相似之处,但是它可能仍然像一个标准字段一样将其数据存储在数据库中,并且可以从重用 Django 的许多更基本的字段方法中受益,例如分配名称并将其自身存储在_meta.fields中。在这些情况下,从Field本身继承,而不是从一个特定的子类继承,并且只继承这个最基本的功能是非常合理的。

在模型注册期间执行操作

任何字段经历的第一步都是由ModelBase元类处理,每当 Python 遇到利用该字段的模型类时。对于标准的 Python 对象,这意味着像平常一样简单地分配给模型类,没有额外的处理。然而,字段采取不同的路径,并且每个字段都有机会定制它如何应用于模型类。

contribute_to_class(self,cls,name)

这可能是一个字段可以包含的最重要的方法,因为它提供了一个基本特性:字段能够知道它被分配给了什么类,以及它被赋予了什么名称。这似乎是一个简单的要求,但是 Python 本身通常没有一种方法来实现这一点。

您可能还记得在第二章的中描述的描述符,它有一种方法来识别哪个类——甚至是那个类的哪个实例——被用来访问对象,但是这只有在访问属性时才可用;仍然没有办法知道任务发生时的信息。更重要的是,即使是描述符也没有提供任何方法来识别使用了什么名称来访问它们,这在试图缓存信息或者与需要使用名称的其他特性(比如数据库列)进行交互时会是一个相当大的问题。

相反,通过使用元类,Django 可以在 Python 处理类的地方进行调解,并使用一个contribute_to_class()方法来识别需要不同处理的对象。如果这个方法存在,它将代替标准的setattr()被调用,允许字段以最适合其用途的方式注册自己。这样做的时候,Django 还提供了类本身作为参数,以及它被赋予的名称,这是在查看分配给该类的属性时发现的。因此,除了通常的self之外,这个方法还收到两个参数。

  • cls—该字段被分配到的模型的实际类对象。这可用于根据模型本身的名称或其他属性定制字段。
  • name—分配给模型类的属性的名称,以字符串形式表示。字段通常会将其存储为字段本身的属性,以供将来参考。

一旦这两个参数以适合该字段的任何方式被处理,该方法就不应该返回任何东西,因为它的返回值被 Django 忽略了。

CONTRIBUTE_TO_CLASS() VS SETATTR()

contribute_to_class()打交道的时候有一件很重要的事情要记住。它已经在不同的地方被提到过几次,但是它是如此的重要,以至于它值得被非常明确地带回家。如果 Django 发现一个对象有一个contribute_to_class()方法,那么只有那个方法会被调用。

通常,setattr()用于设置一个对象(比如一个类)的属性,但是由于模型字段不能在标准名称空间中设置,所以有意跳过了这一步。因此,如果一个定制字段确实需要被设置为模型类本身的一个属性,那么在执行它的contribute_to_class()方法期间,这样做是字段本身的唯一责任。

有时,字段需要设置一些其他对象,如描述符,作为类的属性,为其他类型的访问提供额外的自定义。这也是 field 类的责任,并且只有在执行它的contribute_to_class()方法时才能保持标准字段的外观。

在标准 Django 字段的情况下,也许对于许多类型的定制字段和其他表现为字段的对象,这种对setattr()的避免是有意的。如果需要这种行为,contribute_to_class()应该简单地避免在模型类上设置任何东西,Django 自己的行为将确保没有任何东西被分配给类本身。

对于与其他模型相关联的字段,一旦相关模型可用,就会调用这个函数,这样属性也可以添加到该模型中。例如,当应用了ForeignKey时,Django 就是这样在相关类上提供一个反向属性的。

它接收的两个参数是cls,关系实际应用到的模型类,和related,关系指向的模型,其中可能还需要应用其他属性。像contribute_to_class()一样,它不应该返回任何东西,因为它无论如何都会被忽略。

改变数据行为

鉴于大多数字段类型都是为了与特定的数据类型交互而存在的,首先要考虑的事情之一就是如何告诉 Django 处理该数据类型。这包括如何将其存储在数据库中,如何确保其值的有效性,以及如何用 Python 表示该值。这些是现场行为的一些最基本的方面,适当地改变它们可以打开一个可能性的世界。

get_internal_type(自身)

该方法返回一个字符串,该字符串有助于确定数据库应该如何存储字段的值。字符串本身不是实际的数据库列类型,而是应用于数据库后端提供的映射,以确定使用哪种类型的列。通过这种方式,可以编写字段,而不必绑定到特定的数据库后端。

因为该函数的返回值被应用到一个已知的类型字典来检索数据库列名,所以该值必须是该字典中的一个有效条目。因此,这里列出了一组有限的可能返回值。

  • AutoField
  • BigIntegerField
  • BooleanField
  • CharField
  • CommaSeparatedIntegerField
  • DateField
  • DateTimeField
  • DecimalField
  • FileField
  • FilePathField
  • FloatField
  • ImageField
  • IntegerField
  • IPAddressField
  • NullBooleanField
  • OneToOneField
  • PositiveIntegerField
  • PositiveSmallIntegerField
  • SlugField
  • SmallIntegerField
  • TextField
  • TimeField

验证(自身、值、实例)

当检查模型值的准确性时,此方法用于确定字段的内容是否正确。它接收的参数是字段本身的值,以及包含所有字段的模型。这使得它不仅可以验证字段本身的值,还可以验证它在更大模型的上下文中的意义。

为什么这在验证单个字段的值时有用应该是显而易见的,但是使用模型的其余值有什么价值就不太清楚了。毕竟,当编写一个字段时,通常无法知道它旁边还会使用哪些字段。

然而,有时一个字段可能是专门为一个特定的模型编写的,因此可以预先知道整个模型看起来是什么样子。例如,在这些情况下,该字段可以检查一个人拥有什么类型的帐户,因为该字段的最大值取决于其他字段。

to_python(自身,值)

字段的值可以用多种不同的方式存储,这取决于它的存储位置。在数据库中,它可以是几种基本类型中的一种,如字符串、整数和日期,而在序列化模型时,所有值都将被强制为字符串。这意味着,当实例化一个模型时,它的值通常必须被强制转换回正确的 Python 表示。这个行为是由to_python()方法处理的,尽管它并不像表面上看起来那么简单。

首先要考虑的是传递给to_python()的值可能是数据的多种表示形式之一。例如,它可以是从数据库适配器返回的任何格式,如字符串、整数或本机 Python 日期,但它也可以是从序列化程序检索的字符串,或者如果字段管理需要初始化的更复杂的自定义数据类型,则该值实际上可以是该类型的完全初始化的实例。

为了说明这一点,考虑一下BooleanField的情况。传递给它的值可能有多种形式,所以它的to_python()方法需要预料到这一点,并确保它总是返回一个布尔值或抛出一个异常,表明该值不适合该字段。

def to_python(self, value):

if value in (True, False): return value

if value in ('t', 'True', '1'): return True

if value in ('f', 'False', '0'): return False

raise exceptions.ValidationError(_("This value must be either True or False."))

如您所见,它必须检查几种不同类型的值,这些值都可以可靠地强制转换为布尔值。除了本机的TrueFalse,它还检查相同的字符串表示,以及在各种情况下可能出现的两个单字符表示。如果找到了合适的值,它会简单地返回合适的本地布尔值,如果找不到合适的值,则会引发上一节中描述的ValidationError

不幸的是,to_python()是一个额外的方法调用,并不总是必要的,所以它并不总是在看起来需要的时候被调用。特别是,它主要用于在提交到数据库之前验证数据,以及从序列化数据中检索内容,因此当从数据库中检索时,假设数据已经过验证,并且数据库后端通常足以返回正确的类型。

因此,Django 在从数据库中检索数据时不会调用to_python()。对于内置类型和许多潜在的附加字段,这已经足够了,但是对于其他数据类型或复杂对象,还需要做更多的工作来将数据库值转换为适合使用的值。为了支持这些类型的字段,Django 提供了一种特殊的方法来强制在填充字段值时调用to_python()

用子字段库支持复杂类型

有时数据库没有必要的数据类型来支持某些类型的应用。例如,大多数数据库无法存储时间长度,并将其作为datetime.timedelta 4 对象呈现给 Python。为此,PostgreSQL 有一个名为interval 5 的列类型,它确实应该直接映射到 Python timedelta中,但其他数据库没有,这使得它在可重用性方面不切实际。它适用于 PostgreSQL,但是为了使应用具有可移植性,它需要可用于多个数据库。

幸运的是,timedelta以天、秒和微秒来存储它的值,并且可以根据作为float传入的秒数来写入整个值。因此,一个新的DurationField可以使用一个DecimalField在数据库中存储一个值,在 Python 中转换成一个float,然后传递给timedelta在模型实例上使用。

import datetime

import re

from django.core.exceptions import ValidationError

def to_python(value):

if isinstance(value, datetime.timedelta):

return value

match = re.match(r'(?:(\d+) days?, )?(\d+):(\d+):(\d+)(?:\.(\d+))?', str(value))

if match:

parts = match.groups()

# The parts in this list are as follows:

# [days, hours, minutes, seconds, microseconds]

# But microseconds need to be padded with zeros to work properly.

parts[4] = groups[4].ljust(6, '0')

# And they all need to be converted to integers, defaulting to 0

parts = [part and int(part) or 0 for part in groups]

return datetime.timedelta(parts[0], parts[3], parts[4]

hours=parts[1], minutes=parts[2])

try:

return datetime.timedelta(seconds=float(value))

except (TypeError, ValueError):

raise ValidationError('This value must be a real number.')

except OverflowError:

raise ValidationError('The maximum allowed value is %s' % \

datetime.timedelta.max)

这是一种不使用to_python()就无法处理的过程,并且它必须在每次模型被实例化时发生,即使是来自数据库。然而,在每次访问数据库时调用额外的方法调用会变得非常昂贵,所以能够在不影响那些不使用它的字段的情况下处理这个问题是非常重要的。

正如在本章末尾将要展示的,描述符可以用来定制当一个字段的值被访问时会发生什么,这是控制这类行为的一个很好的方法。当然,如果描述符只是达到目的的一种手段,那么描述符可能会很棘手,这里描述的to_python()行为是这些复杂数据类型相当常见的需求,所以 Django 提供了一种简化描述符创建的捷径。

位于django.db.models.fields.subclassingSubfieldBase元类是 Django 简化模型字段创建的方法,其to_python()方法将被调用。通过简单地将它应用到一个模型类,它会处理剩下的事情,建立一个描述符,在第一次加载字段时调用to_python()。因此,DurationField示例将在字段定义中使用它,如下所示:

from django.db import models

from django.db.models.fields.subclassing import SubfieldBase

class DurationField(models.DecimalField, metaclass=SubfieldBase):

pass

# Field logic then continues here

控制数据库行为

字段的另一个重要方面是它们如何与数据库交互。这可能包括数据本身如何存储,在发送到数据库之前如何准备,以及如何准备与数据库中已有的值进行比较。Django 自己已经完成了这个过程,每个现有的字段类型都提供了一些方法来定义这个行为。

对于定制字段,通常需要覆盖这种行为,以不同于 Django 本身期望的方式与数据库交互。以下方法定义了字段如何与数据库一起工作的几乎每个方面,因此字段对如何处理数据库交互有很大的控制权。

db_type(自身,连接)

很少被单个字段覆盖,该方法返回特定于数据库的字符串,该字符串控制如何创建用于给定字段的列。Django 在内部使用get_internal_type()方法的结果和每个后端提供的映射来提供这个方法的返回值。这一功能足以满足绝大多数现场应用。

在考虑使用这个方法时,要记住的最重要的事情是,它的返回值特定于特定的数据库后端。为了在具有不同后端的项目中使用这个字段,提供了connection参数来帮助您决定使用什么。在一个简单的例子中,您可以使用connection.settings_dict['ENGINE']来确定该字段在什么类型的数据库上使用,并相应地采取行动。例如,如果DurationField实际上可以在 PostgreSQL 中使用interval,同时仍然支持其他数据库:

class DurationField(models.Field):

def db_type(self, connection):

engine = connection.settings_dict['ENGINE']

if engine == 'django.db.backends.postgresql_psycopg2':

return 'interval'

else:

return connection.creation.data_types['DecimalField']

这个方法的另一个特性是,如果你返回的是None而不是一个字符串,Django 将跳过这个特定字段的创建。如果必须以比单个字符串所能表示的更复杂的方式创建字段,这可能是必要的。但是,Django 在执行查询时仍然会尝试引用该列,所以在尝试使用该字段之前,您需要确保确实创建了该列。

大多数时候,您会希望将这个方法留给 Django,但是它确实提供了一种方法,可以在您真正需要的时候覆盖默认行为。在分布式应用中这样做时要小心,因为您最终将不得不支持多种类型的数据库,而不仅仅是您最熟悉的一种。

get_prep_value(自身,值)

有一些方法处理为数据库中不同的用户准备值,但是它们通常共享相同的代码来准备在数据库中使用的值。以下两种方法都使用get_prep_value()方法来执行这个基本转换。

在大多数情况下,将 Python 对象转换为某种更基本的类型就足以允许自定义字段将值传递给数据库。通过覆盖get_prep_value(),其他数据库准备方法通常可以毫无问题地使用它们的默认实现。例如,DurationField需要这种类型的转换,因为timedelta对象不能直接传递给大多数数据库,这导致使用DecimalField来控制列的行为。一个定制的get_prep_value()方法可以将timedelta对象转换成Decimal值,然后这些值可以正常地传递给数据库。

from django.db import models

from django.db.models.fields.subclassing import SubfieldBase

from django.utils import _decima l

class DurationField(models.DecimalField, metaclass=SubfieldBase):

def get_prep_value(self, value):

return _decimal.Decimal('%s.%s' % (value.days * 86400 + value.seconds

value.microseconds))

# Field logic then continues here

get_db_prep_value(self,value,connection,prepared=False)

如果您需要为不同的数据库连接准备不同的值,这种方法将允许您灵活地这样做。connection参数再次表示正在使用的数据库连接,并可用于对如何继续进行做出必要的决定。准备好的参数表明该值是否已经通过get_prep_value()传递。如果是False,您应该在继续下一步之前调用该方法。如果继续在 PostgreSQL 和其他数据库之间分离其行为,下面是DurationField可能的样子:

from django.db import models

from django.db.models.fields.subclassing import SubfieldBase

from django.utils import _decimal

class DurationField(models.DecimalField, metaclass=SubfieldBase):

def get_prep_value(self, value):

# Nothing to do here, because get_db_prep_value() will do the dirty work

return value

def get_db_prep_value(self, value, connection, prepared=False):

if not prepared:

value = self.get_prep_value(value)

engine = connection.settings_dict['ENGINE']

if engine == 'django.db.backends.postgresql_psycopg2':

# PostgreSQL can handle timedeltas directly

return value

else:

return _decimal.Decimal('%s.%s' % (value.days * 86400 + value.seconds

value.microseconds))

# Field logic then continues here

get_db_prep_save(自身,值,连接)

这与get_db_prep_value()的工作方式非常相似,但是提供了一种方式,在实际将值保存到数据库中时提供单独的行为,而不是其他操作。事实上,如果您没有为此方法提供实现,默认行为将简单地遵从get_db_prep_value(),这通常就足够了。

get_prep_lookup(自身,查找类型,值)

字段必须与数据库交互的另一个领域是在 Python 对象和已经存储在数据库中的值之间进行比较时。例如,每次使用 QuerySet 的filter()方法时都会发生这种情况,以便生成必要的数据库查询。由于比较可能需要与保存不同的处理,Django 使用get_prep_lookup()方法来管理这项任务。

调用时,此方法接收两个显式参数,详细说明预期如何进行查找。第一个是lookup_type,是在filter()方法中请求的比较类型。第二个是value,是用于与数据库值进行比较的 Python 对象。

虽然value相当简单,但是lookup_type有点不同,因为它是一个包含所请求的比较类型的字符串。Django 的数据库 API 中有几个这样的 API, 6 每个都有自己的期望。这是完整的列表,包括每个列表的用途:

  • exactiexact—提供的值必须与数据库中的值完全匹配,iexact不区分大小写。Django 假设一个没有查找类型的过滤器表示exact,它将被传递给get_prep_lookup()
  • containsicontains—提供的值必须至少存在于数据库中的部分值中,icontains不区分大小写。
  • gtgte—数据库值必须大于提供给查找的值,而gte也允许值相等。
  • ltlte—数据库值必须比提供给查找的值小,而lte也允许值相等。
  • in—数据库值必须至少与作为查找值提供的列表中的一个值完全匹配。
  • startswithistartswith—数据库值必须以作为查找值提供的字符串开头,istartswith不区分大小写。
  • endswithiendswith—数据库值必须以作为查找值提供的字符串结尾,iendswith不区分大小写。
  • range—数据库值必须具有由作为查找值提供的开始和结束限制的二元组指定的范围。
  • yearmonthday—数据库值必须包含指定的查找值作为其年、月或日部分,具体取决于所使用的查找类型。这仅对日期有效。
  • isnull—数据库值必须等于NULL才能匹配。
  • search—数据库值必须通过全文索引搜索。这只对 MySQL 有效,并且只在数据库被修改为启用必要的索引时有效。
  • regexiregex—数据库值必须与作为查找值提供的正则表达式指定的格式匹配,iregex不区分大小写。

从某些现有字段继承的字段通常可以避免重写此方法,因为父类通常会做正确的事情。不幸的是,在其他时候,子类需要对某些查找类型进行特殊处理,这非常有用。还有一些时候,有必要完全限制某些类型的查找。

将 Python 代码作为查找过程的一部分来执行的一个有用的副作用是,它允许对该字段无效的查找抛出异常。这就像在其他地方一样,如果您引发了一个异常,它会提前退出查询,显示一条消息指出发生了什么。

WHERE’D MY ERROR GO?

不幸的是,尽管在get_prep_lookup()中引发异常是可能的——而且通常非常有用——但有时您会发现它们被抑制了。如果发生这种情况,查询似乎会执行,但是您可能会收到一个空的结果列表,而不是看到您的错误。

由于一些查询集必须在内部跳转,某些类型的错误——包括似乎是显而易见的选择使用的TypeError——被捕获和抑制,导致 Django 继续处理,尽管没有获得该字段的有效值。

为了确保错误得到最大化并按预期工作,一定要使用ValueError而不是TypeError,因为它不会陷入相同的陷阱。

get_db_prep_lookup(self,lookup_type,value,connection,prepared=False)

这与get_prep_lookup()执行基本相同的任务,除了它的输出将被直接输入数据库查询。它接收相同的参数,但添加了connectionprepared,其工作方式与传递给get_db_prep_value()的参数类似。默认实现遵从get_prep_lookup(),这对于大多数需求来说已经足够了。

处理文件

许多应用需要管理超出传统数据库存储范围的内容。除了常见的数字和字符串,还有大量其他数据格式,从音频和视频到可打印的便携文档格式(PDF)文件等等。像这样的内容不太适合直接存储在数据库中——尽管在某些情况下至少是可能的——但是将它与数据库中的其他内容联系起来仍然是有用的。

为了处理这一点,Django 提供了一个特殊的FileField,它有一些额外的方法来方便对文件的访问。它还使用本章中描述的许多钩子来存储对数据库中文件的引用,以及提供一个可以以可移植方式访问文件的特殊对象。Django 还提供了一个ImageField,它继承了FileField的大部分功能,同时添加了一些自己的功能,专门用于处理图像的特殊需求。

FileField的子类通常不需要覆盖它的许多方法,因为它们主要与所有文件类型共有的文件特性相关。这包括诸如文件名和相对路径之类的东西,它们与特定类型文件的细节没有任何关系。然而,有些,比如save_file(),可以被覆盖,以提供与特定类型文件相关的属性的特殊处理。

获取 _ 目录 _ 名称(自身)

这个方法只是返回一个相对路径,它将与文件名一起存储在数据库中。默认情况下,它会查看字段的upload_to属性来确定目录应该是什么,甚至子类也应该尊重这种行为。然而,正是如何使用该属性,子类可以定制该方法以获得更好的效果。

通常,Django 使用两条信息创建目录名:upload_to字符串本身和当前日期。文件上传的日期应用于目录名,用部分日期替换某些字符。这允许单个字段更精确地控制它们的文件存储在哪里,这有助于保持目录更小,甚至可能更好地利用磁盘容量。

然而,在子类中,基于一些其他类型的信息生成目录名可能更有用,例如多站点设置中的当前站点的域名,或者在有多个 Web 服务器共享公共存储的较大生产环境中接收上传的机器的互联网协议(IP)地址。

本质上,在这里任何事情都是公平的,只要它只需要通过访问FileField实例就可以确定的信息。可以获得当前站点或 IP 地址,而完全不考虑当前模型,当前时间也是如此。但是,其他信息,如提交文件的用户、他或她的远程计算机的 IP 地址或文件将附加到的对象,都不能通过此功能访问,因此不能使用。

当然,还有另一个选项来指定这些附加信息,但是这样做完全绕过了这个方法。通过为upload_to指定一个 callable,如 Django 的文件文档中所述, 7 目录可以根据它将附加到的对象生成,这可能包括拥有该对象的User

注意,当使用一个可调用对象作为upload_to时,该可调用对象应该返回完整的路径,包括目录和文件名,所以在这种情况下根本不会调用get_directory_name(),除非该可调用对象显式地调用它。此外,传入的请求仍然不可用,甚至对那个可调用的请求也不可用,因此基于该信息做出目录命名决策将需要一个自定义视图。

get_filename(自己,文件名)

这与get_directory_name()的工作方式非常相似,除了它负责指定路径的文件名部分而不是目录。它接收传入文件指定的原始文件名,并返回将在数据库和底层存储系统中使用的新文件名。

如果一个FileField子类需要定制一个特定文件的文件名,比如去掉某些字符或者改变文件的扩展名,那么这里就是合适的地方。这也是它接收原始文件名的原因,这样它就有办法创建一个与用户提供的文件名至少部分相关的文件名。

默认情况下,它的输出与get_directory_name()的输出相结合,形成要存储在数据库中并传递给存储系统的完整路径。然而,和它的对应物一样,只有当字段的upload_to参数不可调用时,这种情况才成立。如果指定了 callable,它负责指定整个路径,包括文件名。因此,在这种情况下,只有当upload_to callable 明确请求时,这个方法才会被调用。

生成文件名(自身,实例,文件名)

这是用于生成完整路径的默认方法。它使用相同的函数签名作为可调用的upload_to参数,因为它扮演完全相同的角色。事实上,在FileField内部,所有用于生成文件文件名的引用都会引用这个方法;如果一个可调用函数被提供给了upload_to,它只是被赋予了相同的名字,取代了默认的行为。

默认行为是使用os.path.join()来组合get_directory_name()get_filename()方法的输出,忽略作为参数提供的模型实例。如果一个FileField子类需要一次指定文件的完整路径的能力,那么这个方法就是合适的地方。

当然,请记住,如果 callable 作为upload_to参数提供,这个方法将被替换。不管FileField子类提供什么行为,这都是正确的;特定实例的需求总是胜过其类的行为。因此,尽管覆盖这种行为可以提供一个更有用的缺省值,但这并不能完全消除单个开发人员替换它的能力。

save_form_data(自身,实例,数据)

这是表单的一种实用方法,用作保存与模型实例关联的文件的快捷方式。它接受该字段附加到的模型实例,以及由表单提供的上传文件数据。默认情况下,它只是从上传的文件对象中提取必要的信息,并将其传递给标准的文件保存方法。

instance参数是定义了FileField的模型的一个实例,而data参数是一个UploadedFile对象,如第八章所述。上传的文件包含一个name属性,该属性包含文件名和一个read()方法,该方法用于访问文件的内容,以便可以正确保存。

因为这是 Django 本身的大多数领域处理文件的主要方式,所以覆盖这个字段提供了一个极好的机会来绑定基于特定字段类型的扩展功能。比如 Django 自己的ImageField就以此为契机,将一张图片的宽度和高度存储在单独的字段中,这样就可以直接在数据库中对它们进行索引和搜索。其他文件类型可以采用相同的方法,将文件的某些属性存储在其他字段中,以便于以后访问。

因为这个方法可以访问整个文件的内容,所以可以将这些内容传递给大多数处理文件的库。任何可以读取打开的文件对象的东西都可以处理上传的内容,只需将它包装在一个StringIO 8 对象中。这样,就可以访问这些内容,而不必先将它们写入存储系统,然后再将它们读回来。

删除文件(自己、实例、发件人)

虽然这可能看起来只是删除文件的一种方式,但它实际上有一个非常特殊的目的,这通过出现一个sender参数来暗示。FileFieldcontribute_to_class()方法将该方法设置为post_delete信号的监听器。它并不打算被单独调用,而是在每次删除带有FileField的模型实例时被调用。正如对post_delete的描述,instance参数是刚刚被删除的对象,sender参数是该实例的模型类。

触发时,它会检查指定实例上该字段引用的文件是否应被删除。毕竟,如果没有其他实例引用同一个文件,并且它不是新实例的默认值,那么很可能没有对该文件的引用。在这些情况下,文件将从存储系统中永久删除。

覆盖它的用途很清楚,因为何时删除文件的逻辑直接包含在这个方法中。如果一个FileField子类需要有不同的规则,简单地覆盖这个方法就足够了。

一个明显的例子是,由于历史原因,文件是否应该总是保留,即使在与它们相关联的模型实例被删除之后。提供该行为很简单,只需定义该方法的一个空实现。

from django.db import models

class PermanentFileField(models.FileField):

def delete_file(self, instance, sender, **kwargs):

pass

当然,这也有其他可能的用例,但是这些用例的细节将在很大程度上取决于单个应用的需求。

属性 _ 类别

作为一个简单的属性,而不是一个方法,attr_class可能看起来不会提供太多的功能或灵活性。令人欣慰的是,外表往往具有欺骗性,因为它实际上是一些非常有用的功能的门户。attr_class属性被设置为一个类,当在 Python 中被引用时,该类将用于表示字段的值。这意味着这个简单属性的值实际上是指定输入特定FileField实例的数据在公共 API 上可用的特性的主要方式。

下一节描述默认情况下为此属性指定的类的行为,以及如何重写其方法以提供附加功能。

自定义文件类

当一个模型定义了一个FileField时,在实际模型实例上作为属性可用的值是一个专门为管理文件而设计的特殊对象。位于django.db.models.fields.filesFile类提供了许多独立于平台和存储的方法,用于访问文件的内容和内容的属性,以及保存新文件和删除现有文件。

因为它是用于访问文件的面向公众的 API,所以为需要经常引用的具有共同特性的文件类型提供附加功能通常非常有用。这提供了一种很好的、干净的、面向对象的方法来将公共代码封装在一个地方,而不是要求应用的其余部分一遍又一遍地编写它。

例如,Django 自己的ImageField提供了自己的子类ImageFile,它包含了访问图像宽度和高度的额外方法,以及缓存图像以加快后续访问的速度。这是一个很好的例子,说明提供这种额外的功能是多么容易。

但是,除了提供新的方法之外,还有许多现有的方法可以从被重写中受益。这些不太可能直接使用,但是正如ImageFile所示,它们可以用于执行一些重要的任务,比如更新缓存值或者使缓存值失效。

在很大程度上,接下来描述的方法直接映射到第八章中描述的文件存储方法。主要区别在于,它们特定于特定的文件类型,并且可以针对该文件类型特有的方面进行定制,而存储系统只是设计用于处理文件,而不考虑要处理什么类型的内容。

路径(自身)

如果文件存储在本地文件系统上,这将返回文件的路径。对于存储在其他后端的文件,不能用 Python 内置的open()函数访问,这将引发一个AttributeError,因为相应的方法在相关的存储系统对象上不可用。

对于那些在引入这个新的文件处理系统之前编写的项目,这主要是作为与旧版本 Django 的兼容层提供的。在现实世界中,为较新版本的 Django 编写的项目应该避免使用这种方法,而是使用本节中列出的open()方法以更可移植的方式访问文件。覆盖它也没什么用,所以这里列出它是为了和 API 的其他部分一起完整。

网址(自我)

此方法返回可以在 Web 上检索文件的 URL。它可能由 Django 项目本身提供,一个由网站所有者运营的媒体服务器,或者甚至是由第三方运营的存储服务。这个 URL 来自哪里的确切细节是由存储系统指定的,所以这个方法是访问文件的 URL 的一种可移植的方式。

在大多数情况下,忽略这一点没有什么好处,但是根据具体情况,有几个理由这样做。一个例子可能是管理具有特定结构的 HTML 文件的FileField子类,因此 URL 可能包含一个名称引用,以将浏览器指向文件中的特定点。

尺寸(自身)

这将检索底层文件的大小,并将其缓存以供将来参考。虽然这是一个非常有用的特性,但是在子类中覆盖它没有什么价值。文件大小的本质是它不会随文件类型而变化,并且实际上无法定制如何获得大小,所以这里只是为了完整起见才包括它。

打开(self,mode='rb ')

这将检索文件的内容并返回一个打开的文件或类似文件的对象,从而允许访问该文件。这是以可移植方式访问文件内容的首选方法,因为文件的大部分功能都是通过存储系统实现的。

mode属性采用与 Python 自带的open()函数、 9 相同的选项,可以用来打开文件进行读或写访问。重写此方法的一个用途是更改默认的访问模式,但仅用于更改默认情况下是否应以二进制模式打开它。默认情况下,至少应该打开文件进行读取,而不是写入。

子类化它的另一个潜在原因是为返回的类似文件的对象提供自定义行为。默认情况下,该方法将返回存储系统返回的任何对象,但是特定的文件类型可能会用于定制该对象上的方法,例如使用write()close()来改变文件写入的方式和时间。因为这个方法负责返回一个打开的类似文件的对象,所以它可以将真正的类似文件的对象包装在另一个对象中,在完成任何需要做的额外工作后传递给真正的对象。

保存(自己,姓名,内容,保存=真)

顾名思义,这将一个新文件保存到存储系统,替换模型实例上当前的文件。参数应该是不言自明的,其中name是新文件应该保存的名称,content是使用该名称写入的文件的实际内容。

  • 当然,文件名中的无效字符或同名的现有文件可能会导致文件名被存储系统更改。这种变化将反映在存储在模型实例上的文件名中。
  • 然而,save的论点值得进一步解释。因为这保存了一个与模型实例相关的文件,所以新的文件名将存储在该实例上,以供将来参考。然而,立即将更改提交给数据库并不总是有益的。
  • 默认情况下,它会立即保存实例,但是如果将save设置为False,则会绕过它,允许在提交到数据库之前进行其他更改。但是,在执行此操作时要小心。该文件已经提交到存储系统,因此如果最终未能使用新文件名保存实例,将导致文件中没有对它的引用。
  • 覆盖它可以提供一种方法来定制或记录将要使用的文件名,更改默认的数据库提交行为,或者最常见的是,检索有关文件内容的信息并相应地更新任何缓存的信息。默认的File对象为文件大小做这件事,并且ImageFile也更新它的维度缓存。

删除(self,save=True)

同样不言自明的是,这将直接从存储系统中删除文件,而不管使用的是哪个存储系统。它还从模型实例中删除文件名,这样它就不再引用该文件。

save参数的工作方式就像来自save()方法的参数一样,决定模型实例是否被保存。同样像save(),如果提供了False,确保实例最终被保存是很重要的。否则,它将包含对已被删除的文件的引用。更糟糕的是,如果另一个实例保存了一个同名的文件,那么来自第一个实例的引用将不再是孤立的,而是实际上完全指向错误的文件。

覆盖它提供了与覆盖save()相同的大部分好处,因为它能够删除任何缓存的信息,这样如果以后访问它就不会引起混乱。

信号

第二章描述了与 Django 捆绑在一起的信号调度系统,以及信号一般是如何工作的。如前所述,信号可以在任何 Python 模块中创建和使用,并且可以用于任何目的。对于处理模型,提供了开箱即用的几种信号,可以在许多情况下使用。

下面的信号在django.db.models.signals都是可用的,每个都将模型类作为标准的sender参数发送给监听器。此外,许多信号包含一个模型实例作为附加参数。这些和其他附加参数在这里列出的每个信号的描述中有详细说明。

班级 _ 已准备

当 Django 的ModelBase元类处理完一个模型类时,这个信号被触发,表明这个类已经完全配置好,可以使用了。因为 Python 一遇到类声明,元类就开始运行,所以在 Python 继续处理包含该声明的模块之前,class_prepared就会被触发。

然而,需要考虑的一个重要注意事项是,这个事件正好发生在模型注册到AppCache之前。因此,如果一个class_prepared的监听器通过AppCache检查到目前为止已经被处理的模型,发出信号的模型将不会出现。在流程的这一点上,检查应用缓存可能有一些用途,但是如果没有完整的应用缓存,它的价值就非常有限。

与本节中列出的大多数其他信号不同,class_prepared只发送标准的sender参数。由于在触发信号的时间点上没有任何可用的实例,并且新模型类上的_meta属性包含了关于它是如何被声明的所有信息,所以模型本身就足以获得在那个时间点上可用的所有信息。

>>> from django.db import models

>>> def listener(sender, **kwargs):

...     print('%s.%s' % (sender._meta.app_label, sender._meta.object_name))

...

>>> models.signals.class_prepared.connect(listener)

>>> class Article(models.Model):

...     title = models.CharField(max_length=255)

...     class Meta:

...         app_label = 'news'

...

news.Article

像所有的信号一样,class_prepared的监听器可以注册一个特定的监听模型,也可以不注册,尽管这似乎是不可能的。毕竟,如果侦听器必须在发出信号之前注册,而信号是在 Python 甚至继续模块的其余部分之前发出的,那么它怎么可能注册到要侦听的类呢?即使可以,它又能达到什么目的呢?

这两个问题的答案都是contribute_to_class()。记住,模型上的属性有机会定制它们如何应用于模型。当遇到一个具有contribute_to_class()方法的对象时,它被调用,而不是通常的setattr(),在那里它被传递模型类和属性名,允许对象执行它想要的任何功能。

这里的关键是contribute_to_class()接收模型类作为参数。这是为正在处理的类注册一个class_prepared监听器的绝佳机会。事实上,根据手头的需要,这不仅是可能的,而且可能是绝对必要的。

考虑这样一种情况,一个类似字段的对象需要知道它所连接的模型的所有信息,以便正确地配置自己。由于不能保证在对相关对象调用contribute_to_class()时所有其他字段都已处理完毕,因此有必要推迟配置的剩余部分,直到该类完成处理。

前初始化和后初始化

当一个模型被实例化时,pre_init在任何其他工作被执行之前触发。它甚至在传递到模型中的任何参数被分配给适当的属性之前就被调度。这是一个在实际发生之前检查将分配给实例的参数的好机会,特别是因为这允许侦听器在遇到可能由指定的参数导致的任何错误之前触发。

因为这发生在任何字段值被填充到对象本身之前,所以当触发信号时,它不会发送新的对象。相反,除了sender之外,它还传递另外两个参数,这两个参数对应于传递给模型的位置和关键字参数。

  • args—包含传递给模型构造函数的位置参数的元组
  • kwargs—包含传递给模型构造器的关键字参数的字典

请注意,尽管这些名称与《??》第二章中描述的多余参数技术的名称相同,但它们是作为显式关键字参数传递给听者的,而不是使用***。侦听器必须显式定义这些参数,以便它们能够正常工作。

>>> from django.db.models.signals import pre_init

>>> from news.models import Article

>>> def print_args(sender, args, kwargs, **signal_kwargs):

...     print('%s(*%s, **%s)' % (sender._meta.object_name, args, kwargs))

...

>>> pre_init.connect(print_args, sender=Article)

>>> article = Article(title=u'Testing')

Article(*(), **{'title': u'Testing'})

类似地,post_init作为模型实例化过程的一部分被触发,但是在结束时而不是开始时,一旦所有的参数都被映射到基于模型上定义的字段的适当属性。因此,顾名思义,对象在这一点上是完全初始化的。

因此,当post_init触发时,它会被传递给完全配置的模型实例以及标准的sender,后者是模型类。新对象作为instance参数传递给监听器,监听器可以根据应用对其进行任何必要的处理。

>>> from django.db.models.signals import post_init

>>> from news.models import Article

>>> def print_args(sender, args, kwargs, **signal_kwargs):

...     print('Instantiated %r' % instance)

...

>>> post_init.connect(sender=Article)

>>> article = Article(title=u'Testing')

Instantiated <Article: Testing>

预保存和后保存

当一个模型实例被提交到数据库时,Django 提供了两种方法来连接到这个过程,在开始和结束的时候。因此,两者之间的主要区别是在对象提交到数据库之前调用了pre_save,而在之后调用了post_save。根据应用的需要,这种简单的区别可能非常重要。

当被pre_save触发时,监听器接收模型类作为sender,以及模型实例作为instance。这允许监听器在实例到达数据库之前访问甚至修改将要保存的实例。这是一种为第三方应用提供的模型提供或覆盖默认参数的有用方法。

另一方面,post_save是在执行保存之后调用的,并且实例已经提交到数据库。这在两个方面都是有用的一步,因为它不仅确保了数据确实存在于数据库中,这在处理相关模型时是必要的,而且它还发生在 Django 决定是向数据库中插入新记录还是更新现有记录之后。

除了 sender 和 instance 参数的工作方式与在pre_save中相同之外,post_save的监听器可以接收另一个参数。created参数是一个布尔值,表示是否必须从头开始创建实例。值True意味着它是新插入到数据库中的,而False意味着已有记录被更新。当使用post_save信号跟踪数据库变化时,这是一个重要的区别,可以用来确定其他应用的行为。要了解这一点,请参阅本书第十一章中的历史示例。

因为模型管理器的create()方法实际上确实向数据库提交了一个新实例,所以它触发了这两个信号。也可以假设任何时候使用create()时,created参数都将是True,但是请记住,很可能还有其他时候该参数也是True

>>> from django.db.models import signals

>>> from news.models import Article

>>> def before(instance, **kwargs):

...     print('About to save %s' % instance)

...

>>> signals.pre_save.connect(before, sender=Article)

>>> def after(instance, created, **kwargs):

...     print('%s was just %s' % (instance, created and 'created' or 'updated'))

...

>>> signals.post_save.connect(after, sender=Article)

>>> Article.objects.create(title='New article!')

About to save New article!

New Article! was just created<Article: New article!>

A NOTE ABOUT COMBINING PRE_SAVE() AND POST_SAVE()

pre_savepost_save还有一个非常重要的区别,因为它们并不总是被称为一对。因为pre_save是在流程开始时触发的,所以您可以可靠地假设每次启动save()时都会调用它。但是,post_save只在最后发生,所以如果保存过程中出现任何问题,post_save不会被触发。

这是一个重要的区别,因为为模型保存信号注册一对侦听器似乎很方便,期望每次都调用这两个侦听器。虽然对于大多数情况来说这可能是真的,当然当没有出错时,有时事情确实会出错。示例包括具有重复主键或其他唯一列的条目、数据类型错误或数据库连接超时。

在需要这种类型行为的情况下,唯一合理合理的方法是覆盖模型上的save()方法。这允许在实际的数据库交互之前和之后运行定制代码,但是它也提供了一种方法来识别在这个过程中发生的问题。此外,它允许代码有更好的机会更充分地将两部分功能配对,因为如果确实出错了,就更容易识别,因此任何未完成的操作都可以被取消。

删除前和删除后

类似于上一节的精神,pre_deletepost_delete是与模型实例的删除相关的一对信号。除了都只提供了senderinstance参数之外,它们的功能与储蓄功能几乎相同。

使用post_delete时,请记住传递给监听器的实例已经从数据库中删除了,因此它的许多方法在使用时都会引发异常。如果它以前与其他模型的实例相关,则尤其如此。当post_delete被触发时,这些关系将会丢失,所以对这些情况的任何处理都应该在pre_delete中完成,或者通过覆盖模型上的delete()方法来完成。如果你覆盖了模型的delete()方法,你需要确保在调用父类的delete()方法之前访问模型及其关系。一旦你通过父类删除它,你将处于与使用post_delete信号时相同的情况。

此外,因为实例将被删除,所以它的主键值将不再与数据库中的任何内容匹配。然而,为了更准确地跟踪哪个对象被删除,主键值在实例上保持不变,并且可以使用本章前面描述的pk快捷方式读取。

post_syncdb

与特定模型无关,post_syncdb作为syncdb管理命令的正常流程的一部分被触发。它为应用提供了一种方法来识别应用的模型何时被安装到数据库中,以便根据它们的定义执行其他任务。

虽然可能还有其他用途,但是post_syncdb的主要用途是在应用的模型第一次安装到数据库中时配置应用本身,或者识别正在安装的其他应用,并采取适当的措施。在 Django 内部,这两种类型的功能都有例子。

  • 一旦模型安装完毕,django.contrib.auth应用就使用它将新模型的权限安装到数据库中,如果auth应用本身刚刚安装完毕,它还会创建一个新的超级用户。
  • django.contrib.contenttypes应用使用它来维护它自己的正在使用的模型的记录,因此它可以提供与任何已安装模型的关系。
  • django.contrib.sites应用使用它为所有使用该应用的新项目安装一个默认站点。

使post_syncdb相当有效的关键是它为伴随所有信号的sender参数使用不同类型的值。这个信号不是使用特定的模型,而是发送应用的models模块,这是 Django 用来标识应用的对象。这允许为所有应用配置侦听器,或者只为注册它的应用配置侦听器。

每次执行命令时,INSTALLED_APPS设置中列出的所有应用都会发出一个post_syncdb信号,即使没有任何变化。因此,除了senderpost_syncdb的听众还会收到三个额外的参数来更详细地指出syncdb被调用的情况,并帮助控制他们的响应行为。

  • app—应用对象(其models模块)代表刚刚与数据库同步的应用。这与sender参数完全相同,但是在这里命名为app是为了让监听器函数更具可读性。
  • created_models—Pythonset,包含在syncdb执行期间实际安装到数据库中的应用的所有模型。这就是监听器如何识别那些新模型的方法,这通常是post_syncdb处理者需要知道的最重要的事情。这将一直被提供,但是在没有新东西的应用中,它将只是一个空的set
  • verbosity—一个整数,标识执行syncdb的用户所请求的详细级别。有效值为012,其中0为最小输出(大多数情况下没有输出),1为正常输出,2为全部输出(包括指示正在执行的操作的消息,即使它们不需要用户输入)。post_syncdb的监听器应该总是准备好输出它们正在执行的活动,并且应该使用这个参数来确定何时应该显示不同的消息。

from django.db.models import signals

def app_report(app, created_models, verbosity, **kwargs):

app_label = app.__name__.split('.')[-2]

if verbosity == 0:

# Don't do anything, because the

# user doesn't want to see this.

return

# Get a list of models created for just the current application

app_models = [m for m in created_models if m._meta.app_label == app_label]

if app_models:

# Print a simple status message

print('Created %s model%s for %s.' % (len(app_models)

len(app_models) > 1 and 's' or ''

app_label))

if verbosity == 2:

# Print more detail about the

# models that were installed

for model in app_models:

print('  %s.%s -> %s' % (app_label

model._meta.object_name

model._meta.db_table))

elif verbosity == 2:

print('%s had no models created.' % app_label)

signals.post_syncdb.connect(app_report)

用于post_syncdb监听器的代码通常放在应用的management包中,每当manage.py用于包含该应用的项目时,该包就会自动加载。这确保了它不会在不需要的情况下被不必要的加载,同时也确保了它在必要的时候被加载。此外,因为是 Python,所以您的management包中的代码也可以做其他事情,比如检查INSTALLED_APPS设置并决定是否应该注册监听器。

应用技术

考虑到单个模型可以使用大量工具来定制它们的行为、它们与数据库的交互以及与数据库相关联的字段,选项几乎是无限的。接下来的技术仅仅代表了可能性的一小部分。

按需加载属性

当处理特定类型的数据时,构建一个复杂的 Python 对象来表示给定值有时会非常昂贵。更糟糕的是,应用的某些部分甚至可能不使用该对象,尽管模型的其余部分可能是必需的。现实世界中的一些例子是复杂的地理表示或嵌套对象的大树。

在这些情况下,我们必须能够在必要时访问完整的对象,但是如果不使用该对象,那么不构造它对性能非常重要。理想情况下,当模型被实例化时,数据将从数据库中加载,但是原始值将位于实例中,而不会被加载到整个对象中。当属性被访问时,它将在该点被构造,然后被缓存,这样以后的访问就不必不断地重新构造对象。

再次回顾《??》第二章,描述符是完成这项任务的完美工具,因为它们允许代码在访问属性的准确时刻运行。必须注意确保完整构造的对象被正确缓存以备将来使用,但是通过使用单独的nameattname,这也相当简单。

为了说明这在实践中是如何工作的,考虑一个用于存储和检索任意 Python 对象的 pickled 副本的字段。没有办法预先知道 Python 表示会有多复杂,所以在这种情况下,理想的做法是推迟对象的构造,直到真正需要的时候。

存储原始数据

第一步是告诉 Django 如何使用标准字段管理数据库中的原始数据。由于腌制对象只是字符串,某种形式的文本字段显然是谨慎的,而且由于没有办法预先知道腌制表示将有多大,几乎无限的TextField似乎是一个显而易见的选择。

当然,考虑到这个新领域还会有一些额外的工作要做,单靠TextField是不够的。相反,我们将创建一个继承数据库功能TextField的子类,同时允许在必要的地方进行额外的定制。因为字段和其他任何类一样都是 Python 类,所以它的工作方式和您预期的一样,但是有一点不同。为了使用与其他 Python 代码交互不同的值与数据库交互,attname属性需要与name属性不同。这是由一个自定义的get_attname()方法控制的。

from django.db import models

class PickleField(models.TextField):

def get_attname(self):

return '%s_pickled' % self.name

仅此一项就足以为数据库正确设置字段。在这一点上,甚至可以将一个PickleField实例分配给一个模型,并将其与数据库同步,并且所创建的列在这个示例的持续时间内将完全可用。当然,它只管理到目前为止的原始数据;它根本不能处理真正的 Python 对象,更不用说处理必要的酸洗和拆洗了。

酸洗和反酸洗数据

为了在完整的 Python 对象和可以存储在数据库中的字符串表示之间进行转换,Python 的 pickling 模块 10 将是首选工具。实际上 Python 为此提供了两个独立的模块:cPickle,用 C 编写以提高性能,和pickle,用纯 Python 编写以提高灵活性和可移植性。两者之间有一些细微的区别, 11 但是可以互换使用。

有两个可用的模块使得导入比平常更棘手。由于显而易见的原因,拥有更好的性能是非常有价值的,但是 Python 和 Django 的一个关键方面是跨多个平台和环境使用的能力。因此,当希望导入一个酸洗模块时,最好先尝试更高效的模块,必要时再使用更便携的模块。

try:

import cPickle as pickle

except ImportError:

import pickle

有了一个可用的pickle模块,我们可以给PickleField实际上提取和反提取数据的能力。通过提供几个基本方法,可以以更加面向对象的方式与底层模块进行交互。此外,可以有把握地假设,当准备提交到数据库时,字段的值将是完整的 Python 对象,这显然必须经过处理。

另一方面,当使用 QuerySet 的filter()方法与数据库中的值进行比较时,经过酸洗的数据将毫无用处。从技术上讲,将查询的值与数据库中找到的值进行比较是可能的,但是比较的是经过提取的值,而不是原始的 Python 对象,这可能会导致不正确的结果。

更重要的是,即使保证了在必要时可以正确地解除对一个已标记值的拾取,但是在不同的场合或者可能在不同的机器上标记的同一个值很可能会有不同的字符串来表示原始对象。这是酸洗工作方式的副作用,必须加以考虑。

考虑到这一点,允许对经过标记的数据进行任何类型的比较都是不合理的,所以如果试图进行这样的比较,应该抛出一个异常。如本章前面所述,该行为由get_db_pre_lookup()控制,它可以被覆盖以抛出这样的异常。到目前为止,整个字段如下:

class PickleField(models.TextField):

def pickle(self, obj):

return pickle.dumps(obj)

def unpickle(self, data):

return pickle.loads(str(data))

def get_attname(self):

return '%s_pickled' % self.name

def get_db_prep_lookup(self, lookup_type, value):

raise ValueError("Can't make comparisons against pickled data.")

注意picklecPickle只支持 pickled 数据字符串作为纯字节字符串,不支持完整的 Unicode 字符串。由于 Django 中的所有内容都尽可能地被强制转换成 Unicode,包括从数据库中检索,unpickle()需要采取额外的步骤将其强制转换回字节字符串,以便正确地解包。

WHY THE EXTRA METHODS?

当 pickling 模块已经在模块的名称空间中可用时,定义单独的pickle()unpickle()方法似乎有点奇怪。毕竟,对于开发人员来说,这不仅需要编写额外的代码行,而且 Python 还需要执行额外的函数调用来完成工作,这稍微减慢了速度,而且看起来没有必要。

这样做的最大好处是,如果任何其他应用需要继承PickleField的子类,并且希望覆盖数据是如何被提取和取消提取的,那么拥有显式的方法会使这个过程变得更加容易。它们可以像普通的一样被覆盖,只要其余的PickleField只是引用这些方法,子类就可以很好地工作。

这让我们更近了一步,因为PickleField可以正确地在数据库中存储值。然而,它仍然没有解决将数据加载到 Python 对象中的主要问题,并且只在真正必要时才这样做。

按需拆卸

如果我们不关心性能,很容易在to_python()方法中执行拆包步骤,只需使用SubfieldBase来确保每次实例化一个对象时都发生拆包,而不管它来自哪里。不幸的是,对于那些不能访问该字段的情况,这会导致大量不必要的开销,所以仍然值得按需加载它,只有在请求时才加载。

如前所述,Python 描述符特别适合这种场景。它们在访问属性时被调用,并且可以在那时执行自定义代码,用为手边的任务设计的东西替换标准的 Python 行为。

第一步是确定如何实例化描述符,这也意味着确定完成工作需要哪些数据。为了正确地从模型实例中检索原始数据,它需要访问 field 对象,从中可以收集字段本身的名称。

class PickleDescriptor(property):

def __init__(self, field):

self.field = field

它将存储对该字段所有功能的引用,这些功能在以后会很有用。有了这些,就有可能编写__get__()__set__()方法,从长远来看,它们将真正完成艰巨的工作。实际上,__set__()是两者中比较容易实现的;它只需将原始数据直接分配给实例的名称空间。

def __set__(self, instance, value):

instance.__dict__[self.field.name] = value

setattr(instance, self.field.attname, self.field.pickle(value))

有了这些,整个过程中最棘手的部分就是描述符的__get__()方法,它必须能够执行以下任务才能正常工作。

  • 确定是否需要创建完整的 Python 对象。
  • 仅在必要时,通过分离原始数据来生成完整的 Python 对象。
  • 缓存生成的 Python 对象以备将来使用。
  • 如果对象的缓存副本可用,则返回它,否则返回新的副本。

最后一点实际上有点转移注意力,因为很容易确保在方法的末尾有一个 Python 对象可用,并且只返回它,而不考虑它来自哪里。其余的,虽然看起来像一个洗衣清单,但是用一个小的、可读的方法来执行所有这些任务并不困难。

def __get__(self, instance, owner):

if instance is None:

return self

if self.field.name not in instance.__dict__:

# The object hasn't been created yet, so unpickle the data

raw_data = getattr(instance, self.field.attname)

instance.__dict__[self.field.name] = self.field.unpickle(raw_data)

return instance.__dict__[self.field.name]

这个方法如何执行每个需求应该是相当清楚的。第一个块检查来自模型类的访问,引发适当的异常。第二个块执行另外三项任务,首先检查缓存副本的存在,否则继续执行。然后,它在一行中执行两次以上的操作,如果缓存中没有填充原始数据,就将它存储在缓存中。最后,它只是返回缓存中的内容,而不管该方法开始时它是否在缓存中。

把这一切放在一起

要使整个工作正常进行,唯一要做的就是在正确的时间获取模型上的描述符,这样在访问属性时就可以调用它了。这正是contribute_to_class()的意图,Django 已经为第三方代码提供了一种方式,比如这样,来绑定到模型创建过程中。只要确保总是在父类上调用conribute_to_class()方法,以确保所有标准的 Django 功能以及应用更特殊的需求都得到应用。

def contribute_to_class(self, cls, name):

super(PickleField, self).contribute_to_class(cls, name)

setattr(cls, name, PickleDescriptor(self))

有了所有这些,我们总共有三个 import 语句、两个新类和一个新字段来执行一个非常有用的任务。这只是如何使用这种技术的一个例子,还有很多使用复杂 Python 数据结构的应用。从这个例子中得到的重要的东西是如何在必要的时候使用描述符来填充那些复杂的对象,这在不经常使用描述符的情况下是一个很大的优势。

try:

import cPickle as pickle

except ImportError:

import pickle

from django.db import models

class PickleDescriptor(property):

def __init__(self, field):

self.field = field

def __get__(self, instance, owner):

if instance is None:

return self

if self.field.name not in instance.__dict__:

# The object hasn't been created yet, so unpickle the data

raw_data = getattr(instance, self.field.attname)

instance.__dict__[self.field.name] = self.field.unpickle(raw_data)

return instance.__dict__[self.field.name]

def __set__(self, instance, value):

instance.__dict__[self.field.name] = value

setattr(instance, self.field.attname, self.field.pickle(value))

class PickleField(models.TextField):

def pickle(self, obj):

return pickle.dumps(obj)

def unpickle(self, data):

return pickle.loads(str(data))

def get_attname(self):

return '%s_pickled' % self.name

def get_db_prep_lookup(self, lookup_type, value):

raise ValueError("Can't make comparisons against pickled data.")

def contribute_to_class(self, cls, name):

super(PickleField, self).contribute_to_class(cls, name)

setattr(cls, name, PickleDescriptor(self))

在运行时动态创建模型

第二章展示了 Python 类如何像其他任何类一样是真正的对象,并且可以在运行时通过使用内置的type()构造函数和传递一些关于如何定义它的细节来创建。由于 Django 模型实际上只是以特定方式声明的 Python,因此有理由期望它们也可以在运行时使用相同的特性来创建。必须小心,但这在各种情况下都是非常有用的技术。

诀窍是记住 Python 如何处理类,Django 如何处理它的模型。第二章已经说明了完成这项工作所必需的基本工具,所以现在的问题只是将它应用到 Django 模型的具体细节上。有几件事情将模型与其他 Python 类区分开来:

  • 所有型号子类django.db.models.Model
  • 字段在模型的声明中被指定为类属性。
  • 额外的选项在模型声明中的Meta类中指定。

有了这些概述的需求,将模型声明映射到type()的参数就相当容易了。特别要记住,构造一个类需要三个参数:namebasesattrs。模型的名字被清晰地映射到name,而models.Model的单个子类可以被包装在一个元组中并传递给bases。类声明的其余部分将放入attrs,包括一个用于任何附加模型级配置选项的Meta类。

第一遍

为了第一次了解这个函数可能是什么样子,让我们从类创建的最基本的方面开始,然后从那里开始。首先,考虑一个生成具有正确名称和基类的类的函数,以说明动态创建一个类并返回它供其他地方使用的基本技术。

from django.db import models

def create_model(name):

return type(name, (models.Model,), {})

不幸的是,这实际上有点过于简单化了。在 Python 中尝试这样做将会产生一个KeyError,因为 Django 期望属性字典包含一个__module__键,其值是定义模型的模块的导入路径。这通常由 Python 为源文件中定义的所有类自动填充,但是因为我们是在运行时生成模型,所以它是不可用的。

这只是动态模型必须面对的一个小细节,没有办法完全避免它。相反,create_model()需要被更新以直接提供一个__module__属性。这也是为什么把这段代码放在一个地方是个好主意的另一个例子;想象一下,每次需要动态模型时都必须处理这个问题。下面是包含类的模块路径的样子:

def create_model(name, module_path):

return type(name, (models.Model,), {'__module__': module_path})

现在它可以接受一个模块路径,让 Django 高兴。只要模块路径已经被导入,它就能让 Django 高兴,这意味着它必须实际存在。在正常情况下,模型的__module__属性被设置为定义它的模块的路径。因为模型只会在执行模块时被处理,所以总是保证模块存在并且已经被成功导入。毕竟,如果没有的话,这个模型会在第一时间被发现。

现在,由于模块路径的唯一要求是它是有效的并且已经被导入,Django 自己的django.db.models将成为一个合理的候选。当然,应该在适当的时候覆盖它,但是在事情开始之前,这是一个不错的默认设置。

def create_model(name, attrs={}, module_path='django.db.models'):

attrs = dict(attrs, __module__=module_path)

return type(name, (models.Model,), attrs)

显然,这些动态模型在很大程度上改变了事物,绕过了 Python 通常如何处理这样的过程。__module__问题只是遇到的第一个问题,也是最容易解决的问题之一。值得庆幸的是,即使还有一些其他的问题需要处理,如果使用得当,也是值得的。

这个基本示例的下一步是包含一个属性字典,这些属性就像是直接在类定义中声明的一样。这将允许在模型中包含字段,以及自定义管理器和通用方法,如__unicode__()。因为我们已经传递了一个用作属性的字典,所以向该字典分配附加项是一个简单的过程。

def create_model(name, attrs={}, module_path='django.db.models'):

attrs = dict(attrs, __module__=module_path)

return type(name, (models.Model,), attrs)

通常,提供一个可变对象(比如字典)作为默认参数是不明智的,因为对它的修改会影响函数未来的所有执行。然而,在本例中,它仅用于填充一个新的字典,并立即被新的字典替换。正因为如此,使用默认参数是安全的,这样可以保持方法的简洁。

到目前为止,我们已经建立了一个 3 行函数来创建具有任意数量属性的基本模型,然后可以在 Django 的其他领域中使用。从技术上讲,这个函数本身可以用来生成任何可以想象的模型,但是它已经为设置__module__提供了一个快捷方式,所以通过Meta内部类为设置模型配置提供另一个快捷方式是有意义的。这样,创建模型的代码就不必直接设置那个类。

添加模型配置选项

Django 模型通过一个名为Meta的内部类接受配置,该类包含所有指定选项的属性。这听起来应该很熟悉,因为这基本上也是模型本身所做的。不幸的是,由于 Django 处理Meta类的方式,我们不得不采用不同的方法。

Meta中定义的属性被传递到一个特殊的Options对象中,该对象位于django.db.models.options。作为这个过程的一部分,Options确保没有提供它不知道如何处理的属性。不幸的是,因为Meta是一个类的事实只是将它的名称空间从主模型中分离出来的一种方式。Options只知道如何处理旧式 Python 类——即不从内置object类型继承的类。

这是一个重要的区别,因为直接调用type()会创建一个新样式的类,即使它不是从object或者任何子类继承的。这最终在类上创建了两个额外的属性,Options不知道如何处理,所以它引发了一个TypeError来指出这个问题。创建一个Meta类有两种选择:删除额外的属性或者使用其他方法创建一个旧式的类。

虽然可以删除冒犯Options的属性,但更好的办法是提供它所期望的:一个旧式的类。显然,使用type()是不可能的,这让我们只能使用标准语法声明一个类。因为这甚至在函数中也是可能的,而且它的名称空间字典可以用新的属性更新,所以这是解决这个问题的一个不错的方法。

from django.db import models

def create_model(name, attrs={}, meta_attrs={}, module_path='django.db.models'):

attrs['__module__'] = module_path

class Meta: pass

Meta.__dict__.update(meta_attrs, __module__=module_path)

attrs['Meta'] = Meta

return type(name, (models.Model,), attrs)

这将接受两个属性字典,一个用于模型本身,另一个用于Meta内部类。这允许随时创建完全定制的 Django 模型。虽然目前这看起来是一个相当抽象的概念,但是请参见第一章中的完整例子,了解如何在实践中自动记录模型的所有变更。

现在怎么办?

有了 Django 模型的坚实基础,下一步是编写一些允许用户与这些模型交互的代码。下一章将展示视图如何为您的用户提供对这些模型的访问。

Footnotes 1

1 http://prodjango.com/sql-injection/

2

2 http://prodjango.com/model-inheritance/

3

3 http:/prodjango.com/serialization/

4

4 http://prodjango.com/timedelta/

5

5 http://prodjango.com/postgresql-interval/

6

6 http://prodjango.com/db-api/

7

7 http://prodjango.com/file-api/

8

8 http://prodjango.com/stringio/

9

9 http://prodjango.com/open/

10

10 http://prodjango.com/pickle/

11

11 http://prodjango.com/cpickle/

四、URL 和视图

Abstract

这本书的大部分内容被分成了相当独立的章节,但这本书涵盖了两个看似不相关的概念,因为它们彼此非常依赖。URL 是站点的主要入口点,而视图是响应输入事件的代码。视图中发生的事情是非常开放的。除了接受请求和返回响应之外,没有视图应该遵守的特定协议,也没有关于它们允许或不允许做什么的规则。

这本书的大部分内容被分成了相当独立的章节,但这本书涵盖了两个看似不相关的概念,因为它们彼此非常依赖。URL 是站点的主要入口点,而视图是响应输入事件的代码。视图中发生的事情是非常开放的。除了接受请求和返回响应之外,没有视图应该遵守的特定协议,也没有关于它们允许或不允许做什么的规则。

视图的可能性太大了,以至于无法详细描述,而且也没有专门为视图在执行时使用而设计的实用程序。相反,可以挂钩到 Django 用来将 Web 地址映射到它们应该执行的视图的过程。这使得 URL 和视图之间的链接变得极其重要,对它的透彻理解可以实现更高级的技术。

此外,就 Django 如何管理传入请求而言,URL 配置的存在只是为了将请求分派给能够处理它的视图。独立于视图讨论 URL 和 URL 配置没有什么价值。

资源定位符

由于对 Web 服务器的所有传入请求都源于 Web 浏览器对 URL 的访问,因此讨论 URL 是一个重要的起点。浏览器将 URL 转换成发送到 Web 服务器的消息的过程超出了本章的范围,但是第七章提供了更多信息。

一个常见的混淆点是网址应该被称为统一资源标识符(URI)还是统一资源定位器(URL)。许多人交替使用这两个术语,不管他们是否知道区别。简而言之,URI 是一个完整的寻址机制,包括两条信息。

  • 用于连接到资源的方案或协议的名称。这后面总是跟着一个冒号。
  • 可以找到资源的路径。对于不同的方案,此路径的确切格式可能不同,因此并非所有 URI 路径看起来都一样。

另一方面,URL 是一小组连接方案中的地址,它们的路径部分都符合一种格式。该协议集中包括 HTTP、HTTPS 和 FTP 等常见协议,本质上是当今网络上常见的协议。这些协议共享的路径格式如下。

  • 用于访问资源的协议,例如标准 HTTP 的http://。这是对 URI 的 scheme 部分的一点扩展,因为它假设所有的 URL 协议都会在冒号后包含两个正斜杠。
  • 资源所在的主机域,如 prodjango.comwww.prodjango.com
  • 或者,服务器响应的端口号。每个协议都有一个默认端口,如果没有提供,将会使用该端口。对于标准 HTTP,这是80,而对于使用安全套接字层(SSL)的加密 HTTP,这将是443
  • 资源在服务器上的路径,如/ 第四章 /

因此,虽然所有的网址肯定是 URIs,但不是所有的 URIs 网址。当在网上工作时,这种微妙的区别可能会令人困惑,因为这两个词都可以用来描述随处可见的地址。由于 Django 是为网络而构建的——也就是 URL 方案下的地址——本书的其余部分将把这些地址称为 URL,因为 URIs 的全部范围可能不适合 Django 的调度机制。

DESIGNING CLEAN URLS

在理想情况下,你第一次建立网站时选择的 URL 永远不会改变, 1 保持不变,直到文档或者整个服务器不再可维护。仅仅因为网站的重新设计或重组而改变 URL 通常是不好的形式,应该避免。

让 URL 长期可维护并让用户更容易跟踪它们的关键是首先要把它们设计好。Django 让这变得很容易,允许你以任何你喜欢的层次结构设计你的 URL,在 URL 中分配变量,并把 URL 结构分成可管理的块。

最重要的是,URL 是应用用户界面的一部分,因为用户必须看到它们,阅读它们,并且经常手动输入它们。在设计你的 URL 时,请记住这一点。

标准 URL 配置

Django 没有提供任何为任何站点自动发现或生成 URL 结构的特性。相反,每个站点和应用都应该使用 URL 配置明确声明最合适的寻址方案。这不是一个限制——这是一个允许你以自己喜欢的方式定义站点地址的特性。毕竟,网站就像房地产一样;你的网络框架不应该决定你的平面图。

定义一个 URL 配置看起来很简单,但是有一点需要特别注意,特别是因为 Django 自己的工具并不是定义这个配置的唯一方法。这个实现位于django.conf.urls.defaults中,提供了两个协同工作来管理 URL 配置的函数。

patterns()函数

URL 配置由一个模式列表组成,每个模式将特定类型的 URL 映射到一个视图。这些模式每个都有一些组件,但是它们都被一起指定为patterns()函数的参数。

from django.conf.urls.defaults import *

urlpatterns = patterns(''

(r'^$', 'post_list')

(r'^(?P<id>\d+)/$', 'post_detail')

(r'^(?P<id>\d+)/comment/$', 'post_comment', {'template': 'comment_form.html'})

)

该函数的参数可以分为两组:

  • 指定为字符串的任何视图的单个导入路径前缀
  • 任意数量的 URL 模式

从历史上看,所有视图都被指定为字符串,因此前缀是减少从单个应用将 URL 映射到视图所需的重复量的好方法。最近,URL 模式被允许将视图指定为可调用的,在这种情况下,前缀将被忽略。使用前缀将视图指定为字符串仍然很有用,因为它不需要视图的一组导入,从而减少了整体代码。

传统上,url 模式是以元组的形式传入的,尽管本章后面的“URL()函数”一节描述了一个更新的内容。该元组的每个部分的细节如下:

  • 用于匹配 URL 的正则表达式
  • 为匹配此模式的请求调用的视图函数
  • 或者,要传递给函数的参数字典

这个元组包含将传入请求映射到视图函数所需的所有信息。将根据正则表达式检查 URL 的路径,如果发现匹配,请求将被传递给指定的视图。正则表达式捕获的任何参数都与额外字典中的显式参数相结合,然后与请求对象一起传递给视图。

Note

像大多数正则表达式一样,URL 模式通常使用原始字符串来描述,由前缀r表示。原始字符串不经过标准转义,这在这里很有用,因为正则表达式提供了自己的转义形式。如果我们不使用原始字符串,我们必须对每个反斜杠进行转义,以便将其传递给正则表达式。这里的例子可以写成没有原始字符串的'^(?P<id>\\d+)/$'

MULTIPLE ARGUMENTS WITH THE SAME NAME

单个 URL 配置可以以两种不同的方式提供值:在 URL 的正则表达式中和在附加到模式的字典中。接受来自两个不同来源的参数使得为同一个键提供两个不同的值成为可能,这需要以某种方式解决。如果你尝试对一个标准函数的关键字参数这样做,Python 会抛出一个TypeError,如第二章所述。

Django 允许指定多个参数而不会引发异常,但是它们不能一起传递给视图。正如本章的第二部分所示,视图的调用就像任何普通的 Python 函数一样,所以这些多个参数会导致与第二章中的相同的TypeError。为了正确地解决这个问题,Django 必须可靠地选择一个而不是另一个。URL 配置中字典提供的任何参数都将优先于 URL 中找到的任何内容。

以这种方式提供同名的多个参数是不好的,因为它严重依赖 Django 对情况的处理才能正常工作。虽然这种行为不太可能因一时兴起而改变,但依赖它可能会在未来引发问题。更重要的是,在多个地方指定相同的参数名称会大大降低 URL 配置的可读性。即使在闭源应用中,在你完成代码后很久,其他人也可能需要阅读你的代码。

url()函数

为了提供更好的长期灵活性,URL 模式元组已经被弃用,取而代之的是url()实用函数。url()采用传入元组的相同参数,但也可以采用额外的关键字参数来指定所描述的 URL 模式的名称。

这样,一个站点可以多次使用同一个视图,但仍然可以通过反向 URL 查找被引用。在这一节的后面可以找到更多的信息。

包含( )函数

与在一个文件中提供所有的 URL 模式不同,include()函数允许它们被分割到多个文件中。它只有一个参数:一个可以找到另一个 URL 配置模块的导入路径。这不仅允许将 URL 配置拆分到多个文件中,还允许将正则表达式用作包含的 URL 模式的前缀。

使用include()时要记住的一件重要事情是不要在正则表达式中指定字符串的结尾。表达式不应以美元符号($)结尾。美元符号($)使得表达式只匹配完整的 URL。这不会留下任何额外的 URL 片段传递给包含的配置。这意味着额外的 URL 模式只有在专门检查空字符串时才会匹配。

将 URL 解析到视图

视图很少被您自己的代码直接调用,而是被 Django 的 URL 调度机制调用。这允许视图从触发它们的特定 URL 中分离出来,并且对于大多数项目来说,这两个方面如何联系的细节可以安全地忽略。但是因为视图并不总是简单的函数,所以了解 Django 如何从 URL 到视图是很重要的,这样才能确定视图真正能够做什么。

将 URL 映射到视图是一个简单的、有良好文档记录的过程,但是值得在这里介绍一些基础知识以供参考。典型的 URL 模式由几个不同的项目组成:

  • 与请求的传入 URL 匹配的正则表达式
  • 对要调用的视图的引用
  • 每次访问视图时传递的参数字典
  • 反向查找期间用于引用视图的名称

由于 URL 模式是用正则表达式表示的,正则表达式可以捕获字符串的某些部分以备后用,Django 将此作为从 URL 中提取参数的自然方式,以便将它们传递给视图。有两种方法可以指定这些组,这决定了它们的捕获值如何传递到视图中。

如果指定的组没有名称,它们将被放入一个元组中,作为多余的位置参数传递。这种方法使正则表达式变得更小,但是它有一些缺点。这不仅降低了正则表达式的可读性,还意味着视图中参数的顺序必须始终与 URL 中组的顺序相匹配,因为 Django 将它们作为位置参数发送。这比通常更好地将 URL 耦合到视图;在某些情况下,例如本章后面描述的基于对象的视图,它仍然非常有用。

如果组被给定了名称,Django 将创建一个字典,将这些名称映射到从 URL 中提取的值。这种替代方法通过将捕获的值作为关键字参数传递给视图,有助于鼓励 URL 和视图之间的松散耦合。注意,Django 不允许在同一模式中同时使用命名组和未命名组。

将视图解析为 URL

正如上一节提到的,Django 提供了另一个 URL 解析过程,如果应用得当,这个过程会更有用。应用通常需要提供链接或重定向到应用的其他部分或网站上的其他地方,但直接硬编码这些链接通常不是一个好主意。毕竟,即使是专有应用也可以改变它们的 URL 结构,而分布式应用可能根本不知道 URL 结构是什么样子。

在这些情况下,将 URL 放在代码之外很重要。Django 提供了三种不同的方法来指定一个位置,而不需要事先知道它的 URL。从本质上来说,它们都以相同的方式工作,因为它们都使用相同的内部机制,但是每个接口都适合特定的用途。

代码引用 URL 最明显的地方之一是在大多数模型的get_absolute_url()方法中。提供此方法是一种常见的约定,因此模板可以轻松地提供到对象详细信息页面的直接链接,而不必知道或关心使用什么 URL 或视图来显示该页面。它不带任何参数,返回一个包含要使用的 URL 的字符串。

为了适应这种情况,Django 提供了一个装饰器,位于django.db.models.permalink,它允许函数返回一组描述要调用的视图的值,将它转换成调用视图的 URL。这些值作为函数(如get_absolute_url()方法)的返回值提供,并遵循特定的结构——一个最多包含三个值的元组。

  • 第一个值是要调用的视图的名称。如果视图已命名,则此处应该使用该名称。否则,应该使用视图的导入路径。这是必须的。
  • 第二个值是应该应用于视图的一组位置参数。如果没有任何参数要应用于视图,则不需要提供该值,但是如果需要关键字,这应该是一个空元组。
  • 这个元组中的第三个值是一个将关键字参数映射到它们的值的字典,所有这些值都将被传递给指定的视图。如果不需要关键字参数,这个值可以不包含在元组中。

给定以下 URL 配置:

from django.conf.urls.defaults import *

from django.views.generic.detail import DetailView

from library.import models

class LibraryDetail(DetailView):

queryset = models.Article.objects.all()

urlpatterns = patterns('django.views.generic'

url(r'^articles/(?P<object_id>\d+)/$', LibraryDetail.as_view()

name='library_article_detail')

)

相应的模型(位于library应用中)可能如下所示:

from django.db import models

class Article(models.Model):

title = models.CharField(max_length=255)

slug = models.SlugField()

pub_date = models.DateTimeField()

def get_absolute_url(self):

return ('library_article_detail'

(), {'object_id': self.id})

get_absolute_url = models.permalink(get_absolute_url)

url 模板标记

另一个常见的需求是让模板提供到不是基于模型的视图的链接,但是不应该有硬编码的 URL。例如,一个联系表单的链接不一定与数据库或任何模型有任何联系,但是仍然需要以一种能够适应未来变化或分布的方式进行链接。

这个模板的语法看起来非常类似于permalink decorator,因为它将值传递给同一个实用函数。有一些细微的区别,因为作为一个模板标签,它不使用真正的 Python 代码。

{% url library_article_detail object_id=article.id %}

reverse()实用函数

Django 还提供了一个 Python 函数,该函数提供了从视图描述及其参数到触发指定视图的 URL 的转换。生活在django.core.urlresolversreverse()函数正是这样做的。它采用前面两种技术描述的所有相同的参数,但也有一个参数,允许它指定应该使用哪个 URL 配置模块来解析 URL。这个函数由permalink装饰器和url模板标签在内部使用。reverse()函数最多接受四个参数。

  • viewname—要调用的视图的名称或导入路径(如果未指定名称)。这是必须的。
  • urlconf—用于查找的 URL 配置模块的导入路径。这是可选的,如果它不存在或None,该值取自ROOT_URLCONF设置。
  • args—将传递给视图的任何位置参数的元组。
  • kwargs—将传递给视图的任何关键字参数的字典。

使用与上一节相同的例子,下面是如何使用reverse()来获取特定对象的 URL。

>>> from django.core.urlresolvers import reverse

>>> reverse('library_article_detail', kwargs={'object_id': 1})

'/articles/1/'

请记住,argskwargs是分开的、不同的参数。reverse()效用函数不使用第二章中描述的任何形式的参数展开。

POSITIONAL VS. KEYWORD ARGUMENTS

为了说明最佳实践,本节中的示例都在 URL 的正则表达式中使用命名组,这允许——实际上要求——使用关键字指定参数的反向解析。这极大地提高了代码的可读性和可维护性,这是编写 Python 的主要目标。不过,也可以指定 URL 而不命名捕获组,这需要反向解析来仅使用位置参数。

例如,如果 URL 模式被定义为r'^articles/(d+)/$',为了正常工作,前面的例子必须这样编写:

  • permalink装饰者— return ('library_article_detail', (self.id,), {})
  • url模板标签— {%url library_article_detail article.id %}
  • reverse()功能— reverse('library_article_detail', args=(1,))

因为 URL 配置只允许位置参数或关键字参数,而不是两者都允许,所以没有必要在同一个反向解析调用中同时指定这两种类型。

基于功能的视图

来自其他环境的程序员感到困惑的一点是,Django 使用的术语“视图”与其他人有点不同。传统上,模型-视图-控制器(MVC)体系结构中的视图指的是向用户显示信息——本质上是用户界面的输出部分。

网络不是这样的。查看数据通常是用户操作的直接结果,对该视图的更新只是对后续操作的响应。这意味着输出过程不可避免地与用户输入过程联系在一起,这可能会导致一些困惑,甚至是传统的 MVC 模式应该如何定义视图。

因此,对于 Django 的观点与其他环境相比如何的问题,没有简单的答案,因为没有任何可靠的东西可以比较。不同背景的人可能对视图有不同的期望。坏消息是 Django 可能和他们中的任何一个都不一致。好消息是,一旦您开始使用 Django,视图的概念就有了明确的定义,所以在与其他 Django 开发人员交流时不会有什么困惑。

模板稍微打破了它

Django 的视图执行输出接口的基本功能,因为它们负责发送给浏览器的响应。从严格意义上来说,这个响应就是整个输出,它包含了用户将会看到的所有信息。在保持可读性的同时,这在 Python 中要做的工作通常太多了,所以大多数视图依赖模板来生成大部分内容。

最常见的做法是让每个视图调用一个单独的模板,这可以利用许多工具来最小化为特定视图使用而必须编写的模板代码的数量。第六章包括了关于模板语言和可用工具的更多细节,但是对于这一节来说,重要的是要知道模板是一种从整体上简化编码过程的好方法。它们有助于减少必须编写的代码量,同时使代码在将来更具可读性和可维护性。

虽然第一章将模板列为一个单独的层,但是请记住,它们实际上只是 Django 提供给应用其他部分的一个工具,包括视图。最终,无论是否使用模板来生成内容,视图单独负责生成最终的响应。Django 的模板系统没有请求或响应的概念;它只是生成文本。剩下的就由视图来处理了。

视图的剖析

视图是一个接受 HTTP 请求并返回 HTTP 响应的函数。考虑到视图的潜在力量,这有点过于简单了,但这确实就是全部了。一个视图总是接收 Django 创建的HttpRequest作为它的第一个参数,并且它应该总是返回一个HttpResponse,除非出错。关于这些物体的全部细节,它们的用途和属性在第七章中有所介绍。

该定义的第一个方面是视图必须是标准函数的概念。这个定义有点灵活,因为在现实中,任何 Python 可调用对象都可以用作视图;只是基本功能很容易使用,并且提供了大多数情况下所需的一切。方法——包括类和实例上的方法——和可调用对象,使用第二章中描述的协议,都完全可以用作视图。这打开了各种其他的可能性,其中一些将在本章后面描述。

下一点是在视图方面不可改变的。每当调用一个视图时,不管传递什么其他参数,第一个参数总是一个HttpRequest对象。这也意味着所有视图必须至少接受这一个对象,甚至那些没有任何显式参数的视图。一些简单的视图,比如显示服务器当前时间的视图,甚至可能不使用请求对象,但是无论如何都必须接受它,以满足视图的基本协议。

关于参数,另一点是视图必须能够接受传递给它的任何参数,包括从 URL 捕获的参数和传递到站点的 URL 配置中的参数。这似乎是显而易见的,但是一个常见的混淆点是假设 Django 使用某种魔法来允许 URL 配置指定应该使用哪个模板,而不需要视图中的任何支持代码。

Django 的通用视图都允许您指定模板名称,许多用户认为 Django 会以某种方式直接传递给模板系统,以覆盖视图默认使用的名称。事实是,通用视图对这个参数有特殊的处理,视图本身负责告诉模板系统使用哪个模板。Django 依赖于标准 Python,所以在幕后没有试图解释你的参数应该是什么意思的魔法。如果您计划为函数提供一个参数,请确保视图知道如何处理它。

最初的视图描述中的最后一个概念是视图必须返回一个HttpResponse对象,即使这样也不完全准确。返回响应无疑是所有视图的主要目标,但是在某些情况下,引发一个异常更合适,这将通过其他方式来处理。

请求和响应之间发生的事情在很大程度上是不受限制的,并且视图可以用于需要满足的许多目的。可以构建视图来服务于特定的目的,或者可以使它们足够通用以用于分布式应用。

将视图编写为通用视图

Django 开发中的一个常见主题是使代码尽可能地可重用和可配置,以便应用和代码片段在多种情况下都有用,而不必为每一种需求重写代码。这就是 DRY 的全部意义:不要重复自己。

视图对于 DRY 来说是一个挑战,因为它们只被传入的请求调用。看起来似乎不可能编写一个视图,除了它最初打算用于的请求之外,它还可以被调用。然而,Django 本身有很多通用视图的例子,这些视图可以用于各种各样的应用和情况,每次新的使用只需要少量的配置。

有一些准则可以极大地帮助视图的重用,使它们足够通用,可以在各种应用中使用。视图甚至可以如此通用,以至于可以分发给其他人,并包含在原作者没有概念的项目中。

使用大量的论据

通常,一个视图可以执行很多不同的任务,所有的任务组合起来解决一个特定的问题。这些任务中的每一个通常都必须对它应该如何工作做出假设,但是这些假设通常可以使用参数提取到一个可配置的选项中。考虑下面的视图,该视图旨在检索一篇博客文章并将其传递给模板。

from django.shortcuts import render_to_response

from django.template import RequestContext

from blog.models import Post

def show_post(request, id):

post = Post.objects.get(id=id)

context = RequestContext(request, {'post': post})

return render_to_response('blog/detail.html', context)

这个视图将非常适合它的预期目的,但是它与一个特定的博客应用紧密相连。它仍然是松散耦合的,因为它不需要处理如何检索博客文章或呈现模板的细节,但仍然依赖于特定于博客应用的细节,如模型和模板。

相反,有可能将这些假设转移到可以在其他情况下替换的论点中。虽然最初这将涉及到一些额外的工作,但是如果这种视图在很多情况下被使用,它可以节省很多时间。更重要的是,视图越复杂,使用这种技术可以重用的代码就越多。一旦这些选项被移到参数中,特定的值就可以通过 URL 配置传入,所以不必为每个目的编写视图。

对于这个特殊的视图,一些事情可以这样分解。不需要预先知道模型,视图也应该能够使用 QuerySet,以便特定的 URL 可以对有限的数据集进行操作。此外,字段名称不应该是硬编码的,模板名称应该在视图之外提供。

from django.shortcuts import render_to_response

from django.template import RequestContext

def show_object(request, id, model, template_name):

object = model._default_manager.get(pk=id)

context = RequestContext(request, {'object': object)})

return render_to_response(template_name, context)

然后,当需要使用这个视图时,通过使用 URL 配置提供这些细节,就可以很容易地进行定制。只需在 URL 配置中提供参数值作为额外的字典,每次从该 URL 模式调用视图时都会传递这些参数值。

from django.conf.urls.defaults import *

from blog.models import Post

urlpatterns = patterns(''

(r'^post/(?P<id>\d+)/$', 'blog.views.show_object', {

'model': Post

'template_name': 'blog/detail.html'

})

)

这种方法甚至可以用于使用其他类型 id 的模型,例如使用 DJNG-001 格式的目录号的音乐数据库;任何可以保证在所有对象中唯一的东西都可以用作对象的主键。因为我们新的通用视图只是将 ID 直接传递给数据库 API,所以只需适当地调整 URL 模式,就可以很容易地支持其他类型的 ID。

r'^album/(?P<id>[a-z]+-[0-9])/$'

这个特殊的视图不应该写在第一位,因为 Django 为此提供了一个现成的视图DetailView,它甚至比这里显示的例子更加通用。它使用了十几个不同的参数,所有这些参数都应该在 URL 配置中进行定制。

一旦您有了一个接受大量参数进行定制的视图,就很容易要求在每个 URL 配置中指定太多的参数。如果每次使用视图都需要指定所有的配置选项,那么使用通用视图的工作量很快就会变得和每次从头开始编写视图一样多。显然,需要有一种更好的方式来处理所有这些争论。

提供合理的默认值

因为函数可以为任何使用它们的参数定义默认值,所以管理这种复杂性的最合理的方法是尽可能提供合适的默认值。确切地说,每个视图可以提供什么样的缺省值以及它们看起来是什么样的会有所不同,但是通常有可能为它们提供一些合理的值。

有时您有许多视图,每个视图服务于不同的目的,但是可能有一些共同的代码。这通常是样板文件,每个视图都需要使用,但并不适合任何单个视图的真正功能。

例如,个人页面的视图必须始终验证用户是否登录以及他们是否拥有适当的权限。一个应用可能有十几种不同类型的视图,但是如果它们都是私有的,那么它们每次都必须使用相同的代码。幸运的是,我们正在使用 Python,这提供了一个有用的选择。

视图装饰者

视图中的大多数样板文件要么在最开始,要么在最末尾。通常,它处理诸如初始化各种对象、测试标准先决条件、优雅地处理错误或在响应到达浏览器之前定制响应之类的任务。视图的真正核心是位于中间的部分,这是写起来有趣的部分。在第二章的中描述,装饰器是一种很好的方式,可以将几个函数封装在一些只需编写一次就可以轻松测试的公共代码中,这样可以减少 bug 和程序员的疲劳。因为视图通常只是标准的 Python 函数,所以这里也可以使用 decorators。

第二章展示了如何使用 decorators 编写原始函数的包装器,该包装器可以访问该函数的所有参数,以及函数本身的返回值。就视图而言,这意味着 decorators 总是可以访问传入的请求对象和传出的响应对象。在某些情况下,装饰器可以是特定应用的特例,这将允许它预期特定于该应用的大量参数。

decorators 可以提供很多视图,其中一些很常见,足以保证包含在 Django 本身中。位于django.views.decorators的是一些包含装饰器的包,你可以在任何应用的任何视图上使用它们。下面列出的包只提供了完整导入路径的尾部,因为它们都位于同一位置。

  • cache.cache_page—将视图的输出存储到服务器的缓存中,以便以后有类似的请求时,不必每次都重新创建页面。
  • cache.never_cache—防止缓存特定视图。如果您设置了站点范围的缓存,但某些视图不能过时,这是很有用的。
  • gzip.gzip_page—压缩视图的输出并添加适当的 HTTP 头,以便 Web 浏览器知道如何处理它。
  • http.conditional_page—仅当自浏览器上次获取副本以来页面发生了更改时,才将整个页面发送到浏览器。
  • http.require_http_methods—接受一个 HTTP 方法列表(在第七章的中有详细描述),该视图仅限于这些方法。如果用任何其他方法调用视图,它会发送一个响应告诉浏览器这是不允许的,甚至不调用视图。包含的两个快捷方式变体是http.require_GEThttp.require_POST,它们不带任何参数,分别针对 GET 和 POST 请求进行硬编码。
  • vary.vary_on_header—根据传递给装饰器的头的值,通过指示页面内容的变化,帮助控制基于浏览器的页面缓存。特定于Cookie割台的简单变体可在vary.vary_on_cookie获得。

位于django.contrib的捆绑应用提供了额外的装饰器。这些装饰器都位于该路径之下,所以和前面的列表一样,只提供了相关的路径:

  • admin.views.decorators.staff_member_required—一个简单的装饰器,检查当前用户是否有人员访问权。这将自动用于 Django 内置管理中的所有视图,但也可以用于您站点上任何其他员工专用的视图。如果用户没有 staff 权限,装饰者会将浏览器重定向到管理员的登录页面。
  • auth.decorators.user_passes_test—接受单个参数,这是一个针对某些任意条件测试当前用户的函数。所提供的函数应该只接受User对象,如果测试通过,则返回True,如果测试失败,则返回False。如果测试通过,用户将被授予访问页面的权限,但如果测试失败,浏览器将重定向到网站的登录页面,这由LOGIN_URL设置决定。
  • auth.decorators.login_requireduser_passes_test的特殊版本,这个装饰器在允许访问视图之前简单地检查用户是否登录。
  • auth.decorators.permission_requireduser_passes_test的另一个专门化,它在视图加载之前检查用户是否有给定的权限。装饰器只接受一个参数:要检查的权限。

这些只是 Django 本身捆绑的装饰器。decorators 还有许多其他用途,第三方应用也可以提供它们自己的用途。然而,为了让这些装饰器有用,它们必须应用于视图。

应用视图装饰器

第二章描述了装饰器如何应用于标准 Python 函数。将 decorators 应用于视图的工作方式是相同的,但是有一个显著的区别:视图并不总是在您的控制之下。

第二章描述的技术假设你修饰的功能是你自己的。虽然这是经常发生的情况,但分布式应用的数量意味着许多 Django 支持的网站将使用其他来源的代码,并有自己的视图。如前所述应用 decorators 需要修改第三方代码。

目标是将 decorators 应用于第三方视图,而不实际修改第三方代码。做到这一点的关键在于 Python 2.3 和更早版本的旧式装饰语法。请记住,新语法允许在函数定义之上应用装饰器,但是旧语法依赖于将函数直接传递给装饰器。因为 Python 函数可以从任何地方导入,并且可以随时作为参数传入,所以这是从第三方代码创建修饰视图的一种极好的方式。

还要记住,URL 配置是在 Python 模块中定义的,当它被读取时就会被执行。这使得这种配置可以使用大量的 Python,包括将函数传递给 decorators 来创建新函数的能力。

from django.conf.urls.defaults import *

from django.contrib.auth.decorators import login_required

from thirdpartyapp.views import special_view

urlpatterns = patterns(''

(r'^private/special/$', login_required(special_view))

)

编写视图装饰器

第二章讲述了装饰者本身是如何工作的,以及如何在各种情况下编写它们,尽管视图的装饰者有一些具体的细节需要注意。这些与编写 decorators 的技术方面关系不大,更多的是在具体使用视图时如何实现某些有用效果的细微差别。

decorators 用于视图的最常见的任务是在原始视图周围创建一个包装函数。这允许装饰者执行视图本身通常会做的额外工作,包括

  • 基于传入的请求执行额外的工作或改变其属性
  • 改变传递给视图的参数
  • 修改或替换传出响应
  • 处理视图内部发生的错误
  • 分支到其他代码,甚至不执行视图

编写装饰器时要考虑的第一件事是,它接收所有针对视图本身的参数。前几节讨论了这一点,但只是在使用*args**kwargs接收参数并将它们直接传递给包装函数的常见上下文中。对于视图,您预先知道第一个参数将始终是传入的请求对象,因此包装器函数可以预见到这一点,并与其他参数分开接收请求。

通过在执行视图之前与请求对象进行交互,decorators 可以做两件重要的事情:根据传入的请求做出决策,并对请求进行更改以改变视图的操作方式。这些任务并不互相排斥,许多装饰者两者都做,比如下面来自 Django 的例子。

from django.utils.functional import wraps

def set_test_cookie(view):

"""

Automatically sets the test cookie on all anonymous users

so that they can be logged in more easily, without having

to hit a separate login page.

"""

def wrapper(request, *args, **kwargs):

if request.user.is_anonymous():

request.session.set_test_cookie()

return view(request, *args, **kwargs)

return wraps(view)(wrapper)

PRESERVING A VIEW’S NAME AND DOCUMENTATION

内置的管理接口使用视图函数本身的名称和 docstring 为应用的视图生成文档。通过使用 decorators 包装函数,我们实际上是用包装器替换了原始的视图函数。这导致管理界面看到包装器,而不是视图。

通常,这将导致视图的名称和文档字符串在混乱中丢失,因此管理员的文档特性不能正确地处理这些视图。为了获得正确的文档,函数的这些属性必须在整个包装过程中保持不变。

Django 提供了一个额外的装饰器,位于django.utils.functional.wraps,它被设计成将这些属性复制到包装的函数上,这样看起来更像原始视图。这个过程在第九章中有更详细的描述,但是本节中的所有例子都用它来说明修饰视图的最佳实践。

装饰器的另一个常见用途是从一组视图的开头或结尾提取一些公共代码。这在查看传入参数时特别有用,因为 decorators 可以在调用视图之前执行任何查找和初始化。然后,装饰者可以简单地将完全准备好的对象传递给视图,而不是从 URL 获取原始字符串。

from django.utils.functional import wraps

from django.shortcuts import get_object_or_404

from news.models import Article

def get_article_from_id(view):

"""

Retrieves a specific article, passing it to the view directly

"""

def wrapper(request, id, *args, **kwargs):

article = get_object_or_404(Article, id=int(id))

return view(request, article=article, *args, **kwargs)

return wraps(view)(wrapper)

像这样的装饰器的优点在于,尽管它包含的逻辑相当少,但它确实减少了为视图复制的代码量,这些视图都根据 URL 中提供的 ID 获得一个Article对象。这不仅使视图本身更具可读性,而且任何时候您都可以减少必须编写的代码,这有助于减少 bug。

此外,通过访问响应,装饰者可以对响应应该如何表现做出一些有趣的决定。在第七章中描述的中间件类,在访问响应方面有更多的用途,但是装饰者仍然可以做一些有用的事情。

值得注意的是能够设置响应的内容类型,这可以控制浏览器在收到内容后如何处理它。第七章更详细地描述了这一点,以及在创建响应时如何设置它。然而,也可以在响应已经被创建并从视图返回之后设置它。

这种技术是为特定类型的视图覆盖内容类型的好方法。毕竟,如果没有指定内容类型,Django 会从DEFAULT_CONTENT_TYPE设置中提取一个值,默认为'text/html'。对于某些类型的视图,尤其是那些面向 Web 服务的视图,最好使用另一种内容类型,比如'application/xml',同时仍然能够使用通用视图。

from django.utils.functional import wraps

def content_type(c_type):

"""

Overrides the Content-Type provided by the view.

Accepts a single argument, the new Content-Type

value to be written to the outgoing response.

"""

def decorator(view):

def wrapper(request, *args, **kwargs):

response = view(request, *args, **kwargs)

response['Content-Type'] = c_type

return response

return wraps(view)(wrapper)

return decorator

然后,这个装饰器可以在将内容类型应用到视图时接受它。

@content_type('application/json')

def view(request):

...

视图装饰器的一个很少使用的特性是捕捉由视图或它执行的任何代码引发的任何异常的能力。视图通常只是直接返回一个响应,但是在很多情况下视图可能会选择引发一个异常。Django 自己的通用视图中常见的一个例子是抛出Http404异常来表示找不到某个对象。

第九章涵盖了 Django 在其标准发行版中提供的例外情况,其中许多可以由视图出于这样或那样的原因提出。此外,许多标准 Python 异常可能会在各种情况下出现,捕捉这些异常可能会很有用。当出现异常时,装饰器可以执行各种额外的任务,从简单地将异常记录到数据库,到在某些异常的情况下返回不同类型的响应。

考虑一个具有如下日志条目模型的自定义日志记录应用:

from datetime import datetime

from django.db import models

class Entry(models.Model):

path = models.CharField(max_length=255)

type = models.CharField(max_length=255, db_index=True)

date = models.DateTimeField(default=datetime.utcnow, db_index=True)

description = models.TextField()

提供这个模型的应用也可以为项目提供一个装饰器,应用到它们自己的视图中,自动记录这个模型的异常。

from django.utils.functional import wraps

from mylogapp.models import Entry

def logged(view):

"""

Logs any errors that occurred during the view

in a special model design for app-specific errors

"""

def wrapper(request, *args, **kwargs):

try:

return view(request, *args, **kwargs)

except Exception as e:

# Log the entry using the application’s Entry model             Entry.objects.create(path=request.path

type='View exception'

description=str(e))

# Re-raise it so standard error handling still applies

raise

return wraps(view)(wrapper)

所有这些例子反复出现的主题是,视图装饰者可以封装一些公共代码,否则这些代码必须在视图的每个实例中重复。本质上,视图装饰器是在原始代码之前或之后扩展视图代码的一种方式。为了认识到视图装饰器有多大的潜力,概括这些例子是很重要的。您发现自己在视图的开头或结尾复制的任何样板文件都可以放在装饰器中,以节省时间、精力和麻烦。

基于类的视图

视图不必局限于函数。最后,对 Django 来说,重要的是它得到了一个可调用函数;如何创建这个可调用函数仍然取决于您。您还可以将视图定义为类,这提供了一些优于传统函数的关键优势。

  • 更高的可配置性
  • 更轻松地定制专业应用
  • 重复使用可能用于其他目的的对象

尽管最终结果必须是可调用的,但创建类的方法比创建函数的方法还要多。Django 自己的通用视图遵循特定的结构,本着尽可能保持相似性的精神,尝试与之匹配是个好主意。请记住,如果这种格式不像其他格式那样容易满足您的需求,您可以编写不同的类。

django.views.generic.base.View

将基础知识加入到您的类中的最简单的方法是子类化 Django 自己的View类。当然,它不能满足您开箱后的所有需求,但它提供了基本功能:

  • 验证传入视图配置的参数
  • 防止使用以 HTTP 方法命名的参数
  • 收集在 URL 配置中传递的参数
  • 将请求信息保存在方便方法访问的地方
  • 验证视图是否支持请求的 HTTP 方法
  • 自动处理选项请求
  • 根据请求的 HTTP 方法调度视图方法

其中一些功能是特定于它是一个类的,比如使方便的请求信息可以方便地用于各种视图方法。其他的,比如强制执行特定的 HTTP 方法和直接处理选项请求,实际上都是很好的 HTTP 实践,因为类可以提供这样的功能,而您不必记住在自己的代码中遵从它,所以变得更加容易。

类为多个方法提供了交互的机会,所以 Django 使用一些标准方法来处理常见的项目,同时为您提供了一种添加其他方法的方式,现在只需要担心您的应用的细节。而且因为它只是一个类,你也可以添加你认为合适的其他方法。Django 不会对它们做任何事情,所以您必须自己调用它们,但是它确实给了您一个机会,让您比使用原始函数更容易地抽象出公共代码。

Django 的所有通用视图都继承自一个共同的祖先来提供所有这些挂钩,您的视图也很容易做到这一点。让我们看看 Django 在默认通用视图上提供的一些方法。

init(自我,**夸格)

作为一个设计用来创建对象的类,__init__()显然是类的实例开始的地方。它的关键字参数是在 URL 中定义的选项,但是实际上在任何时候都不会直接调用它。相反,您的 URL 配置将使用as_view(),它做一些事情,包括初始化类。

__init__()的默认实现只是将所有提供的关键字参数设置为视图对象上的实例变量。

BE CAREFUL OVERRIDING INIT()

正如您将在下面几节中看到的,直到请求到达并被发送到生成的视图函数,视图类才被实例化。这意味着__init__()会为每个传入的请求触发,而不是在 Django 处理您的 URL 配置时只触发一次。

因此,如果您需要对配置选项执行任何更改,或者以不需要访问来自实际请求的任何信息的任何方式对它们做出反应,您将想要覆盖as_view()并在那里添加您的逻辑。实际上,__init__()甚至看不到请求对象本身;它只接收从 URL 捕获的参数。

因此,虽然__init__()通常是提供额外配置特性的好地方,但在这种情况下,它往往不会工作得很好。如果需要处理配置,最好重写as_view(),如果需要处理与传入请求相关的任何事情,最好重写dispatch()

as_view(cls,**initkwargs)

这个类方法是视图的主要入口点。当您配置一个 URL 来使用这个视图时,您将调用这个方法,它将返回一个供 Django 使用的视图函数。您还将把配置选项传递到方法调用中,而不是把它们放在视图本身旁边的字典中。例如:

from django.views.generic.base import View

urlpatterns = patterns(''

(r'^example/', View.as_view(template_name='example.html'))

)

as_view()被召唤时,负责几件事:

  • 它验证所提供的选项都不匹配 HTTP 方法的名称。如果发现任何错误,它会立即引发一个TypeError,而不是等待请求的到来。
  • 它还验证所有提供的选项是否与类中现有的命名属性相匹配。这实施了一种模式,其中默认选项被建立为类属性,然后根据需要被单独的 URL 配置覆盖。例如,前面的例子会引发一个TypeError,因为template_name没有被命名为内置View类的一个属性。
  • 然后,它创建一个简单的视图函数,该函数将返回到 URL 配置中,供实际请求进入时使用。然后这个视图用来自类和任何应用的装饰器的一些属性更新,使它在以后自省时更有用。
  • 最后,它返回新创建的视图函数,所以当请求开始进来时,Django 就有东西可处理了。

as_view()创建的视图功能甚至更简单。它接受一个request,以及*args**kwargs,因此它可以根据 URL 配置中的正则表达式接收从 URL 捕获的任何内容。这与任何其他视图的工作方式相同;Django 的 URL 调度处理基于类的视图的方式没有什么特别的。

一旦有了这些信息,它只负责一点点记录和调用更有用的东西:

  • 首先,它创建 view 类的一个实例,传递提供给as_view()的配置选项。这就是__init__()最终发挥作用的地方,因为视图的实例只适用于单个请求。每个后续请求将获得视图类的一个新实例。
  • 接下来,它检查视图是否有get()head()方法。如果它有get()而没有head(),它会设置视图,这样 HEAD 请求就会被发送到get()方法。一般来说,HEAD 应该像 GET 一样工作,但是不返回内容,所以这是一个合理的默认行为。
  • 然后,它将请求和 URL 捕获的信息设置到对象上,作为名为requestargskwargs的实例属性。您可能不需要将这些信息作为对象的属性来访问,但是如果您确实需要它们,它们就在那里。
  • 最后,它将执行委托给dispatch()方法,传递请求和所有捕获的参数,就像它们被传递给视图本身一样。

派遣(自身,请求,参数,夸尔格斯)

这是正确处理请求的地方。像任何视图一样,它负责接受请求并返回响应。它的默认实现处理不同 HTTP 方法的一些复杂性,同时允许您在附加的视图方法中编写代码。

  • 它首先检查所请求的 HTTP 方法是否有效,并在类上有一个匹配的视图方法来处理请求。如果没有,它将返回一个状态代码为 405 Method Not Allowed 的响应,而不是尝试以任何额外的能力为其提供服务。
  • 如果类确实有匹配的视图方法,dispatch()只是遵从它,将所有参数传递给它。

HTTP 方法的第一个测试是对照已知方法的列表检查方法字符串的小写副本,该列表存储为名为http_method_names的类属性:

  • get
  • post
  • put
  • delete
  • head
  • options
  • trace

请注意,较新的选项(如修补程序)不在此列表中。如果您确实需要一个不同的方法,并且您所在的环境会将它传递给 Django,那么您可以覆盖这个列表来添加您需要的任何其他方法。如果一个 HTTP 方法不在这个列表中,Django 不会允许,即使你有一个同名的视图方法。Django 在这里的行为通常是首选的,但是如果您愿意,您可以覆盖dispatch()来提供其他功能。例如,如果您有一个可以以各种格式返回数据的 API,那么您可以使用dispatch()方法根据需要格式化输出,让各个方法只检索和返回原始数据。

个人视图方法

dispatch()确定视图可以处理请求后,它将请求发送到几个可能的函数中的一个,根据 HTTP 方法命名。例如,GET 请求将被路由到get(),POST 请求将被路由到post(),等等。这些函数的行为就像一个标准的视图函数,接受一个请求和额外的参数并返回一个响应。

为了演示这在实践中如何帮助您的代码,请考虑以下在传统的基于函数的视图中处理表单的示例:

def view(request, template_name='form.html'):

if request.method == 'POST':

form = ExampleForm(request.POST)

if form.is_valid():

# Process the form here

return redirect('success')

else:

return render(request, template_name, {'form': form})

else:

form = ExampleForm()  # no data gets passed in

return render(request, template_name, {'form': form})

这个视图服务于 GET 和 POST 请求,因此它必须处理形成需要处理的数据的请求,同时还要管理没有任何数据的请求,以便首先显示表单。下面是使用基于类的视图时该视图的样子。

class FormView(View):

template_name = 'form.html'

def get(self, request):

form = ExampleForm()

return render(request, self.template_name, {'form': form})

def post(self, request):

form = ExampleForm(request.POST)

if form.is_valid():

# Process the form here

return redirect('success')

else:

return render(request, self.template_name, {'form': form})

这是一个更加清晰的关注点分离,作为一个额外的好处,基于类的版本将自动正确地处理 HEAD 和 OPTIONS 请求,同时拒绝 PUT、DELETE 和 TRACE 请求。

Django 还提供了一个简单的options()方法,它指明了 URL 可以提供什么特性。默认行为使用可用的视图方法来指示允许哪些 HTTP 方法,并在响应的Allow头中提供这些方法。如果您有更多的特性需要包含在这里,比如跨源资源共享所必需的特性, 2 您可以简单地覆盖options()来提供这些信息。

装饰视图方法

当涉及到装饰者时,这些基于类的视图的结构使它们有些有趣。一方面,它们是类,不能用函数所用的装饰器来装饰。事实上,在 Python 3 之前,类根本不能被修饰。另一方面,as_view()方法返回一个简单的函数,可以像其他函数一样进行修饰。

最简单的解释技巧是修饰as_view()的输出。因为它返回一个函数,所以它可以像任何其他函数一样被修饰。因此,如果您需要要求用户登录,您可以像往常一样简单地使用标准的login_required装饰器。

from django.contrib.auth.decorators import login_required

urlpatterns = patterns(''

(r'^example/'login_required(FormView.as_view(template_name='example.html')))

)

另一方面,如果你知道它们总是需要的东西,你可以直接在你的类中修饰单独的方法。有两件事比典型的函数情况更复杂。首先,这些是实例方法,而不是简单的函数,这意味着它们接受一个self参数,这在传统的基于函数的视图中是没有的。就像装饰者经常遇到的问题一样,解决方案是另一个装饰者,在这种情况下是由 Django 自己提供的。method_decorator可以用来包装一个普通的装饰器,让它忽略self,只处理它期望的参数。

from django.utils.decorators import method_decorator

class FormView(View):

@method_decorator(login_required)

def get(request):

# View code continues here

第二个问题是,现在涉及到了多个函数,而不仅仅是一个可以直接修饰的函数。您可以修饰任何您喜欢的函数,但是基于类的视图的调度过程的一个有趣的事实是,dispatch()是唯一一个与传统函数具有相同目的的方法。所有请求都要经过它,它还可以访问关于类、实例和传入请求的所有可用信息。

因此,这也是应用任何视图装饰器的最佳地方。如果您对dispatch()应用一个装饰器,它将修改每个请求的行为,不管后来使用了什么其他方法。如果有充分的理由,您可以修饰单个方法,但最有用的是使用dispatch()并让它像在传统的基于函数的视图中一样工作。

将对象用作视图

正如在第二章中所描述的,Python 提供了一种定义类的方法,它的实例可以像函数一样被调用。如果定义在一个类上,那么当对象被传入一个期望函数的地方时,__call__()方法将被调用。与任何其他可调用对象一样,这些对象也可以用作 Django 视图。

有多少种方法定义对象本身,就有多少种方法使用对象作为视图。除了使用__call__()接收每个传入的请求之外,对象内部发生的事情也是公开的。在典型的情况下,请求将被分派给单独的方法,类似于 Django 自己的基于类的视图,但是您可以做您需要的任何事情。

应用技术

通过允许自定义对象和装饰器用于 URL 模式和视图,几乎任何有效的 Python 代码都可以自定义 URL 如何映射到视图以及视图本身如何执行。以下只是一个尝试的可能性;其余的取决于您的应用的需要。

跨产地资源共享(CORS)

从一个域跨到另一个域的请求存在安全风险,因为它们可能会将敏感数据暴露给不应该访问这些数据的站点。想象一下,如果一个随机的博客可以对你的银行网站进行 AJAX 调用。如果你登录后浏览器没有任何保护措施,这个电话可能会把你的银行账户信息发送到一个你一无所知、只是碰巧访问过的博客上。

幸运的是,现代浏览器确实有针对这类事情的保护措施。默认情况下,在您的浏览器中,从一个站点向另一个站点发出的请求将被禁止。不过,像这样的跨源请求也有合法的用途,比如在可信站点之间,或者在为一般用途提供公共数据文件时。跨源资源共享(CORS)规范允许一个站点指示哪些其他站点可以访问某些资源。

CORS 室内设计师

在传统的基于函数的视图中,这种功能可以作为装饰添加。以下是如何装饰视图,使其从任何请求它的站点公开可用的方法:

@cross_origin(allow_origin=['*'])

def public_data(request):

# Data retrieval goes here

就装饰者而言,实现非常简单:

def cross_origin(allow_credentials=False, allow_headers=None

allow_methods=None, allow_headers=None

allow_origin=None, expose_headers=None, max_age=None):

def decorator(func):

@functools.wraps(func)

def wrapper(request, *args, **kwargs):

headers = {}

if access_control_allow_credentials:

headers['Allow-Credentials'] = allow_credentials

if access_control_allow_headers:

headers['Allow-Headers'] = ', '.join(allow_headers)

if access_control_allow_methods:

headers['Allow-Methods'] = ', '.join(allow_methods)

if access_control_allow_origin:

headers['Allow-Origin'] = ' '.join(allow_origin)

if access_control_expose_headers:

headers['Expose-Headers'] = ', '.join(expose_headers)

if access_control_max_age:

headers['Max-Age'] = self.max_age

response = func(request, *args, **kwargs)

for name, value in headers:

response.headers['Access-Control-%s' % name] = value

return response

return wrapper

return decorator

没有必要支持使用不带参数的装饰器,因为如果你不提供任何参数,它不会做任何事情。所以它只支持参数,当响应从修饰视图返回时,只需添加所有正确的头。如您所见,其中一些接受列表,而另一些只接受单个值。

Mixin 合唱团

这个装饰器可以使用method_decorator直接应用于基于类的视图,但是为了使它更容易配置,我们可以使用 mixin。下面是它在基于类的类似视图中的样子:

class PublicData(View, CrossOrigin):

access_control_allow_origin = ['*']

def get(self, request):

# Data retrieval goes here

实现比简单的装饰器稍微复杂一些,但是仍然非常简单:

class CrossOrigin(object):

"""

A view mixin that provides basic functionality necessary to add the necessary

headers for Cross-Origin Resource Sharing

"""

access_control_allow_credentials = False

access_control_allow_headers = None

access_control_allow_methods = None

access_control_allow_origin = None

access_control_expose_headers = None

access_control_max_age = None

def get_access_control_headers(self, request):

headers = {}

if self.access_control_allow_credentials:

headers['Allow-Credentials'] = self.access_control_allow_credentials

if self.access_control_allow_headers:

headers['Allow-Headers'] = ', '.join(self.access_control_allow_headers)

if self.access_control_allow_methods:

headers['Allow-Methods'] = ', '.join(self.access_control_allow_methods)

if self.access_control_allow_origin:

headers['Allow-Origin'] = ' '.join(self.access_control_allow_origin)

if self.access_control_expose_headers:

headers['Expose-Headers'] = ', '.join(self.access_control_expose_headers)

if self.access_control_max_age:

headers['Max-Age'] = self.access_control_max_age

return headers

def dispatch(self, request, *args, **kwargs):

response = super(CORSMixin, self).dispatch(request, *args, **kwargs)

for name, value in self.get_access_control_headers(request):

response.headers['Access-Control-%s' % name)] = value

return response

这里值得注意的是,header 功能已经转移到一个单独的方法中,该方法接收请求作为参数。这允许您在子类中覆盖该方法,以防您需要根据传入请求的细节对 CORS 头进行更改。

例如,如果您有许多需要访问资源的不同域,您可以根据这些域检查传入的请求,只将该域添加为允许的源,而不必在每个响应中包含整个列表。这是一个很好的例子,说明了类比装饰器能够更好地定制内部细节,装饰器倾向于以一种您无法修改的方式隐藏那些实现。

提供装饰器和混合器

如果您想将它作为一个可重用的助手来提供,您甚至可以同时提供函数 decorator 和类 mixin。这很容易做到,只需将公共代码提取到一个单独的函数中,可以从每种不同的方法中调用该函数。

def cors_headers(allow_credentials=false, allow_headers=None, allow_methods=None,                 allow_origin=None, expose_headers=None, max_age=None):

headers = {}

if allow_credentials:

headers['Access-Control-Allow-Credentials'] = allow_credentials

if allow_headers:

headers['Access-Control-Allow-Headers'] = ', '.join(allow_headers)

if allow_methods:

headers['Access-Control-Allow-Methods'] = ', '.join(allow_methods)

if allow_origin:

headers['Access-Control-Allow-Origin'] = ' '.join(allow_origin)

if expose_headers:

headers['Access-Control-Expose-Headers'] = ', '.join(expose_headers)

if max_age:

headers['Access-Control-Max-Age'] = self.max_age

return response

def cross_origin(allow_credentials=false, allow_headers=None, allow_methods=None

allow_origin=None, expose_headers=None, max_age=None):

def decorator(func):

@functools.wraps(func)

def wrapper(request, *args, **kwargs):

response = func(request, *args, **kwargs)

headers = cors_headers(response, allow_credentials, allow_headers

allow_methods, allow_origin, expose_headers, max_age)

response.headers.update(headers)

return response

return wrapper

return decorator

class CrossOrigin(object):

"""

A view mixin that provides basic functionality necessary to add the necessary

headers for Cross-Origin Resource Sharing

"""

access_control_allow_credentials = false

access_control_allow_headers = None

access_control_allow_methods = None

access_control_allow_origin = None

access_control_expose_headers = None

access_control_max_age = None

def get_access_control_headers(self, request):

return cors_headers(self.access_control_allow_credentials

self.access_control_allow_headers

self.access_control_allow_methods

self.access_control_allow_origin

self.access_control_expose_headers

self.access_control_max_age):

def dispatch(self, request, *args, **kwargs):

response = super(CORSMixin, self).dispatch(request, *args, **kwargs)

headers = self.get_access_control_headers(request)

response.headers.update(headers)

return response

现在,decorator 和 mixin 唯一要做的事情就是为每种技术适当地收集参数,把实际的头应用到一个公共函数的细节留下来。这并不是一个突破性的技术,但它有助于了解装饰者和混合者到底有什么不同。它们的配置稍有不同,但最终还是要接受请求并返回响应。

现在怎么办?

URL 构成了站点架构的基础,定义了用户如何访问您提供的内容和服务。Django 不参与 URL 方案的设计,所以你可以随心所欲地构建它。一定要花适当的时间,记住 URL 配置仍然是网站设计的一种形式。

视图是任何应用的真正主力,接收用户输入并将其转化为有用的输出。虽然视图可以使用整个 Python,但是 Django 确实提供了一个非常重要的工具来处理 Web 上最常见的用户输入任务之一:表单。

Footnotes 1

http://prodjango.com/cool-uris-dont-change/

2

http://prodjango.com/cors/

五、表单

Abstract

现代 Web 应用的关键要素之一是交互性——接受用户输入的能力,这有助于塑造他们的体验。输入可以是任何内容,从简单的搜索词到用户提交的整部小说。关键是能够处理这些输入,并将其转化为有意义的功能,丰富网站所有用户的体验。

现代 Web 应用的关键要素之一是交互性——接受用户输入的能力,这有助于塑造他们的体验。输入可以是任何内容,从简单的搜索词到用户提交的整部小说。关键是能够处理这些输入,并将其转化为有意义的功能,丰富网站所有用户的体验。

该过程首先向 Web 浏览器发送一个 HTML 表单,用户可以在其中填写表单并将其提交回服务器。当数据到达时,必须对其进行验证,以确保用户没有忘记任何字段或输入任何不适当的内容。如果提交的数据有任何问题,必须将其发送回用户进行更正。一旦知道所有的数据都是有效的,应用最终就可以使用这些数据执行有意义的任务。

没有框架也可以做到这一切,但是如果涉及到多个表单,那么这样做将会涉及大量的重复工作。手动管理表单也给程序员带来了走捷径的高风险。表单跳过必要的验证是很常见的,要么是因为缺少时间,要么是觉得没有必要。许多被利用的安全漏洞可以直接归因于这种类型的疏忽。

Django 通过提供一个管理这些细节的框架来解决这个问题。一旦定义了表单,Django 就会处理生成 HTML、接收输入和验证数据的细节。之后,应用可以对收到的数据做任何想做的事情。像 Django 中的其他东西一样,您也可以绕过这种表单处理,在必要时手动处理。

声明和标识字段

Django 的表单和它的模型一样,使用声明性语法,其中字段作为属性分配给表单的类定义。这是 Django 最明显的特征之一,在这里也用得很好。它允许将一个窗体声明为一个简单的类,同时在幕后提供大量的附加功能。

模型和表单的第一个区别是它们识别字段的方式。模型实际上根本不识别字段;它们只是检查属性是否有一个contribute_to_class()方法并调用它,而不管它附加到什么类型的对象。表单实际上会检查类中每个属性的类型,以确定它是否是一个字段,特别是寻找django.forms.fields.Field的实例。

与模型一样,表单保留了对所有已声明字段的引用,尽管表单的做法略有不同。根据表单所处的阶段,表单上可能会有两个单独的字段列表,每个列表都有自己的用途。

第一个是base_fields,是元类执行时找到的所有字段的列表。它们存储在 form 类本身中,并且对所有实例都可用。因此,只有在极端情况下才应该编辑这个列表,因为这样做会影响表单的所有未来实例。当查看表单类本身或识别那些实际上直接在类上声明的字段时,作为参考总是有用的。

所有表单实例都有一个fields属性,其中包含实际用于生成表单 HTML 以及验证用户输入的字段。大多数时候,这个列表与base_fields相同,因为它只是它的一个副本。但是,有时一个表单需要根据一些其他信息来定制它的字段,这样各个实例在不同的情况下会有不同的行为。

例如,联系人表单可以接受一个User对象来确定用户是否登录。如果没有,表单可以添加另一个字段来接受用户名。

from django import forms

class ContactForm(forms.Form):

def __init__(self, user, *args, **kwargs):

super(ContactForm, self).__init__(*args, **kwargs)

if not user.is_authenticated():

# Add a name field since the user doesn't have a name

self.fields['name'] = forms.CharField(label='Full name')

绑定到用户输入

因为表单是专门用来接受用户输入的,所以该活动必须在其他任何活动之前执行。这非常重要,实例化的表单被认为处于两种状态之一:绑定或未绑定。绑定窗体给定用户输入,然后用户可以使用它来做进一步的工作,而非绑定窗体没有与之相关联的数据,通常只用于向用户询问必要的数据。

这两者的区别是在实例化表单时根据是否传入了数据字典来确定的。这个字典将字段名映射到它们的值,如果它被传入,它总是表单的第一个位置参数。即使传递一个空字典也会导致表单被认为是绑定的,尽管它的用处是有限的,因为没有数据,表单就不太可能被验证。一旦一个表单被实例化,通过检查它的布尔is_bound属性,很容易确定它是否被绑定到数据。

>>> from django import forms

>>> class MyForm(forms.Form):

...     title = forms.CharField()

...     age = forms.IntegerField()

...     photo = forms.ImageField()

...

>>> MyForm().is_bound

False

>>> MyForm({'title': u'New Title', 'age': u'25'}).is_bound

True

>>> MyForm({}).is_bound

True

还要注意,所有值都是作为字符串传递的。有些字段可能接受其他类型,如整数,但字符串是标准的,所有字段都知道如何处理它们。这是为了支持实例化表单的最常见方式,使用视图中可用的request.POST字典。

from my_app.forms import MyForm

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST)

else:

form = MyForm()

...

有时,表单也可能接受文件,这与其他类型的输入略有不同。文件可以作为传入请求对象的FILES属性来访问,这通过接受该属性作为第二个位置参数来实现。

from my_app.forms import MyForm

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST, request.FILES)

else:

form = MyForm()

...

不管以何种方式实例化,表单的任何实例都有一个data属性,它包含传递给它的任何数据的字典。对于未绑定的表单,这将是一个空字典。单独使用data是不安全的,因为不能保证用户提交的数据适合表单的需要,事实上它可能会带来安全风险。这些数据在使用前必须经过验证。

验证输入

一旦表单被绑定到一组传入数据,它就可以检查该数据的有效性,并且应该在继续之前一直这样做。这可以防止您的代码对数据质量做出无效的假设,从而防止许多安全问题。

从表面上看,验证用户输入的过程非常简单,只需调用表单的is_valid()方法。这将返回一个 Boolean 值,指示根据表单字段设置的规则,数据是否确实有效。仅这一点就足以确定是继续处理表单还是重新显示表单以供用户更正错误。

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST, request.FILES)

if form.is_valid():

# Do more work here, since the data is known to be good

else:

form = MyForm()

...

NEVER TRUST USER INPUT

在 Web 开发领域有一句古老的谚语,经常被这样表述:“用户输入是邪恶的。”这有点极端,但基本思想是 Web 应用不是在真空中运行,而是暴露在外部世界中,供各种各样的用户交互。这些用户中的大多数都是正直的网络公民,他们只希望按照预期的方式使用网站。然而,其他人最想做的就是让你宝贵的应用屈服。

任何基于用户输入采取行动的应用都有潜在的风险。因为决策是基于用户提供的内容做出的,所以用户对应用的行为有很大的控制权。在某些情况下,用户输入直接传递给数据库或文件系统操作,并假设输入将在某个已知值的既定范围内。

一旦有人心怀恶意,他可以利用这一事实,将其他数据推入应用,希望说服它做一些它不应该做的事情,例如读取用户不应该访问的内容,写入应该只读的区域,关闭应用,这样就没有人可以使用它,或者最糟糕的是,在您不知情的情况下获得对系统的完全访问权。这些类型的攻击通常分为几类,如 SQL 注入、跨站脚本、跨站请求伪造和表单操纵,但有一个主题将它们联系在一起:它们都依赖于应用过于信任传入的数据。

对这类攻击的解决方案是通过仔细验证所有输入的内容,有力地保护您的应用免受恶意输入。Django 的表单有多种方法来控制这种验证,但是is_valid()方法确保它们都运行,这样应用就可以知道是否应该使用输入。永远不要跳过这一步,因为这样做会使您的应用容易受到这些攻击。

同样重要的是要认识到,无论用户的 Web 浏览器内部发生了什么,都必须通过form.is_valid()的方式在服务器上进行验证。在这个 Web 2.0 和富 Web 应用的时代,很多工作都是在浏览器中用 JavaScript 完成的,很容易认为这足以在数据到达服务器之前确保输入数据的质量。

然而,在浏览器和服务器之间可以发生很多事情,有很多免费的工具可以帮助用户在 JavaScript 处理完提交的数据后对其进行操作。HTTP 也是一个容易使用的协议,所以完全绕过浏览器是很容易的。再多的客户端验证都不足以保护应用免受攻击;一切都必须在服务器上检查。

在幕后,is_valid()通过间接调用表单的full_clean()方法做了更多的工作,该方法填充了另外两个属性。第一个是cleaned_data,它是一个类似于前面提到的data属性的字典,除了它的值已经被表单的字段处理并转换成适当的 Python 数据类型。第二个是errors,这是一个字典,包含了输入数据遇到的所有问题的信息。

这两个属性在某种程度上相互关联,因为不应该同时在两个属性中标识任何字段。也就是说,如果一个字段的名称在cleaned_data中,它就不在errors中,反之亦然。因此,在理想情况下,cleaned_data将包含每个字段的数据,而errors将为空。

哪些数据被认为是有效的以及哪些错误将被返回的确切细节通常由每个字段使用其clean()方法来指定。对于大多数表单来说,这已经足够了,但是有些表单可能需要超出单个字段的额外验证。为了支持这一点,Django 提供了一种将附加验证规则注入表单的方法。

可以在表单上定义特殊的方法来帮助这个过程,并根据它们关联的字段来命名。例如,设计用来验证和清理title字段的方法将被称为clean_title()。以这种方式定义的每个方法负责在cleaned_data中查找它的值,根据适合表单的任何规则验证它。如果该值需要额外清理,该方法还必须用适当清理的值替换cleaned_data中的值。

使用基于类的视图

查看到目前为止显示的视图,您会注意到它们倾向于遵循一个共同的模式。事实上,您将遇到的大多数表单处理视图看起来很像这样:

from django.shortcuts import render, redirect

def my_view(request):

if request.method == 'POST':

form = MyForm(request.POST, request.FILES)

if form.is_valid():

form.save()

return redirect('/success/')

return render(request, 'form.html', {'form': form})

else:

form = MyForm()

return render(request, 'form.html', {'form': form})

如第四章中的所示,通过在基于类的视图中分别处理 GET 和 POST 情况,这可以变得更容易管理。

from django.shortcuts import render, redirect

from django.views.generic.base import View

class MyView(View):

def get(self, request):

form = MyForm()

return render(request, 'form.html', {'form': form})

def post(self, request):

form = MyForm(request.POST, request.FILES)

if form.is_valid():

form.save()

return redirect('/success/')

return render(request, 'form.html', {'form': form})

这当然是一个进步,但是这里仍然有很多样板文件。您几乎总是以相同的方式实例化和验证表单,并且在最初显示表单和显示错误时,模板呈现是相同的。归根结底,这个视图唯一真正有趣的部分是你用一个有效的表单做什么。在这种情况下,它只是调用form.save(),但你可以用它来发送电子邮件,传输一些文件,触发支付交易或任何其他事情。

为了避免所有这些重复,Django 提供了另一个基于类的视图,称为FormView。它抽象出了这些共性,所以您只需提供一些基本的细节和一个名为form_valid()的方法,该方法接收一个有效的表单作为唯一的参数。

from django.shortcuts import render, redirect

from django.views.generic.edit import FormView

class MyView(FormView):

form_class = MyForm

template_name = 'form.html'

success_url = '/success/'

def form_valid(self, form):

form.save()

return super(MyView, self).form_valid(form)

这让事情简单多了。事实上,您甚至不需要提供form_valid(),但是考虑到默认情况下它只是重定向到success_url,根本不用对表单做任何事情,您几乎总是希望至少提供这么多。根据需要,您还可以定义许多其他方法来控制其行为的各个方面。

  • get_form_class(self)—返回在整个过程中使用的表单类。默认情况下,它只返回form_class属性的内容,如果不提供属性,则返回None
  • get_initial(self)—返回一个字典,以传递到表单的initial参数中。就其本身而言,这只是返回视图的initial属性的内容,默认情况下这是一个空字典。
  • get_form_kwargs(self)—返回一个字典,在为每个请求实例化表单时用作关键字参数。默认情况下,这包括get_initial()的结果,如果请求是 POST 或 PUT,它还会添加request.POSTrequest.FILES
  • get_form(self, form_class)—通过将从get_form_kwargs()检索到的参数传递给从get_form_class()返回的类,返回一个完全实例化的表单。假设您可以通过get_form_kwargs()控制进入表单实例的所有参数,那么这只对在表单创建之后、有效性测试之前对表单进行修改有意义。
  • form_valid(self, form)—主要工作,当表单被验证时触发,允许您采取适当的行动。默认情况下,这会将用户重定向到get_success_url()方法的结果。
  • form_invalid(self, form)—一个自然对应物form_valid(),当表单被认为无效时被调用,将被赋予无效表单本身。默认情况下,这只是用表单重新呈现模板。
  • get_success_url(self)—这将返回成功验证表单后用户将被发送到的 URL。默认情况下,它返回success_url属性的值。

如您所见,FormView让您有机会定制表单流程的各个方面。不必为表单的每次使用都编写单独的视图,您可以只控制特定于您需要的部分。

如果您需要使用特定模型的表单,有另一个通用视图可以为您做更多的事情。Django 提供了几个其他的类供您使用,而不是重写其中的几个方法来创建一个ModelForm实例。这些都住在django.views.generic.edit里,因为它们允许你编辑数据。

  • CreateView用于帮助创建新对象。
  • UpdateView在编辑现有对象时使用。
  • DeleteView用于删除已有的对象。

所有这三种视图的工作方式都相似。要获得基本的功能,您真正需要的只是提供一个模型供他们使用。然后视图处理剩下的工作,包括设置表单、验证用户输入、保存数据以及将用户重定向到适当的 URL。

from django.views.generic import edit

from my_app.models import MyModel

class CreateObject(edit.CreateView):

model = MyModel

class EditObject(edit.UpdateView):

model = MyModel

class DeleteObject(edit.DeleteView):

model = MyModel

success_url = '/'

这里唯一令人惊讶的是,除了模型之外,DeleteView实际上还需要指定一个success_urlCreateViewUpdateView都产生一个有效的对象,数据与之相关联,因此它们的默认实现可以简单地调用修改后的对象上的get_absolute_url()

DeleteView的情况下,当视图完成工作时,被访问的对象不再存在,所以get_absolute_url()不是一个选项。由于没有标准的方法来描述对象列表的 URL,Django 无法猜测将用户发送到哪里。因此,为了正确使用DeleteView,你总是需要声明一个success_url

自定义字段

虽然 Django 包含的字段适用于大多数任务,但并不是每个应用都适合其他人认为常见的情况。对于那些现有字段不够用的应用,很容易为表单定义自定义字段,就像创建模型字段一样。创建表单域甚至比创建模型域更容易,因为它们不需要与数据库交互。

模型字段和表单字段的主要区别在于,表单只需要处理字符串输入,这大大简化了处理过程。不需要担心支持多个后端,每个后端都有自己的复杂性,更不用说添加到大量模型字段中的所有不同的查找类型和关系。

如前所述,所有表单字段都继承自Field,位于django.forms.fields。因为表单使用这一事实来区分字段与方法或其他属性,所以所有自定义字段都必须是这个继承链的一部分,以便正常工作。令人欣慰的是,Field提供了许多有用的特性,使得实现特定类型的字段变得更加容易。

像许多其他类一样,字段定义了一些属性和方法来控制特定的行为,例如使用什么小部件和显示什么错误消息,以及如何验证和清除传入的值。它们中的任何一个或全部都可以被覆盖,以自定义特定字段的功能。

确认

也许字段最重要的行为是它如何验证和清理用户输入。毕竟,字段是危险的传入数据和安全的 Python 环境之间的桥梁,所以正确地完成这种转换是非常重要的。字段的clean()方法主要负责这一点,既针对不正确的数据引发异常,又在输入有效时返回干净的值。

该方法的签名只是简单的clean(self, value),接受字段对象本身以及传入的值。然后,如果根据字段的要求,该值被认为是不合适的,那么它应该产生一个django.forms.util.ValidationError的实例,并显示一条消息,指出哪里出错了。否则,它应该将该值转换为适合该字段的任何本机 Python 数据类型,并将其返回。

除了确保错误消息尽可能具有描述性之外,保持错误消息的维护简单也很重要,同时仍然允许单个实例覆盖它们。Django 通过一对名为error_messagesdefault_error_messages的属性以及一个名为error_messages的参数来提供便利。这看起来像是一个价值纠结的巢,但它的工作方式相当简单。

字段类在名为default_error_messages的类级属性中定义其标准错误消息。这是一个将易于识别的键映射到实际错误信息字符串的字典。由于字段通常会从其他字段继承,这些字段可能会定义自己的default_error_messages属性,所以当字段被实例化时,Django 会自动将它们合并到一个字典中。

除了使用default_error_messages,Django 还允许单个字段实例通过error_messages参数覆盖其中的一些消息。该字典中的任何值都将替换指定键的默认值,但仅限于该特定的字段实例。该字段的所有其他实例将不受影响。

这意味着错误消息可能来自三个不同的地方:字段类本身、字段的父类和用于实例化字段的参数。当希望将异常作为clean()方法的一部分时,需要一种简单的方法来检索特定的错误消息,而不管它实际上是在哪里定义的。为此,Django 填充每个字段实例的一个error_messages属性,该属性包含以所有三种方式定义的所有消息。这样,clean()可以简单地在self.error_messages中查找一个键,并使用它的值作为ValidationError的参数。

from django.forms import fields, util

class LatitudeField(fields.DecimalField):

default_error_messages = {

'out_of_range': u'Value must be within -90 and 90.'

}

def clean(self, value):

value = super(LatitudeField, self).clean(value)

if not -90 <= value <= 90:

raise util.ValidationError(self.error_messages['out_of_range'])

return value

class LongitudeField(fields.DecimalField):

default_error_messages = {

'out_of_range': u'Value must be within -180 and 180.'

}

def clean(self, value):

value = super(LatitudeField, self).clean(value)

if not -180 <= value <= 180:

raise util.ValidationError(self.error_messages['out_of_range'])

return value

注意这里使用了super()来调用父DecimalField类的clean()方法,它首先确保该值是有效的小数,然后才检查它是否是有效的纬度或经度。由于无效值会导致异常,如果对DecimalField.clean()的调用允许代码继续执行,那么就可以确保该值是有效的十进制数。

控制小部件

字段类中定义的另外两个属性指定了在某些情况下使用哪些小部件为字段生成 HTML。第一个是widget,它定义了当 field 实例没有明确指定小部件时使用的默认小部件。这被指定为一个小部件类,而不是一个实例,因为小部件与字段本身同时被实例化。

第二个属性叫做hidden_widget,控制当字段应该输出到 HTML 中,但不向用户显示时使用哪个小部件。这不应该被覆盖,因为默认的HiddenInput小部件对于大多数字段来说已经足够了。有些字段,比如MultipleChoiceField,需要指定多个值,所以在这些情况下使用了特殊的MultipleHiddenInput

除了为这些情况指定单独的小部件类之外,字段还可以定义一个widget_attrs()方法来指定一组属性,这些属性应该添加到用于在 HTML 中呈现字段的任何小部件中。它接收两个参数,通常的selfwidget,这是一个完全实例化的小部件对象,任何新的属性都将附加到它上面。widget_attrs()应该返回一个字典,包含应该分配给小部件的所有属性,而不是直接附加属性。这是内置的CharField用来给 HTML 输入字段分配一个maxlength属性的技术。

定义 HTML 行为

如前一节所述,窗口小部件是字段在 Web 页面中以 HTML 表单表示的方式。虽然字段本身更多地处理数据验证和转换,但是小部件关心的是显示表单和接受用户输入。每个字段都有一个关联的小部件,用于处理与用户的实际交互。

Django 提供了各种小部件,从基本的文本输入到复选框和单选按钮,甚至是多选列表框。Django 提供的每个字段都有一个最适合该字段最常见用例的小部件,作为它的widget属性,但是有些情况可能需要不同的小部件。这些小部件可以在单个字段的基础上被覆盖,只需向字段的构造函数提供一个不同的类作为widget参数。

自定义小部件

像字段一样,Django 提供的小部件对于大多数常见的情况都很有用,但是不能满足所有的需求。有些应用可能需要提供额外的信息,如度量单位,以帮助用户准确地输入数据。其他人可能需要集成客户端 JavaScript 库来提供额外的选项,比如用于选择日期的日历。这些类型的附加特性是由定制的小部件提供的,它们满足了相关领域的需求,同时在 HTML 中提供了很大的灵活性。

虽然没有像字段那样严格执行,但所有的小部件都应该从django.forms.widgets.Widget继承,以便从一开始就获得最常见的功能。然后,每个定制小部件可以覆盖最适合它需要执行的任务的任何属性和方法。

呈现 HTML

定制小部件最常见的需求是通过 HTML 的方式为用户提供定制的字段显示。例如,如果一个应用需要一个字段来处理百分比,如果它的小部件可以在输入字段后输出一个百分号(%),那么用户使用这个字段会更容易。这可以通过覆盖小部件的render()方法来实现。

除了正常的selfrender()方法接收三个额外的参数:HTML 元素的name,当前与之关联的valueattrs,一个应该应用于元素的属性字典。其中,只有attrs是可选的,如果没有提供,它将默认为一个空字典。

>>> from django import forms

>>> class PriceInput(forms.TextInput):

...     def render(self, name, value, attrs=None):

...         return '``$ %s

...

>>> class PercentInput(forms.TextInput):

...     def render(self, name, value, attrs=None):

...         return '``%s %%

...

>>> class ProductEntry(forms.Form):

...     sku = forms.IntegerField(label='SKU')

...     description = forms.CharField(widget=forms.Textarea())

...     price = forms.DecimalField(decimal_places=2, widget=PriceInput())

...     tax = forms.IntegerField(widget=PercentInput())

...

>>> print ProductEntry()

<tr><th><label for="id_sku">SKU:</label></th><td><input type="text" name="sku" i

d="id_sku" /></td></tr>

<tr><th><label for="id_description">Description:</label></th><td><textarea id="i

d_description" rows="10" cols="40" name="description"></textarea></td></tr>

<tr><th><label for="id_price">Price:</label></th><td> $ <input type="text" name="

price" id="id_price" /> </td></tr>

<tr><th><label for="id_tax">Tax:</label></th><td> <input type="text" name="tax" i

d="id_tax" /> % </td></tr>

从发布的数据中获取值

由于小部件都与处理 HTML 有关,并且值是使用 HTML 指定的格式提交给服务器的,在 HTML 元素规定的结构中,小部件提供了在传入数据和数据映射到的字段之间进行转换的额外功能。这不仅将字段与 HTML 输入如何工作的细节隔离开来,也是管理使用多个 HTML 输入的小部件的唯一方法,并且允许小部件在 HTML 输入没有提交任何内容的情况下填充默认值,如None

负责这个任务的小部件方法是value_from_datadict(),除了标准的self之外,它还接受三个参数。

  • data—提供给表单构造器的字典,通常为request.POST
  • files—传递给表单构造器的文件,使用与request.FILES相同的格式
  • name—小部件的名称,实际上就是字段的名称加上添加到表单中的任何前缀

该方法使用所有这些信息来检索从浏览器提交的值,进行任何必要的更改,并返回适合字段使用的值。这应该总是返回一个值,如果找不到合适的值,默认为None。默认情况下,所有 Python 函数都返回None,如果它们不返回其他任何东西的话,所以只要确保value_from_datadict()不引发任何异常,就很容易遵循这条规则,但是为了可读性,最好总是显式返回None

跨多个小部件拆分数据

由于小部件是字段和 HTML 之间的桥梁,它们对使用什么 HTML 以及如何向字段报告有很大的控制权。事实上,以至于有可能将一个单独的字段分割成多个 HTML 字段控件。由于render()value_from_datadict()钩子放置在流程中的位置,这甚至可以在现场不知道的情况下完成。

具体如何工作很大程度上取决于小部件将使用什么样的 HTML 输入,但总体思路很简单。一个字段将其值传递给小部件的render()方法,该方法将其分解成多个 HTML 输入,每个输入包含一部分原始值。一个例子是为DateTimeField的每个日期和时间部分设置一个单独的文本框。

然后,当小部件通过它的value_from_datadict()方法接收回数据时,它将这些片段重新组合成一个值,然后将这个值返回给字段。无论小部件做什么,字段都不需要处理多个值。

不幸的是,这都要求每个小部件负责所有的 HTML 标记,并在收到值时重新组装。有时候,简单地组合两个或更多现有的字段,依靠它们的小部件来完成这项工作也是一样有用的。因为有一个实用程序来帮助解决这个问题非常方便,所以 Django 提供了一个。

准确地说,Django 提供了两个实用程序:一个字段MultiValueField和一个小部件MultiWidget,它们被设计为协同工作。就其本身而言,它们在现实世界中并不十分有用。相反,它们提供了大量必要的特性,同时允许子类填充特定用例的细节。

在字段方面,在清理数据时,MultiValueField通过对组成组合的每个单独的字段进行验证来处理细节。它留给子类的唯一两件事是定义哪些字段应该合并,以及它们的值应该如何压缩成适合其他 Python 代码使用的单个值。例如,在 Django 中,SplitDateTimeField组合了一个DateField和一个TimeField,并将它们的值压缩到一个单独的datetime对象中。

定义应该使用哪些字段的过程很简单,在新字段类的__init__()方法中处理。所要做的就是用应该组合的字段实例填充一个元组。然后,简单地将这个元组作为第一个参数传递给父类的__init__()方法,该方法从那里处理其余的部分。这使得特定字段的方法定义非常简单,通常只有几行。

压缩由这些多个字段生成的值发生在compress()方法中。除了通常的self之外,这还需要一个值,?? 是一个值序列,应该组合成一个本地 Python 值。然而,内部发生的事情可能会更复杂一些,因为有一些情况需要考虑。

首先,对于字段的任何部分,可能根本没有提交任何值,这意味着传入的数据将是一个空列表。默认情况下,字段是必需的,在这种情况下,在调用compress()之前会抛出一个异常。如果一个字段用required=False声明,这是一个非常可能的场景,在这种情况下,该方法应该返回None

此外,很有可能只提交一部分值,因为它被分割在多个 HTML 输入中。同样,如果该字段是必需的,这是自动处理的,但是如果该字段是可选的,compress()仍然必须做一些额外的工作,以确保如果任何值被提交,所有的值都会被提交。这通常通过对照标准的EMPTY_VALUES元组检查值序列中的每一项来处理,该元组也位于django.forms.fields。包含空值的字段的任何部分都应该引发一个异常,通知用户字段的哪个部分缺少值。

然后,如果所有的值都被提交并且是有效的,compress()执行它真正的工作,在处理表单时返回一个适合 Python 使用的值。这个返回值的确切性质将完全取决于所创建的字段的类型,以及它的预期用途。考虑以下字段示例,该字段接受纬度和经度坐标作为单独的小数,并将它们组合成一个简单的元组。

from django.forms import fields

class LatLonField(fields.MultiValueField):

def __init__(self, *args, **kwargs):

flds = (LatitudeField(), LongitudeField())

super(LatLonField, self).__init__(flds, *args, **kwargs)

def compress(self, data_list):

if data_list:

if data_list[0] in fields.EMPTY_VALUES:

raise fields.ValidationError(u'Enter a valid latitude.')

if data_list[1] in fields.EMPTY_VALUES:

raise fields.ValidationError(u'Enter a valid longitude.')

return tuple(data_list)

return None

解决了字段方面的问题后,下一步是创建一个小部件来分别捕获这两个元素。因为想要显示的只是两个文本框,所以让定制小部件由两个TextInput小部件简单组合是有意义的,这解决了识别要使用的小部件的第一个挑战。base MultiWidget在呈现输出和从传入数据中检索值方面做得很好,所以剩下的唯一挑战是将单个压缩值转换成由各个小部件呈现的值列表。

如您所料,与字段的compress()方法相对应的是小部件的decompress()方法。它的签名非常相似,只接受一个值,但它的任务是将该值分成尽可能多的小部件来呈现它们。通常,这是从单个值中取出一些比特和片断,并把它们放入一个序列中,比如一个元组或一个列表。由于前面显示的LatLonField直接将其值输出为一个元组,所以剩下的唯一事情就是提供一个空值元组(如果没有提供的话)。

from django.forms import fields, widgets

class LatLonWidget(widgets.MultiWidget):

def __init__(self, attrs=None):

wdgts = (widgets.TextInput(attrs), widgets.TextInput(attrs))

super(LatLonWidget, self).__init__(wdgts, attrs)

def decompress(self, value):

return value or (None, None)

class LatLonField(fields.MultiValueField):

widget = LatLonWidget

# The rest of the code previously described

自定义表单标记

除了定义自定义小部件,还可以自定义表单本身如何呈现为 HTML。与前面的例子不同,在 Django 的模板语言中使用了下面的技术,这使得针对单个表单进行修改变得更加容易。

可以定制的最明显的东西是实际的<form>元素,因为 Django 表单根本不输出它。这主要是因为没有办法假设表单应该使用 GET 还是 POST,以及应该发送到哪个 URL。任何需要提交回服务器的表单都需要手动指定,因此这是一些专门化的绝佳机会。例如,当使用包含一个FileField的表单时,<form>元素需要包含一个属性,比如enctype="multipart/form-data"

除了表单的提交行为之外,需要配置的一个常见内容是使用级联样式表(CSS)来呈现表单。用 CSS 引用元素有很多方法,但最有用的两种方法是分配一个 ID 或一个类,这两种方法通常都放在<form>元素本身上。因为必须定义该元素,所以添加这些额外的属性也很容易。

此外,根据站点整体外观的实现方式,还经常需要配置表单字段的显示方式。不同的站点可能会使用表格、列表甚至简单的段落来呈现表单,所以 Django 试图尽可能简单地适应这些不同的场景。

在模板中输出表单时,有几种方法可以选择使用哪种输出格式。默认情况下,as_table将每个字段包装在一行中,适合在标准表中使用,而as_ul()将字段包装在列表项中,as_p()将字段包装在段落中。然而,这些都没有输出所有字段周围的任何类型的元素;这就留给了模板,这样就可以添加额外的属性,比如 CSS 引用的 id 和类,就像 form 元素一样。

虽然提供的这三种方法对于它们自己的目的是有用的,但是它们并不一定适合每种情况。为了与 DRY 保持一致,它们实际上都是围绕一个公共方法定制的包装器,该方法将任何类型的标记包装在表单的所有字段周围。这个常见的方法_html_output()不应该直接从表单外部调用,但是非常适合由另一个为更具体的目的而设计的定制方法使用。它有许多参数,每个参数指定 HTML 输出的不同方面。

  • normal_row—用于标准行的 HTML。它被指定为将接收字典的 Python 格式字符串,因此这里可以放置几个值:errorslabelfieldhelp_text。这些应该是不言自明的,除了field实际上包含由字段的小部件生成的 HTML。
  • error_row—用于仅包含错误消息的行的 HTML,主要用于与特定字段无关的表单级错误。根据列表末尾描述的errors_on_separate_row选项,它还用于配置为在独立于字段本身的行上显示字段错误的表单。它也是一个 Python 格式的字符串,带有一个未命名的参数,即要显示的错误。
  • row_ender—用于标识行尾的标记。由于前面的行必须直接指定它们的结尾,所以不用将它追加到行中,而是将任何隐藏字段插入到最后一行,就在它的结尾之前。因此,始终确保以下情况成立:normal_row.endswith(row_ender)
  • help_text_html—写出帮助文本时使用的 HTML。这个标记将直接放在小部件之后,并将帮助文本作为这个格式字符串的一个未命名参数。
  • errors_on_separate_row—一个布尔值,指示在呈现字段本身之前是否应使用error_row呈现字段错误。这不会影响传递给normal_row的值,所以如果表单希望错误出现在单独的行上,一定要将错误排除在格式字符串之外。否则,错误将被打印两次。

访问单个字段

除了能够在 Python 中定制表单的整体标记之外,在表单本身上,直接在模板中指定表单的标记也非常简单。这样,表单尽可能地可重用,同时仍然允许模板对呈现的标记进行最终控制。

使用第二章中描述的技术,表单对象是可迭代的。这意味着模板可以使用for block 标签简单地遍历它们,每次迭代都是表单上的一个字段,它已经被绑定到一个值。然后,这个绑定的字段对象可以用来显示字段的各个方面,在任何对模板最有意义的标记中。它有很好的属性和方法选择来帮助这个过程。

  • field—原始字段对象及其所有相关属性
  • data—绑定到字段的当前值
  • errorsErrorList(如下一节所述)包含该字段的所有错误
  • is_hidden—一个布尔值,指示默认小工具是否为隐藏输入
  • label_tag()—HTML<label>元素及其内容,用于字段
  • as_widget()—字段的默认呈现,使用为其定义的小部件
  • as_text()—使用基本TextInput而不是自己的小部件呈现的字段
  • as_textarea()—使用Textarea而不是为其定义的小部件呈现的字段
  • as_hidden()—使用隐藏输入而不是任何可见小部件呈现的字段

自定义错误的显示

默认情况下,用于显示错误的标记由一个名为ErrorList的特殊 Python 类指定,它位于django.forms.util。这就像一个标准的 Python 列表,除了它有一些额外的方法以 HTML 的表单输出它的值。特别是,默认情况下,它有两个方法,as_ul()as_text(),分别将错误输出为无序列表或未加修饰的文本。

通过创建一个定制的错误类,作为ErrorList的子类,很容易在显示错误时覆盖这些方法来提供定制的标记。该标记包括任何包含元素,如<ul>,因为无论是作为默认标记的一部分,还是通过直接访问字段的errors属性,整个标记都将被放置在显示字段错误的地方。

默认情况下,as_ul()方法用于呈现错误,尽管希望进行进一步定制的模板可以调用对模板最有意义的方法。事实上,可以添加全新的方法,甚至可以通过覆盖__unicode__()方法来覆盖默认使用的方法。模板也可以简单地遍历列表中的错误,并根据情况用任何有意义的标记包装每个错误。

编写一个定制的ErrorList子类是不够的;它还必须以某种方式传递到表单中,以确保它被使用。这也很简单:只需将自定义类作为error_class参数传递给表单的构造函数。

除了显示单个字段的错误,表单的clean()方法还允许显示整个表单验证失败的错误。在模板中显示它需要访问表单的non_field_errors()方法。

应用技术

虽然 Django 的表单主要是为处理相当常见的用户输入需求而设计的,但是也可以让它们做一些复杂的跑腿工作。它们既可以单独使用,也可以成组使用,以进一步扩展用户界面。几乎任何形式的用户输入都可以用 Django 表单来表示;以下只是可用内容的一个示例。

挂起和恢复表单

表单通常是用来一次接收所有的输入,处理输入并相应地运行。这是一个一次性的循环,表单必须重新显示的唯一原因是显示验证错误,允许用户修复错误并重新提交。如果用户需要暂时停止处理表单,稍后再回来,这意味着从头开始。

虽然这是普遍接受的方法,但对于复杂的表单或用户可能需要提供需要时间收集的信息(如税务信息)的表单来说,这也是一种负担。在这些情况下,如果能够以部分填充的状态保存表单,并在以后的某个时间点返回该表单,将会更加有用。这不是表单通常的工作方式,所以显然有一些工作要做,但这真的没有那么难。

因为表单被声明为类,所以没有理由违反这一假设,此后开发的类将可以作为父类使用,就像forms.Form一样。事实上,从所有的意图和目的来看,它应该是标准类的替代物,简单地给它的子类注入额外的功能。考虑在properties申请中提供房子的以下表格,这通常不会被轻易接受。通过允许表单被挂起并在以后恢复,用户可以在承诺这样的投资之前花必要的时间来查看报价。

from django import forms

from django_localflavor_us import forms as us_forms

from pend_form.forms import PendForm

class Offer(PendForm):

name = forms.CharField(max_length=255)

phone = us_forms.USPhoneNumberField()

price = forms.IntegerField()

注意,除了切换到PendForm之外,这是像任何其他标准 Django 表单一样定义的。这一简单改变的优点将在下面的章节中描述,这些章节概述了一个新的pend_form应用。

为以后存储值

为了保存处于部分完成状态的表单,它的当前值必须以某种方式存储在数据库中。它们还必须与字段名相关联,以便以后可以用来重新创建表单。这听起来像是动态模型的工作,它可以根据表单的定义自动创建,以有效地存储值。然而,由于一些原因,它们不适合这个用例。

首先,表单字段没有直接等价的模型字段。因为动态模型必须填充与表单字段包含相同数据的字段,所以必须有某种方法来基于表单字段确定模型字段。模型字段确实定义了可以与它们一起使用的表单字段,但不是相反。

从技术上讲,可以手动提供表单域到模型域的映射,这样无论如何都可以创建这样的模型。这也存在一些问题,因为它不能支持自定义表单域。本质上,任何不存在于映射中表单字段都没有匹配的模型字段,这种技术就会失败。

此外,在基于表单字段类型的模型字段中存储字段值需要首先将这些值转换为 Python 对象,这意味着它们都必须是有效值。应该可以挂起一个表单,即使有无效的值,以便以后可以更正。如果必须用特定的数据类型(包括数据验证或类型检查)将值填充到模型字段中,这是完全不可能的。

相反,我们可以相信,所有表单数据在提交回服务器时都是以字符串的形式到达的。作为表单验证过程的一部分,必须将这些字符串转换为原生 Python 对象,因此字符串本身是从提交的表单中获取实际原始数据的最后机会。更好的是,由于它们都是字符串,Django 提供了一种简单的方法来存储它们以备后用:TextField。一个TextField是必要的,因为不同的表单值提供不同长度的数据,其中一些可能会超过CharField的 255 个字符的限制。

有了存储值的可靠方法,下一步就是确定数据库中还必须存储哪些信息,以便重新构建表单。显然,应该包括字段的名称,这样就可以将值放回正确的位置。此外,由于不同的表单可能有不同的结构,具有不同数量的字段,因此最好在数据库中为每个字段的值指定自己的行。这意味着需要有一种方法将字段作为表单的一部分保存在一起。

这里的技巧是表单没有唯一的标识符。毕竟,通常不希望它们存在于特定的请求/响应周期之外,除了验证更正,在验证更正中,整个表单作为新请求的一部分被重新提交。根本没有内置的方法来标识表单的实例,所以必须使用不同的方法。

识别这种复杂结构的一种非常常见的方法是根据数据创建一个散列。虽然不能保证散列是唯一的,但对于大多数目的来说,它们已经足够接近了,而且有些东西可以和散列一起包含,以获得更好的唯一性。

在表单的情况下,这个哈希可以从完整的字段数据集合中获取,因此任何名称或值的更改都会导致数据产生的哈希发生变化。可以与哈希一起存储的另一条信息是表单的导入路径,如果有多个表单具有相同的字段集合,这可以区分多组数据。

现在有一些信息需要存储,考虑它们应该如何相互关联。这里本质上有两个层次:表单和它的值。这可以看作是两个独立的模型,通过标准的外键关系将多个值关联到一个表单。表单端包含表单的路径及其所有值的散列,而值端包含每个字段的名称和值,以及对它所属表单的引用。

pend_form应用的models.py模块如下所示:

class PendedForm(models.Model):

form_class = models.CharField(max_length=255)

hash = models.CharField(max_length=32)

class PendedValue(models.Model):

form = models.ForeignKey(PendedForm, related_name='data')

name = models.CharField(max_length=255)

value = models.TextField()

这个简单的结构现在能够存储任何形式的任何数量的数据。如果应用需要对表单数据进行复杂的查询,效率可能会很低,但是因为它只是用来一次性保存和恢复表单的内容,所以它会工作得很好。

既然已经有了包含表单数据的模型,就需要有一种方法来实际存储这些数据,以便以后检索。幸运的是,表单只是标准的 Python 类,所以只需编写一个额外的方法来直接处理这项任务就足够简单了。然后,当编写需要这种能力的特定表单时,它可以简单地继承下面的表单,而不是通常的forms.Form。这被放在我们的pend_form应用的一个新的forms.py模块中。

try:

from hashlib import md5

except:

from md5 import new as md5

from django import forms

from pend_form.models import PendedForm

class PendForm(forms.Form):

@classmethod

def get_import_path(cls):

return '%s.%s' % (cls.__module__, cls.__name__)

def hash_data(self):

content = ','.join(%s:%s' % (n, self.data[n]) for n in self.fields.keys())

return md5(content).hexdigest()

def pend(self):

import_path = self.get_import_path()

form_hash = self.hash_data()

pended_form = PendedForm.objects.get_or_create(form_class=import_path

hash=form_hash)

for name in self.fields:

pended_form.data.get_or_create(name=name, value=self.data[name])

return form_hash

注意这里对get_or_create()的自由使用。如果一个表单的实例已经存在,并且具有完全相同的值,那么将整个表单保存两次是没有意义的。相反,它只是依赖于这样一个事实,即前一个副本在功能上是相同的,所以它对两者都适用。

重构一个表单

现在,表单可以不经过完全处理甚至验证就放在数据库中,如果以后用户不能检索它们来继续使用它们,它们的用处仍然有限。数据以这样一种方式存储,它可以重新组合成一种表单,剩下的就是实际这样做了。

根据定义,执行此操作的代码必须在使用表单实例之前被调用,因此看起来它必须在模块级函数中。请记住,如果需要的话,可以声明方法在类上使用,而不是在实例上使用。因为这里的目标是将所有这些功能封装在一个子类中,而不必担心所有的机制本身是在哪里编写的,所以一个类方法就可以很好地完成这个任务。

这个新类方法中实际发生的事情更有趣一些。为了实例化一个表单,它将一个字典作为它的第一个参数,这个参数通常只是request.POST,对所有视图都可用。当以后加载表单时,新的请求与表单完全无关,更不用说它包含适当的数据,因此必须从先前存储在数据库中的数据手动构建字典。

这些数据可能会被前面描述的表单散列以及正在使用的表单的导入路径所引用。这两条信息是从数据库中正确定位和检索所有字段值所需的全部信息。由于表单已经知道如何获得它的导入路径,由于前面描述的方法之一,剩下的就是手动提供表单的散列。这最有可能在 URL 模式中被捕获,尽管不同的应用可能有不同的方式来实现这一点。

一旦知道了散列,恢复表单的方法应该能够接受它,将它与自己的导入路径结合起来,从数据库中检索值,根据这些值填充字典,用这些值实例化表单的新副本,并返回新表单供其他代码使用。这听起来工作量很大,但比看起来容易得多。

这里需要解决的一个问题是如何实例化 Python 自己的字典。内置的dict()可以接受各种不同的参数组合,但是其中最有用的是一个 2 元组序列,每个元组包含目标字典中一个条目的名称和值。因为 QuerySets 已经返回了序列,而且像 list comprehensions 和 generator expressions 这样的工具可以很容易地基于它们创建新的序列,所以创建合适的序列是很容易的。

获取导入路径和查找保存的表单很容易,并且该对象的data属性提供了对其所有值的轻松访问。使用生成器表达式,数据的名称/值对可以很容易地传递到内置的dict()中,创建一个可以传递到表单对象的构造函数中的字典。所有这些都由法典明确规定。

@classmethod

def resume(cls, form_hash):

import_path = cls.get_import_path()

form = models.PendForm.objects.get(form_class=import_path, hash=form_hash)

data = dict((d.name, d.value) for d in form.data.all())

return cls(data)

当使用表单生成的哈希值调用这个简单的方法时,它将返回一个完整的表单对象,准备好进行验证并呈现给用户以供进一步查看。事实上,验证和演示将是这种情况下的典型工作流,让用户有机会在决定提交表单或稍后再次挂起表单之前,查看是否有任何需要添加或更正的内容。

完整的工作流程

如前所述,正常的工作流是相当标准的,在野外使用的各种表单之间几乎没有变化。通过允许挂起或恢复表单,工作流中增加了一个可选的额外步骤,这需要在视图中进行一些额外的处理。将这个新的部分添加到拼图中,整个工作流程看起来有点像这样:

Display an empty form.   User fills in some data.   User clicks Submit.   Validate data submitted by the user.   Display the form with errors.   User clicks Pend.   Save form values in the database.   Validate data retrieved from the database.   Display the form with errors.   Process the completed form.

为了维护整个工作流,视图变得有点复杂。现在有四条不同的路径可供选择,这取决于在任何给定时间处理工作流的哪一部分。请记住,这只是采取必要的步骤来处理表单。它没有考虑特定应用所需的任何业务逻辑。

  • 用户请求一个没有任何数据的表单。
  • 用户使用 Pend 按钮发布数据。
  • 用户使用表单哈希请求表单。
  • 用户使用提交按钮发布数据。

从这里开始,典型的工作流步骤仍然适用,例如检查输入数据的有效性,并采取特定于应用功能的适当步骤。一旦在一个视图中将这些内容汇总在一起,它看起来就像这样:

from django import http

from django.shortcuts import render_to_response

from django.template.context import RequestContext

from properties import models, forms

def make_offer(request, id, template_name='', form_hash=None):

if request.method == 'POST':

form = forms.Offer(request.POST)

if 'pend' in request.POST:

form_hash = form.pend()

return http.HttpRedirect(form_hash)

else:

if form.is_valid():

# This is where actual processing would take place

else:

if form_hash:

form = forms.Offer.resume(form_hash)

else:

form = forms.Offer()

return render_to_response(template_name, {'form': form}

context_instance=RequestContext(request))

这里发生了很多事情,但很少与房子报价有关。绝大多数代码的存在只是为了管理表单在任何给定时间可能处于的所有不同状态,并且每次视图使用PendForm子类时都必须重复这些代码,这是没有效率的。

使其通用化

虽然很容易看出视图的哪些方面是重复的,因此应该将其分解成可重用的东西,但是决定如何做有点棘手。主要的问题是,特定于这个特定视图的代码部分不仅仅是一个字符串或一个数字,就像前面的大多数例子中显示的那样,而是一个代码块。

这是一个问题,因为前面的例子已经展示了如何使用通用视图来提取共性,同时允许在 URL 模式中指定具体的差异。这对于基本的数据类型,如字符串、数字、序列和字典来说很有效,但是代码的处理方式不同。这些代码不能在 URL 模式中直接指定值,而是必须在一个单独的函数中定义,然后传递给模式。

虽然这肯定是可能的,但它使 URL 配置模块变得有点麻烦,因为在每个 URL 模式块上面可能声明了许多顶级函数。Lambda 风格的函数可以解决这个问题,但是由于它们仅限于执行简单的表达式,没有循环或条件,它们会严重限制可以使用的代码类型。

一种替代方法是装饰器,它可以应用于标准函数,在包装器中提供所有必要的功能。这样,任何函数都可以用来包含实际处理表单的代码,Python 的全部功能都由它支配。这些代码也不必处理挂起或恢复表单所需的任何样板文件,因为装饰器甚至可以在视图代码本身执行之前完成所有这些工作,只需将表单作为参数传入即可。如果使用装饰器来移除样板文件,那么前面的视图可能是这样的。

from pend_forms.decorators import pend_form

@pend_form

def make_offer(request, id, form):

# This is where actual processing would take place

现在剩下的就是编写装饰器本身,封装从上一个示例中移除的功能,将它包装在将要传入的视图周围。这将被放在一个新的decorators.py模块中。

from django import http

from django.shortcuts import render_to_response

from django.template.context import RequestContext

from django.utils.functional import wraps

def pend_form(view):

@wraps(view)

def wrapper(request, form_class, template_name

form_hash=None, *args, **kwargs):

if request.method == 'POST':

form = form_class(request.POST)

if 'pend' in request.POST:

form_hash = form.pend()

return http.HttpRedirect(form_hash)

else:

if form.is_valid():

return view(request, form=form, *args, **kwargs)

else:

if form_hash:

form = form_class.resume(form_hash)

else:

form = form_class()

return render_to_response(template_name, {'form': form}

context_instance=RequestContext(request))

return wrapper

现在,所有需要做的就是设置一个 URL 配置,提供一个表单类和一个模板名。这个装饰器将处理剩下的工作,只在表单完成并提交处理时调用视图。

基于班级的方法

既然您已经看到了如何使用传统的基于函数的视图来实现这一点,请记住 Django 新的基于类的视图为许多问题提供了一种不同的方法,这也不例外。在这一章的前面,你已经看到了FormView类是如何提供你使用表单所需的大部分功能的,我们也可以扩展它来使用我们的待定功能。事实上,因为我们可以向视图类添加新方法,所以不再需要提供自定义的Form子类。可以使用代码中的任何股票表单来完成。让我们从检索一个以前挂起的表单开始。以前放在Form子类中的一些实用方法可以原封不动地在这里重用,但是我们还需要一种方法将现有的值传递到新的表单中,这对于get_form_kwargs()来说是一个完美的任务。

from django.views.generic.edit import FormView

from pend_form.models import PendedValue

class PendFormView(FormView):

form_hash_name = 'form_hash'

def get_form_kwargs(self):

"""

Returns a dictionary of arguments to pass into the form instantiation.

If resuming a pended form, this will retrieve data from the database.

"""

form_hash = self.kwargs.get(self.form_hash_name)

if form_hash:

import_path = self.get_import_path(self.get_form_class())

return {'data': self.get_pended_data(import_path, form_hash)}

else:

return super(PendFormView, self).get_form_kwargs()

# Utility methods

def get_import_path(self, form_class):

return '%s.%s' % (form_class.__module__, form_class.__name__)

def get_pended_data(self, import_path, form_hash):

data = PendedValue.objects.filter(import_path=import_path, form_hash=form_hash)

return dict((d.name, d.value) for d in data)

因为get_form_kwargs()的目的是为表单的实例化提供参数,所以我们在这里真正需要做的是检索适当的值并返回它们,而不是默认值。如果在 URL 中提供了表单散列,这将足以填充填充的表单。

还要注意的是,form_hash_name是作为一个类级别的属性包含进来的。这允许该视图的用户覆盖指示表单被挂起的参数。您所需要做的就是将它作为一个类属性提供,Django 将允许对它进行定制,返回到您定义的默认值。

下一阶段将允许用户实际保存表单值以备后用。和以前一样,这将需要在数据库中存储表单及其值,以及该信息的散列,以便以后检索。除了一些额外的实用程序,大部分工作必须在post()方法中完成,因为这是我们提交表单时的入口点。

保存表单的原始功能包括相当多的部分,其中一些可以从前面的步骤中重用。以下是保存表单以备后用所需的内容,因此我们可以在一起展示所有代码之前讨论一下。

from django.views.generic.edit import FormView

from pend_form.models import PendedForm, PendedValue

class PendFormView(FormView):

pend_button_name = 'pend'

def post(self, request, *args, **kwargs):

"""

Handles POST requests with form data. If the form was pended, it doesn't follow

the normal flow, but saves the values for later instead.

"""

if self.pend_button_name in self.request.POST:

form_class = self.get_form_class()

form = self.get_form(form_class)

self.form_pended(form)

else:

super(PendFormView, self).post(request, *args, **kwargs)

# Custom methods follow

def get_import_path(self, form_class):

return '%s.%s' % (form_class.__module__, form_class.__name__)

def get_form_hash(self, form):

content = ','.join('%s:%s' % (n, form.data[n]) for n in form.fields.keys())

return md5(content).hexdigest()

def form_pended(self, form):

import_path = self.get_import_path(self.get_form_class())

form_hash = self.get_form_hash(form)

pended_form = PendedForm.objects.get_or_create(form_class=import_path

hash=form_hash)

for name in form.fields.keys():

pended_form.data.get_or_create(name=name, value=form.data[name])

return form_hash

post()方法通常在form_valid()form_invalid()方法之间调度,但是由于挂起的表单不一定有效或无效,所以需要覆盖它以提供第三个调度选项。第三个分派由form_pended()处理,它的名字与 Django 自己的表单有效性方法一致。它完成保存表单及其相关数据的工作,重用 Django 的一些工具,以及显示挂起表单的前一次迭代。

这是所有这些看起来的样子:

from django.views.generic.edit import FormView

from pend_form.models import PendedForm, PendedValue

class PendFormView(FormView):

form_hash_name = 'form_hash'

pend_button_name = 'pend'

def get_form_kwargs(self):

"""

Returns a dictionary of arguments to pass into the form instantiation.

If resuming a pended form, this will retrieve data from the database.

"""

form_hash = self.kwargs.get(self.form_hash_name)

if form_hash:

import_path = self.get_import_path(self.get_form_class())

return {'data': self.get_pended_data(import_path, form_hash)}

else:

return super(PendFormView, self).get_form_kwargs()

def post(self, request, *args, **kwargs):

"""

Handles POST requests with form data. If the form was pended, it doesn't follow

the normal flow, but saves the values for later instead.

"""

if self.pend_button_name in self.request.POST:

form_class = self.get_form_class()

form = self.get_form(form_class)

self.form_pended(form)

else:

super(PendFormView, self).post(request, *args, **kwargs)

# Custom methods follow

def get_import_path(self, form_class):

return '{0}.{1}'.format(form_class.__module__, form_class.__name__)

def get_form_hash(self, form):

content = ','.join('{0}:{1}'.format(n, form.data[n]) for n in form.fields.keys())

return md5(content).hexdigest()

def form_pended(self, form):

import_path = self.get_import_path(self.get_form_class())

form_hash = self.get_form_hash(form)

pended_form = PendedForm.objects.get_or_create(form_class=import_path

hash=form_hash)

for name in form.fields.keys():

pended_form.data.get_or_create(name=name, value=form.data[name])

return form_hash

def get_pended_data(self, import_path, form_hash):

data = PendedValue.objects.filter(import_path=import_path, form_hash=form_hash)

return dict((d.name, d.value) for d in data)

现在,您可以像使用任何其他基于类的视图一样使用它。您需要做的就是为它提供一个表单类,并覆盖这里或FormView中指定的任何默认值。模板、按钮名称和 URL 结构可以通过简单地子类化PendFormView并从那里开始工作来定制。除此之外,您唯一需要做的就是在模板中添加一个按钮,允许用户挂起表单。

现在怎么办?

为了在现实世界中真正有用,表单必须作为 HTML 页面的一部分呈现给用户。Django 没有尝试直接在 Python 代码中生成 HTML 内容,而是提供了模板作为一种对设计人员更友好的替代方式。

六、模板

Abstract

虽然第二章明确指出 Django 完全构建在 Python 之上,并且适用标准的 Python 规则,但模板是规则的例外。模板是 Django 生成基于文本的输出的方式,比如 HTML 或电子邮件,编辑这些文档的人可能没有任何 Python 经验。因此,模板被设计成避免直接使用 Python,而是倾向于使用专为 Django 构建的可扩展、易于使用的定制语言。

虽然第二章明确指出 Django 完全构建在 Python 之上,并且适用标准的 Python 规则,但模板是规则的例外。模板是 Django 生成基于文本的输出的方式,比如 HTML 或电子邮件,编辑这些文档的人可能没有任何 Python 经验。因此,模板被设计成避免直接使用 Python,而是倾向于使用专为 Django 构建的可扩展、易于使用的定制语言。

通过禁止任意的 Python 表达式,模板在某些方面肯定会受到限制,但是有两点需要记住。首先,模板系统是由 Python 支持的,就像 Django 中的其他东西一样,所以总是可以为特定的特性添加 Python 级别的代码。在模板本身中包含实际的 Python 代码是不好的形式,所以 Django 提供了插入额外代码的其他方法。

更重要的是,在模板和支持它们的 Python 代码之间划出一条清晰的界限,可以让两个不同背景和技能的群体一起工作。对于许多业余爱好者的项目来说,这可能听起来像是一种浪费,因为在网站上工作的人只有开发人员。然而,在许多商业环境中,开发人员通常是与维护网站内容和视觉结构的人员分开的一群人。

通过明确分离开发和模板编辑的任务,很容易建立一个环境,在这个环境中,开发人员从事他们真正需要的工作,而内容编辑和设计人员可以从事不需要开发经验的工作。Django 的模板本质上相当简单,任何人都很容易掌握,甚至没有任何编程经验的人。

模板语法的基本细节以及包含的标签和过滤器在其他地方有很好的描述。本章将不再关注这些更高层次的细节,而是涵盖如何加载、解析和呈现模板,如何在模板中管理变量,以及如何创建新的标签和过滤器。本质上,这是关于开发者可以做些什么来使他们的内容编辑同行的生活尽可能的简单。

模板是由什么组成的

尽管模板不是直接用 Python 编写的,但它们有 Python 的支持,使得所有好的东西成为可能。当从文件或其他来源读入模板的代码时,它被编译成 Python 对象的集合,这些对象负责在以后呈现它。对于基本的模板使用,可以忽略这些对象是如何工作的,但是和其他事情一样,正确的理解可以打开一个可能性的世界。

查看一下django.template包内部,Template类作为模板操作的起点非常突出,这是理所当然的。当一个模板被加载时,它的内容被传递给一个新的Template实例,还有一些关于模板本身来自哪里的可选信息。有三个参数被传入新的Template对象,这是一切的基础。

  • template_string—唯一必需的参数,它包含从文件中读取的模板的实际内容。这里最棒的是Template接受一个字符串,而不是文件名或打开的文件对象。只接受一个字符串——无论是 Unicode 字符串还是 UTF 8 编码的常规字符串——就可以从任何来源设置模板。在本章的应用技巧中可以找到一些有趣的用法。
  • origin—表示模板来源的对象,如模板加载器或只是一个原始字符串。只有当TEMPLATE_DEBUG设置为True时才使用它,通常可以忽略它而不会受到惩罚,但是最好将它包含在开发环境中,在那里它可以帮助调试涉及多个模板加载器的问题。
  • name—模板的名称,传递给任何请求它的加载程序(如果有的话)。这通常只是模板的相对路径,但理论上可以是在特定情况下有意义的任何路径。毕竟 Django 真正关心的是template_string;其余的只是在调试问题时有用。

Template的实际代码相当少,将大部分工作委托给一个名为compile_string()的实用函数,该函数解析原始文本,将其编译成一系列节点。这些节点只是 Python 对象,每个节点都是为模板的特定部分配置的。总的来说,它们代表了整个模板,从开始到结束,以一种更容易和更有效的方式呈现。

这些节点作为名为nodelist的属性附加到模板上。当用数据呈现模板时,它简单地遍历这个列表,单独呈现每个节点。这使得Template代码非常少,同时允许最大的灵活性。毕竟,如果每个单独的主题负责呈现自己,它就拥有 Python 的全部能力。因此,创建或定制模板节点是编写一些真正的 Python 代码的简单事情。

例外

所有这些都假设模板一直都工作正常。使用模板时,有许多事情可能会出错,因此可能会引发一些不同的异常。虽然下面的异常在大多数情况下都是自动处理的,但是也可以捕捉这些异常并分别处理。

  • django.template.TemplateSyntaxError—模板代码无法验证为正确的语法,通常是由于使用了无效的标记名。当试图实例化一个Template对象时,这个问题会立即出现。
  • django.template.TemplateDoesNotExist—任何已知的模板加载程序都无法加载请求的模板。这是由本章“检索模板”一节中描述的模板加载函数发出的。
  • django.template.TemplateEncodingErrorTemplate对象无法将提供的模板字符串强制转换为 Unicode 字符串。模板字符串必须已经是 Unicode 字符串,或者是 UTF-8 编码的。任何其他编码在传递给新的Template之前都必须转换成这两种类型中的一种。当试图构造一个新的Template对象时,这个问题会立即出现。
  • django.template.VariableDoesNotExist—在当前上下文中无法解析指定的变量名。请参阅本章后面的“上下文”部分,了解有关此过程的详细信息,以及什么情况会引发此异常。
  • django.template.InvalidTemplateLibrary—模板标签为标签库注册函数之一指定了一些无效参数。发出这种错误的单个标签将导致整个标签库停止加载,并且没有标签可用于模板。这是在使用{% load %}模板标签时引发的。

整个过程

从加载器获得字符串后,必须将其从单个字符串转换为一组可以呈现的 Python 对象。这是自动发生的,大多数情况下不需要干预,但是对于大多数 Django 来说,理解这些内部机制是非常有用的。以下步骤解释了如何处理模板。所有涉及的班级都住在django.template

A new Template object accepts the raw string of the template’s contents, forming the object that will be used later.   A Lexer object also receives the raw template string, to begin processing the template contents.   Lexer.tokenize() uses a regular expression to split the template into individual components, called tokens.   These tokens populate a new Parser object.   Parser.parse() goes through the available tokens, creating nodes along the way.   For each block tag, Parser.parse() calls an external function that understands the tag’s syntax and returns a compiled Node for that tag.   The list of compiled nodes is stored on the Template object as its nodelist attribute.

完成后,留给您的是一个包含 Python 代码引用的Template对象,而不是启动该过程的原始字符串。创建节点列表后,原始字符串将被丢弃,因为这些节点包含呈现模板所需的所有功能。一旦这个过程完成,LexerParser和所有的Token对象也会被丢弃,但是在这个过程中它们会非常有用。

内容令牌

Lexer对象负责第一次遍历模板的内容,识别存在的不同组件。除了模板字符串本身,Lexer还接受一个origin,它表明模板来自哪里。这个处理由Lexer.tokenize()方法完成,该方法返回一个Token对象的列表。这可以被看作是处理模板的语法,而不是它的语义:单个组件被识别,但是它们还没有太多的意义。

令牌包含创建节点所需的所有信息,但令牌本身相对简单。它们只有两个属性:token_typecontentsToken.token_type的值将是在django.template中定义的四个常量之一,而它的contents将由它所属的令牌类型定义。

  • TOKEN_VAR—使用{{ var }}语法的变量标签是数据的占位符,在呈现模板之前不会提供。contents属性包含未解析的完整变量引用字符串。
  • TOKEN_BLOCK—块标记—通常称为“模板标记”—使用{% name %}语法,并由 Python 对象填充,该对象可以在模板渲染期间执行自定义代码。contents属性包含标签的全部内容,包括标签的名称及其所有参数。
  • TOKEN_COMMENT—注释标签使用{# comment #}语法,基本上被模板引擎忽略。作为 lexing 过程的一部分,为它们生成了令牌,但是它们的contents是空的,在这个过程的后面,它们不会成为节点。
  • TOKEN_TEXT—为模板中的所有其他内容生成文本标记,将文本存储在contents中。

在标准模板处理过程中,总是自动创建和使用一个Lexer,但也可以直接使用。这是一种检查和分析模板的有用方法,不需要完全编译它们。为了说明这一点,请考虑下面的示例,该示例将一个简单的单行模板解析为一系列标记。请注意,token_type只按值打印;将这个值与之前命名的常量进行比较要有用得多。

>>> from django.template import Lexer

>>> template = 'This is {# only #}{{ a }}{% test %}'

>>> for token in Lexer(template, 'shell').tokenize():

...     print '%s: %s' % (token.token_type, token.contents)

...

0: This is

3: only

1: a

2: test

将令牌解析为节点

一旦一个Lexer将模板字符串分割成一个令牌列表,这些令牌就被传递给一个Parser,后者会对它们进行更详细的检查。这是模板处理的语义方面,通过将相应的Node对象附加到模板上,每个标记都被赋予了意义。这些节点的复杂性差异很大;注释标记根本不产生节点,文本节点有非常简单的节点,而块标记可以有包含整个模板剩余部分的节点。

Parser对象本身比Lexer要复杂一些,因为它负责更多的进程。它的parse()方法必须遍历令牌列表,确定哪些令牌需要节点,以及在这个过程中要创建哪种类型的节点。使用Parser.next_token()从列表中检索和删除每个令牌。然后,该令牌用于确定要创建的节点类型。

对于文本和变量标记,Django 提供了用于所有实例的标准节点。分别是TextNodeVariableNode,在django.template也有。注释标记被简单地忽略,根本不生成任何节点。块标记通过模板标记库,用节点编译函数匹配标记名。

这些编译函数将在本章后面的“为模板添加特性”一节的“模板标签”部分进行描述,每个函数负责解析一个令牌的contents并返回一个Node对象。每个函数接收两个参数:Parser对象和当前令牌。通过访问Parser对象,节点编译函数可以访问一些额外的方法来帮助控制节点可以访问多少模板。

  • parse(parse_until=None)—这是第一次处理模板时调用的同一方法,也可以从节点内调用。通过为parse_until参数提供一个标记名,这个方法将只返回那些标记名之前的节点。这就是像blockiffor这样的标签如何在开始和结束标签之间环绕附加内容。请注意,这将返回完全编译的节点。
  • next_token()—从列表中检索并返回一个令牌。它还会删除该令牌,以便将来的节点不会收到任何已经处理过的令牌。请注意,这将返回一个尚未编译到节点中的令牌。
  • skip_past(endtag)—该方法类似于parse(),接受一个标记,该标记标记模板应该被处理的结束位置。主要的区别在于,skip_past()并没有将任何记号解析成节点,也没有返回任何找到的记号。它只是将模板推进到结束标记之外,忽略其间的任何内容。

模板节点

虽然这看起来像是一个复杂的概念,但是模板节点相当简单。所有模板节点都扩展了基本的Node类,位于django.template。除了一个用来定制节点行为的__init__()方法之外,节点只有几个需要包含的方法。

首先,为了在一个模板中的所有对象之间维护一个公共结构,每个模板节点都是可迭代的,产生包含在所讨论的节点中的所有节点,而不是呈现它们的内容。这提供了一种获取模板中所有节点的简单方法。

默认情况下,Node简单地产生它自己,这对于简单的模板标签来说工作得很好,这些标签只是呈现一小段文本。对于封装其他内容的更复杂的标签,这个__iter__()应该返回包含在其中的所有节点。

此外,节点还必须提供一个名为get_nodes_by_type()的方法,尽管默认方法对于大多数节点来说已经足够好了。该方法采用一个参数nodetype,即要检索的节点的类。将检查调用该方法的节点,看它是否是该类的实例,以及其中的任何其他节点。找到的确实是指定类型的实例的所有节点将在列表中返回,或者如果没有找到,将返回一个空列表。

节点上最重要的方法是render(),用来输出最终的文本。因为呈现文本需要传递给模板的数据,所以该方法接受单个参数,即一个上下文对象,如即将到来的“上下文”部分所述。

渲染模板

由于模板实际上只是一个编译指令的集合,让这些指令产生输出文本需要一个单独的步骤。可以使用简单的render()方法呈现模板,该方法将一个上下文对象作为唯一的参数。

render()方法根据编译的节点和上下文变量返回一个字符串,其中包含完全呈现的输出。这个输出通常是 HTML,但也可以是任何内容,因为 Django 模板设计用于任何基于文本的格式。渲染的大部分工作被委托给单个节点本身,模板只是遍历所有节点,依次调用每个节点上的render()

通过将这项工作卸载到每个节点本身,整个模板代码可以不那么复杂,同时也最大化了模板系统的灵活性。由于每个节点都对其行为完全负责,因此可能性几乎是无限的。

语境

模板本身主要是一堆静态内容、逻辑和数据占位符,供以后填充。如果没有数据来填补空白,它对 Web 应用来说就没什么用了。从表面上看,标准的 Python 字典似乎已经足够了,因为模板变量只是名称,可以映射到值。事实上,Django 甚至允许在某些情况下使用字典。

这种方法的一个缺点是,在某些情况下,模板标签可能需要更改一些数据,并且只在模板的特定部分保留这种更改。例如,当遍历一个列表时,列表中的每一项都应该可供其他标签使用,但是一旦循环完成,模板的其余部分就不能再访问该变量了。除此之外,如果一个循环定义了一个已经有值的变量,那么一旦循环结束执行,这个现有的值就应该被恢复。

CONTEXTS VS. NAMESPACES

在 Python 中,变量被分配给名称空间,以后可以通过名称检索它们,这使得模板上下文非常相似。还有一些显著的差异可能会引起一些混淆。

Python 允许名称空间嵌套,但只能在定义的类或函数中。在这些嵌套的名称空间中,新变量不能被封装它们的其他名称空间访问。其他类型的代码块,如条件和循环,与它们周围的任何代码共享命名空间,因此新的变量赋值在代码块完成执行后仍然存在。这样做很好,因为名称空间是基于代码编写的位置,而不是代码执行的位置,所以程序员可以很容易地确保相关名称没有任何冲突。

当编写模板标签时,没有办法知道在使用标签的模板中将定义什么变量。如果它向上下文中添加任何新的变量,这些变量很可能会覆盖模板中已经设置的其他内容。为了克服这个问题,模板提供了push()pop()方法,允许标签手动创建一个新的嵌套层次,并在完成时删除它。

这使得模板在这方面的工作方式与 Python 代码有所不同,因为像循环这样的块本质上是在执行期间创建一个新的名称空间,完成后就删除它。这些差异一开始可能会让程序员感到困惑,但是只使用模板的设计人员将只需要习惯一种行为。

为了完成所有这些,Django 将它的数据映射实现为一个特殊的Context对象,它的行为很像一个标准字典,但是有一些额外的特性。最值得注意的是,它在内部封装了一个字典列表,每个字典代表数据地图中的一个特定层。这样,它也可以像一个堆栈一样工作,能够在上面push()新值,并在不再需要时pop()去掉一层。

push()pop()都不接受任何争论。相反,它们只是在列表前面添加或删除一个字典,调整查找变量时首先使用哪个字典,如下所述。该功能在大多数情况下阻止了标准词典的使用;只要模板简单,它就能正常工作,但是一旦遇到其中一个标签,它就会引发一个AttributeError,因为它缺少这些额外的方法。

简单可变分辨率

在上下文中查找数据是最基本的操作之一,尽管在模板中引用变量时会发生很多情况。首先,当使用标准的{{ var }}语法时,Django 自动检查上下文字典,从最近添加的字典到最先添加的字典。这种查找也可以使用标准的字典查找语法在上下文本身上手动执行,这对于检索值和设置值同样有效。

如果给定的名称在最顶层的字典中不存在,上下文将退回到下一个字典,再次检查该名称,然后继续这个过程。通常,短语“当前上下文”用于描述在任何特定时间点模板标签可用的值。尽管在整个渲染过程中模板将使用相同的上下文对象,但是在任何给定点的当前上下文将根据使用的标签和这些标签检索的值而变化。

>>> from django.template.context import Context

>>> c = Context({'a': 1, 'b': 2})

>>> c['a'], c['b']

(1, 2)

>>> c.push()

>>> c['a'], c['b']

(1, 2)

>>> c['b'] = 3

>>> c['a'], c['b']

(1, 3)

>>> c.pop()

{'b': 3}

>>> c['a'], c['b]

(1, 2)

如果它遍历了所有可用的字典却没有发现任何东西,它会像标准字典一样引发一个KeyError。这个KeyError通常由 Django 直接处理,用站点设置中定义的常量值替换变量引用。默认情况下,TEMPLATE_STRING_IF_INVALID设置为空字符串,但这可能会被任何希望在这种情况下显示不同内容的站点覆盖。

复杂变量查找

除了简单的名称查找之外,变量还可以包含对某个对象的某些部分的引用,使用句点来分隔层与层。这使得一个变量节点不仅可以引用一个对象,还可以引用该对象的一个属性、一个方法调用或者字典或列表中的一个条目。这也是嵌套的,所以每次一个点解析一个新变量,另一个点就可以解析下一层深度,比如{{ request.META.host }}

这是使用一个单独的类来处理的,恰当地命名为Variable。它是用一个参数实例化的,这个字符串将被用作变量的路径,包括分隔路径各部分的任何句点。实例化后,它提供了一个方法resolve(),用于执行检索请求值的所有必要步骤。该方法采用单个参数,即应该找到变量的上下文。

如果变量是用文字值声明的,比如数字或带引号的字符串,而不是命名变量,则该值将总是直接返回,甚至不引用提供的上下文。否则,这将使用前面描述的简单查找来解析变量的第一部分。如果找到该部分,它将继续下一部分,依此类推。

链中第一步之后的每一步都基于前一步中检索到的对象。当确定在每个阶段得到什么时,resolve()经历几个不同的阶段,每个阶段的错误导致查找继续到下一个阶段。

  • 字典查找—提供的名称用作字典关键字。
  • 属性查找-名称用于标准getattr()方法。
  • 方法调用-如果属性查找检索到可调用项,如函数,则执行该可调用项时不带任何参数。如果成功,将使用返回值,但是如果函数需要任何参数,将跳过该函数。此外,如果函数的alters_data属性设置为True,作为安全预防措施,该函数将被跳过。
  • 列表索引查找-如果可能,变量名被强制为整数,并用作索引查找来查看值是否出现在列表中。

>>> from django.template import Variable

>>> c = Context({'var': [1, 2, {'spam': u'eggs'}]})

>>> var = Variable('var')

>>> zero = Variable('var.0')

>>> one = Variable('var.1')

>>> spam = Variable('var.2.spam')

>>> var.resolve(c)

[1, 2, {'spam': u'eggs'}]

>>> zero.resolve(c)

1

>>> one.resolve(c)

2

>>> spam.resolve(c)

u'eggs'

因为这提供了一种更健壮、功能更丰富的访问变量的方式,所以当节点需要能够从模板中访问数据时,最好使用Variable。这将确保模板作者在引用变量时有尽可能多的灵活性,即使是在自定义标签中。

包括请求的各个方面

通常有必要从传入的 HTTP 请求中包含某些属性,或者至少根据这些属性查找一些其他有用的信息,并将它们包含在模板上下文中。Django 没有办法神奇地将请求从视图传入模板系统,所以必须手工传递。

因为Context本身只接受字典作为参数,所以需要一个不同的对象来实现这一点。同样位于django.template.contextRequestContext接受一个请求对象作为它的第一个参数,而普通字典被推回到第二个参数。然后,在准备供模板使用的上下文时,可以检索请求的各个方面。

每当在 HTTP 循环中呈现模板时,最好使用RequestContext。Django 自己的通用视图一致地使用它,大多数第三方应用也可靠地使用它。不使用RequestContext可能会导致模板无法访问必要的数据,从而导致模板无法正确渲染。

对于许多网站来说,模板可能会作为自动化流程的一部分呈现,例如每夜发送账单通知电子邮件的作业。在这些情况下,没有 HTTP 请求进来,所以RequestContext是不合适的。在这些情况下,简单地使用标准的Context就足够了。

一旦用请求实例化了一个RequestContext,它必须根据请求的属性填充上下文变量。它不会随意这样做,而是运行 Django 中另一个钩子指定的代码。

正在检索模板

到目前为止,所展示的都是如何使用已经存在的模板。在现实世界中,模板必须根据特定视图的需求按需加载,因此显然还有更多工作要做。

检索模板的一个特殊要求是只能通过名称来引用它们,这样就可以从开发和生产环境之间的不同位置加载它们,而不需要更改任何视图的代码。第八章展示了如何编写自己的模板加载器,进一步增加了可用的选项。为了处理这种抽象,Django 提供了两个在检索模板时应该使用的实用函数。

django . template . loader . get _ template(模板名称)

大多数时候,一个视图只知道一个模板,所以只给出一个名称。get_template()函数采用所请求模板的名称,并返回一个完全实例化的Template对象。然后,可以根据视图的需要呈现该模板。

在后台,get_template()检查每个模板加载器是否存在给定名称的模板,然后返回找到的第一个模板。如果没有找到匹配指定名称的模板,就会引发一个TemplateDoesNotExist异常。

django . template . loader . select _ template(模板名称列表)

有时,有必要使用几个不同名称中的一个来检索模板。当应用希望在每次访问视图时提供某种默认模板,同时在某些情况下允许加载不同的模板时,通常会出现这种情况。

考虑一个房地产网站,其中的所有房产列表看起来都是一样的。自然,属性列表的视图将简单地为数据库中的每个列表使用相同的标准模板。但是,如果某个物业对其上市有特殊要求,例如额外的买家激励措施或关于紧急需要快速关闭的特殊通知,标准模板可能没有这样的地方。对于特定的列表,该信息可能还需要在页面上重新排列。

为了处理这些情况,select_template()接受一个模板名称列表,而不仅仅是一个值。对于列表中的每个名字,它调用get_template()来尝试检索它,如果失败,它简单地移动到列表中的下一个名字。这样,可以先提供一个更具体的名称——通常基于对象的 ID 或 slug——然后是一个更通用的后援。

>>> from django.template import loader

>>> t = loader.get_template('property/listing.html')

>>> t.name

'property/listing.html'>>> loader.get_template('property/listing_123.html')

Traceback (most recent call last):

...

django.template.TemplateDoesNotExist: property/listing_123.html

>>> t = loader.select_template(['property/listing_123.html'

'property/listing.html'])

>>> t.name

'property/listing.html'

在实际的应用中,包含在最具体的模板名称中的数字将由动态的东西提供,比如被请求的 URL。这样,新的属性列表将默认使用通用模板,但是定制一个单独的列表就像使用更具体的名称放入一个新模板一样简单。

加载和渲染模板的快捷方式

虽然完全控制如何加载和呈现模板当然很好,但常见的流程是只加载模板,用给定的上下文呈现它,然后访问结果字符串。这涉及到几个步骤,很容易重复,所以 Django 提供了一些方法来简化这个过程。

render_to_string(模板名称,字典=无,上下文实例=无)

生活在django.templates.loader中,这个简单的函数接受几个参数并返回一个由模板渲染产生的字符串。根据提供的名称来检索模板名称,然后通过将给定的字典传递到提供的上下文中来立即呈现模板名称。

如果没有提供字典,则使用一个空字典,而如果没有提供上下文,Django 将简单地使用一个Context。大多数情况下,使用RequestContext是最合适的,这样所有的上下文处理器都能得到应用。因为 Django 不能神奇地找到正在使用的请求,所以必须首先用请求实例化一个RequestContext,然后作为context_instance传入。

render_to_response(模板名称,字典=无,上下文实例=无,内容类型=无)

生活在django.shortcuts,这个函数的工作方式几乎和render_to_string()一样,除了它使用产生的字符串来填充一个HttpResponse对象,这将在下一章详细介绍。唯一不同的是,它接受一个可选的mimetype,这将在填充HttpResponse时使用。

为模板添加功能

也许 Django 模板最强大的特性是可以轻松地添加新特性,而不必修改框架本身。每个应用都可以提供自己的一套新功能,而不是期望网站开发者提供自己的功能。

Django 自己的模板特性可以分为两种类型,变量和标签,定制插件正好适合这两个领域。变量实际上不能添加到代码中,因为它们是由模板的上下文控制的,但是变量过滤器是应用允许变量被容易地修改的一种方式。另一方面,标签可以做任何事情,从在上下文中添加或修改变量到基于变量的分支到注入其他模板。

设置软件包

为了方便模板作者,Django 要求模板特性存在于应用中的特定包结构中。{% load %}标签使用这种结构在所有已安装的应用中定位特定的模块,而不需要复杂的配置,复杂的配置会使模板设计者的生活更加困难。

任何应用都可以通过在应用的主包中创建一个templatetags包来提供新的模板特性。这个新包可以包含任意数量的模块,每个模块包含一组相互关联的特性。例如,邮件应用可以提供格式化文本、执行基本数学运算和显示消息之间关系的功能。包的结构看起来像这样:

mail/

__init__.py

forms.py

models.py

urls.py

views.py

templatetags/

__init__.py

text.py

math.py

relationships.py

在为这个应用或者您在站点中使用的任何其他应用编写模板时,{% load %}标签使这些特性可用,接受要加载的模块的名称。这些模块可以来自您的INSTALLED_APPS设置中的任何应用。Django 首先在每个应用中寻找一个templatetags包,然后寻找在{% load %}标签中命名的模块。

{% load text math relationships %}

可变过滤器

当在模板中使用变量时,它们通常只是被当前视图传递到上下文中。有时,有必要格式化或修改其中的一些值,以满足特定页面的需要。这些类型的表示细节最好放在模板中,这样视图就可以传递原始值,而不用考虑模板会对它们做什么。

Django 在其核心发行版中提供了许多这样的过滤器,旨在处理您可能会遇到的许多最常见的情况。完整的文档可在网上获得, 1 但这里有一些最常见的过滤器:

  • capfirst—返回首字母大写的字符串
  • length—返回给定序列中的项目数
  • date—使用字符串作为参数来格式化日期

过滤器只是 Python 函数,它将变量值作为输入,并将修改后的值作为返回值返回。这听起来很简单,但是仍然有很大的灵活性。下面是一个简单的过滤函数,显示变量的前几个字符,用作{{ var_name|first:3 }}

from django.template import Library

from django.template.defaultfilters import stringfilter

register = Library()

@register.filter

@stringfilter

def first(value, count=1):

"""

Returns the first portion of a string, according to the count provided.

"""

return value[:count]

接受一个价值

第一个参数是变量的值,并且总是被传入,所以它应该总是过滤器函数所需要的。这个值通常是传递到模板上下文中的变量,但是过滤器可以链接在一起,所以这个值实际上可能是另一个过滤器已经执行的结果。因此,过滤器应该尽可能通用,接受广泛的输入并尽可能优雅地处理它。

Tip

这种“接受自由”的概念长期以来被认为是可互操作系统的最佳实践。早在 1980 年,在今天的网络所基于的技术形成期间,它就已经被记载了。与之相对应的是“发送的内容要保守”,在这种情况下,建议过滤器应该总是返回相同的数据类型。

因为该值可能包含来自视图提供的任何数据,或者来自任何先前过滤器的结果,所以在对其类型进行假设时应该小心。它通常是一个字符串,但也可以是一个数字、模型实例或任何数量的其他本地 Python 类型。

大多数过滤器都是为处理字符串而设计的,所以 Django 也提供了一个处理字符串的快捷方式。不能保证输入是一个字符串,所以基于字符串的过滤器总是需要在继续之前将值强制转换成一个字符串。这个流程已经有一个装饰器,叫做stringfilter,位于django.template.defaultfilters。这会自动将传入的值强制转换为字符串,因此过滤器本身不必这样做。

确保不直接对该对象进行更改也很重要。如果输入是一个可变的对象,比如一个列表或一个字典,那么在过滤器中所做的任何更改也将反映在模板中该变量的任何未来使用中。如果要进行任何更改,例如添加前缀或重新组织项目,必须先制作一份副本,这样这些更改才会反映在过滤器本身中。

接受一个论点

除了接收变量本身,过滤器还可以接受一个参数来定制它的用法。接受一个参数所需的唯一改变是在函数上定义一个额外的参数。使参数可选也很容易,只需在函数定义中提供默认值。

像变量本身一样,它也可以是任何类型,因为它可以被指定为一个文本,也可以通过另一个变量提供。没有提供任何装饰器来将该值强制转换为字符串,因为数字作为过滤器参数非常常见。无论您的过滤器期望什么参数,只要确保将它显式地强制为它需要的类型,并且总是捕捉在强制过程中可能发生的任何异常。

返回值

大多数情况下,过滤器应该返回一个字符串,因为它应该被发送到呈现模板的输出中。返回其他类型数据的过滤器也有一定的用途,例如对列表中的数字进行平均,以数字的形式返回结果的过滤器,但是这些过滤器很少使用,应该很好地记录下来,以防其他过滤器与它们链接在一起,以避免意外的结果。

更重要的是,过滤器应该总是返回值。如果在过滤器的处理过程中出现任何问题,都不应该引发异常。这也意味着如果过滤器调用其他可能引发异常的函数,那么异常应该由过滤器来处理,这样它就不会引发更多的异常。如果出现这些问题,过滤器应该返回原始输入或一个空字符串;使用哪一种取决于所讨论的过滤器的目的。

注册为过滤器

一旦函数编写完成,它就通过使用在django.template提供的Library类注册到 Django。一旦实例化,Library有一个可以用作装饰器的filter()方法,当应用于过滤函数时,它会自动向 Django 注册。这就是代码方面所需要的全部内容。

默认情况下,这并没有使它对所有模板都可用,而是告诉 Django 应用提供了它。任何想要使用它的模板仍然必须使用{% load %}标签加载应用的模板特性。

模板标签

过滤器有一个非常有用和实用的目的,但是由于它们最多只能接收两个值——变量和参数——并且只能返回一个值,所以很容易看出应用可以多快超越它们。获得更多的功能需要使用模板标签,它允许任何事情。

像过滤器一样,Django 在其核心发行版中提供了许多标记,这些标记在网上有文档记录。这里列出了一些比较常见的标签,并简要描述了它们的功能。

  • for—允许模板循环遍历序列中的项目
  • filter—将模板过滤器(如前所述)应用于标签中包含的所有内容
  • now—打印当前时间,使用一些可选参数对其进行格式化

模板标签被实现为一个函数和一个Node类的配对,前者配置后者。该节点就像前面描述的节点一样,表示模板标记的编译结构。另一方面,函数用于接受标记的各种允许的语法选项,并相应地实例化节点。

简单的标签

最简单的标记形式只存在于自身中,通常是根据一些参数将附加内容注入页面。这种情况下的节点非常简单,只需获取和存储这些参数,并在呈现期间将它们格式化为一个字符串。如果将上一节中的过滤器实现为一个标记,看起来会是这样。在模板中,这将类似于{% first var_name 3 %}

from django.template import Library, Node, Variable

register = Library()

class FirstNode(Node):

def __init__(self, var, count):

self.var = var

self.count = count

def render(self, context):

value = self.var.resolve(context)

return value[:self.count]

另一方面,编译它的函数要复杂一些。与过滤函数不同,标记函数总是接受两个参数:模板解析器对象和表示标记中包含的文本的标记。由编译函数从这两个对象中提取必要的信息。

对于这样一个简单的标记,不需要担心解析器对象,但是如果指定了参数,令牌对于获取参数仍然是必要的。

关于标记最重要的事情是split_contents()方法,它智能地将标记的声明分解成单独的组件,包括标记的名称和参数。

它可以正确地处理变量引用、带引号的字符串和数字,尽管它不做任何变量解析,并将引号放在带引号的字符串周围。

为了从我们的模板标签中获取两个必要的信息位,使用token.split_contents()从声明的字符串中提取它们。然后,可以将它们强制转换为正确的类型,并用于实例化前面描述的节点。

@register.tag

def first(parser, token):

var, count = token.split_contents()[1:]

return FirstNode(Variable(var), int(count))

简单标签的快捷方式

谢天谢地,有一条捷径可以让这个过程变得简单很多。Library对象包含另一个装饰器方法simple_tag(),它处理类似这样的简单情况。在幕后,它处理参数的解析和解析,甚至节点类的创建,所以留给模板标记的只是一个看起来非常类似于变量过滤器的函数。

from django.template import Library

register = Library()

@register.simple_tag

def first(value, count):

return value[:count]

这仍然是有限的用途,但有许多这样的情况下,一个简单的标签是必要的,快捷方式可以成为一个相当省时。对于更高级的需求,手动创建节点可以提供更大的能力和灵活性。

向所有模板添加要素

默认情况下,Django 不会自动加载所有应用的模板过滤器和标签;相反,它只对所有模板使用默认设置。对于那些需要访问特定应用的模板特性并且使用{% load %}标签开销太大的模板,还有一个选项可以将应用添加到所有模板的默认标签集中。

同样在django.template中,add_to_builtins()函数默认包含应用的名称。具体来说,这是该应用的app_label,如第三章所述。一旦一个应用被提供给这个函数,它的所有标签和过滤器将对所有模板可用。这可以在应用加载时执行的任何地方调用,比如它的__init__.py模块。

这应该谨慎使用,因为对于那些不使用该应用任何特性的模板来说,它确实会带来一些额外的开销。然而,在某些情况下,有必要覆盖默认过滤器或标签的行为,而add_to_builtins()提供了这个选项。请记住,不止一个应用可以做到这一点,所以仍然不能保证将使用哪个应用版本的特定功能。Django 将在遇到它们时简单地覆盖它们,所以最后加载的应用将被使用。小心使用。

应用技术

Django 模板旨在让那些必须定期编写模板的人尽可能地简单。先进的技术被用来强化这一思想,简化了在模板中执行起来过于复杂的任务。应用通常有自己独特的模板需求,应该提供标签和过滤器来满足这些需求。更好的是,如果需要的话,提供可以被其他应用重用的特性。

嵌入另一个模板引擎

虽然 Django 的模板引擎适用于大多数常见的情况,但它的局限性可能会在需要更强大或更灵活的情况下造成挫折。模板标签扩展了 Django 的功能,但只是通过让程序员为每个单独的需求编写它们。

另一个具有不同设计理念的模板引擎可能更适合这些需求。通过允许模板设计者为模板的部分切换到替代引擎,无需额外编程就可以展示额外的功能。标签仍然可以简化常见的任务,但是切换模板引擎可能是支持极限情况的一种简单方法。

一个这样的替代模板引擎是 Jinja, 2 ,它具有与 Django 相当类似的语法。这两种设计理念存在根本差异,这使得 Jinja 成为输出需要复杂条件和逻辑的情况下的更好选择。这些方面使它成为嵌入 Django 模板的完美候选。

为了说明这一点,考虑一个需要计算复合值以在模板中显示的模板。这个特性在 Django 中是不可用的,所以它通常需要一个定制的模板标签或者一个视图,在将值发送到模板之前计算它的值。

{% load jinja %}

{% for property in object_list %}

Address: {{ property.address }}

Internal area: {{ property.square_feet }} square feet

Lot size: {{ property.lot_width }}' by {{ property.lot_depth }}'

{% jinja %}

Lot area: {{ property.lot_width * property.lot_depth / 43560 }} acres

{% endjinja %}

{% endfor %}

Django 将自动处理直到jinja标记的所有内容,将所有剩余的标记和Parser对象一起传递给 Jinja 编译函数。解析器和令牌可以用来提取写在jinjaendjinja标签之间的内容。然后,在传递给 Jinja 进行渲染之前,需要将其转换回字符串内容。

将令牌转换为字符串

在研究完整的编译函数之前,首先要注意令牌必须转换回字符串,以便 Jinja 处理它们。Jinja 为其模板使用了非常相似的语法,所以 Django 的Lexer准确地识别了变量、块和注释标签。尽管 Jinja 也为这些标签创建了标记,但是来自两个模板引擎的标记彼此不兼容,所以它们必须转换回字符串。Jinja 可以像处理任何来源的模板一样处理它们。

为了完成这种转换,节点编译函数将依赖一个单独的函数,该函数接受一个令牌并返回一个字符串。它的工作原理是,django.template也包含这些标签的开始和结束部分的常量。有了这些信息和记号的结构,就可以从给定的记号创建合适的字符串。

from django import template

def string_from_token(token):

"""

Converts a lexer token back into a string for use with Jinja.

"""

if token.token_type == template.TOKEN_TEXT:

return token.contents

elif token.token_type == template.TOKEN_VAR:

return '%s %s %s' % (

template.VARIABLE_TAG_START

token.contents

template.VARIABLE_TAG_END

)

elif token.token_type == template.TOKEN_BLOCK:

return '%s %s %s' % (

template.BLOCK_TAG_START

token.contents

template.BLOCK_TAG_END

)

elif token.token_type == template.TOKEN_COMMENT:

return u'' # Django doesn't store the content of comments

这不会产生原始模板字符串的精确副本。在 Django 的Lexer处理过程中,一些空白被删除,注释的内容完全丢失。标签的所有功能都被保留下来,所以模板仍然可以正常工作,但是要知道这种技术可能会产生一些小的格式问题。

编译到节点

有了为jinja块中的标记再现字符串的函数,下一步是生成一个Node,它将用于呈现内容以及模板的其余部分。当收集开始标签和结束标签之间的内容时,编译函数通常使用Parser.parse()方法,传入结束标签的名称,这将返回表示内部内容的Node对象列表。

由于 Jinja 标签不能使用 Django 的节点函数进行处理,Parser.parse()会由于不正确的语法而导致问题。相反,Jinja 编译函数必须直接访问令牌,然后可以将令牌转换回字符串。没有提供完全做到这一点的函数,但是将Parser.next_token()与一些额外的逻辑结合起来会工作得很好。

编译函数可以遍历可用的令牌,每次都调用Parser.next_token()。这个循环将一直执行,直到找到一个endjinja块标记或者模板中不再有标记。一旦从解析器获得了令牌,就可以将它转换成一个字符串,并添加到一个内部模板字符串中,该字符串可以用来填充一个JinjaNode

import jinja2

from django import template

from django.base import TemplateSyntaxError

register = template.Library()

def jinja(parser, token):

"""

Define a block that gets rendered by Jinja, rather than Django's templates.

"""

bits = token.contents.split()

if len(bits) != 1:

raise TemplateSyntaxError("'%s' tag doesn't take any arguments." % bits[0])

# Manually collect tokens for the tag's content, so Django's template

# parser doesn't try to make sense of it.

contents = []

while 1:

try:

token = parser.next_token()

except IndexError:

# Reached the end of the template without finding the end tag

raise TemplateSyntaxError("'endjinja' tag is required.")

if token.token_type == template.TOKEN_BLOCK and \

token.contents == 'endjinja':

break

contents.append(string_from_token(token))

contents = ''.join(contents)

return JinjaNode(jinja2.Template(contents))

jinja = register.tag(jinja)

Caution

不使用解析器的parse()方法,就不能在{% jinja %}标签中使用任何其他 Django 标签。这在这里不是问题,因为内容是由 Jinja 处理的,但是在没有充分理由的情况下使用这种技术会导致其他类型的标签出现问题。

准备金贾模板

一旦编译函数从 Django 模板标记中检索到 Jinja 模板内容,就会创建一个JinjaNode来访问该模板。Jinja 提供了自己的Template对象,将内容编译成有形对象,因此在创建JinjaNode时使用它是有意义的。

然后,到了渲染JinjaNode的时候,只需要渲染编译好的 Jinja 模板,并将输出返回给 Django 的模板。这个任务比表面上看起来更棘手,因为 Django 的Context对象包含应该传递给 Jinja 的变量,其行为不完全像 Python 字典。它们支持访问键的通用字典式语法,但是在内部,它们的结构与 Jinja 所期望的完全不同。

为了将嵌套的Context对象正确地传递给 Jinja 模板,必须首先将其展平为一个标准的 Python 字典。这很容易做到,只需遍历存储在上下文中的各个字典,并将它们分配给一个新字典,保持 Django 本身使用的优先级:某个键的第一次出现优先于该键的任何其他实例。只有当一个键在新的 Jinja 上下文字典中不存在时,才应该添加它,这样在这个过程中就不会覆盖现有的值。

一旦字典可用,数据就可以传递给 Jinja 自己的Template.render()方法。该方法的结果是可以从JinjaNode.render()返回的正确呈现的内容,将该内容放置在页面中。

import jinja2

class JinjaNode(template.Node):

def __init__(self, template):

self.template = template

def render(self, django_context):

# Jinja can't use Django's Context objects, so we have to

# flatten it out to a single dictionary before using it.

jinja_context = {}

for layer in django_context:

for key, value in layer.items():

if key not in jinja_context:

jinja_context[key] = value

return self.template.render(jinja_context)

启用用户提交的主题

在本章的前面,我们发现模板可以从任何来源加载,只要有一个合适的加载器知道如何检索它们。这种方法的一个缺点是它只对每个人加载模板有效;没有办法将模板与特定用户相关联。

无论如何,这都不是真正的失败,因为大多数应用都需要它完全按照自己的方式工作。此外,用户信息只有在收到请求时才可用,因此无法以通用的方式访问它。每个工具都有它的使用时间,当然也有将模板绑定到用户身上的时候。

考虑这样一个网站,它鼓励用户定制他们自己的体验,提供他们登录时使用的定制主题。这给了用户很大的控制权,让他们可以更好地参与到网站中,并能让他们更深入地体验网站。如果他们有机会将自己的自定义主题提供给其他人使用,这一点还可以进一步增强。这种想法并不适用于所有的网站,但是对于大量面向社区的网站,尤其是艺术界的网站,它可以极大地提升用户体验。

A WORD ABOUT ADVERTISING

今天,网络上的许多站点至少部分是由放置在它们的各种页面上的广告资助的。这种广告只有在真正展示给用户时才会起作用,因此他们有机会点击广告并购买产品或服务。通过向网站引入用户可编辑的主题,用户有一个绝佳的机会删除网站可能依赖的任何广告,所以仔细考虑这是否适合你的网站是很重要的。

网站工作人员批准供网站普通观众使用的任何主题都可以首先进行检查,以确保它们不会对网站上的广告或网站自身的品牌造成任何伤害。这是在过程中至少实施一些质量控制的好方法。问题在于,用户可以在提交主题以供审批之前,按照自己喜欢的方式创建主题,并可以通过网站自行使用,从自己的体验中删除广告。

将这个问题的影响最小化的一个方法是提供付费的网站会员资格,其中一个好处是能够创建自定义主题。通过这种方式,免费用户将永远把广告视为资助他们使用网站的一种方式,而付费用户则通过年费来抵消他们缺乏广告的影响。

事实上,如果你的网站采用这种模式,最好是完全删除付费用户的广告,不管他们用的是什么主题。没有人喜欢为使用一个网站付费,只是为了给同一个网站带来更多的收入。

从表面上看,这似乎是层叠样式表(CSS)的完美工作。CSS 完全是关于网站的表现,但是它总是受到页面内容排序的限制。例如,文档中位置较高的标记很难放在页面的底部,反之亦然。通过允许用户编辑决定这些位置的模板,很容易打开更多的可能性。

使用 Django 模板带来了一些必须克服的技术和安全挑战,解决这些挑战揭示了许多使用模板的有趣方法。首先考虑需要解决的问题。

  • 如果要编辑模板,它们应该存储在数据库中。
  • 模板需要绑定到一个特定的用户,以限制他们编辑任何东西,并且当主题得到推广时,还需要将功劳分配给适当的作者。
  • 用户无法使用 Django 模板语言的全部内容。这是一个安全风险,会向任何人暴露太多的信息。
  • 主题在提供给所有人使用之前,必须得到工作人员的批准。
  • 一旦主题被提交以供批准,并且在它们被批准之后,用户应该不能进行任何修改。
  • 用户的个人主题——无论是个人创作的还是从他人作品中挑选的——都应该在网站的所有部分使用。
  • 除了模板本身,每个主题都应该有一个与之相关的 CSS 文件,以更好地设计网站的其他方面。

这是一个需要涵盖的相当多的东西的列表,并且单个站点可能有更多的需求。这并不像表面上看起来那么糟糕,因为 Django 已经准备了很多东西来使这些问题容易解决。

建立模型

首要任务是为模板在数据库中的存储腾出空间。在标准的 Django 方式中,这是通过一个模型来完成的,这个模型带有表示模板各种属性的字段。对于这个应用,主题由一些不同的信息组成:

  • 用作模板内容的文本块
  • CSS 文件的 URL
  • 创建它的用户
  • 一个标题,这样其他用户可以很容易地引用它,如果它对每个人都可用的话
  • 指示它是否是站点范围的默认设置,以便尚未选择主题的用户仍然可以使用

这些信息中的大部分只会被主题对象本身使用,因为只有主要的文本块会被传递到模板中。很容易把主题想象成一个独立的模板,它同时是存储在数据库中的一组数据和用于呈现 HTML 的一组指令。Python 提供了一种明确这一概念的方式,并提供了一种处理主题的简单方法。

通过使用多重继承,主题有可能既是模型又是模板,以手头任务所需的任何方式表现。该类继承自django.db.models.Modeldjango.template.Template,并且__init__()被覆盖以分别初始化两端:

from django.db import models

from django import template

from django.contrib.auth.models import User

from themes.managers import ThemeManager

class Theme(models.Model, template.Template):

EDITING, PENDING, APPROVED = range(3)

STATUS_CHOICES = (

(EDITING, u'Editing')

(PENDING, u'Pending Approval')

(APPROVED, u'Approved')

)

author = models.ForeignKey(User, related_name='authored_themes')

title = models.CharField(max_length=255)

template_string = models.TextField()

css = models.URLField(null=True, blank=True)

status = models.SmallIntegerField(choices=STATUS_CHOICES, default=EDITING)

is_default = models.BooleanField()

objects = ThemeManager()

def __init__(self, *args, **kwargs):

# super() won't work here, because the two __init__()

# method signatures accept different sets of arguments

models.Model.__init__(self, *args, **kwargs)

template.Template.__init__(self, self.template_string

origin=repr(self), name=unicode(self))

def save(self):

if self.is_default:

# Since only one theme can be the site-wide default, any new model that

# is defined as default must remove the default setting from any other

# theme before committing to the database.

self.objects.all().update(is_default=False)

super(Theme, self).save()

def __unicode__(self):

return self.title

这足以将主题本身存储在数据库中,但仍然没有涵盖用户在浏览网站时如何选择主题。通常,这将被设置为引用Theme的模型上的ForeignKey,但是由于User模型在我们的控制之外,所以需要做一些其他的事情。

存储以用户为中心的信息(如首选项)的一种方法是添加自定义用户模型。Django 的官方文档 3 详细介绍了这一点,但基本思想是你可以提供自己的模型来代替 Django 自己的用户模型使用。您的自定义模型可以包含任何与用户相关的附加字段,包括选定的主题。然而,一个站点只能有一个自定义用户模型,仅仅为了支持主题而劫持这个特性是没有意义的。相反,我们可以使用一个ManyToManyField将它连接到User模型。

class Theme(models.Model):

...  # All the other fields shown above

users = models.ManyToManyField(User, through='SelectedTheme')

...  # All the other methods shown above

class SelectedTheme(models.Model):

user = models.OneToOneField(User)

theme = models.ForeignKey(Theme)

通过使用一个OneToOneField,我们可以确保每个用户在中间表中只出现一次。这样,每个用户只能有一个选定的主题。还有一些实用函数可以帮助管理这种行为。实际上,这里有两种不同的方法会很有用,都是基于用户来获取主题的。一个用于检索用户选择的主题,另一个用于检索用户创建的主题。

from django.db import models

from django.conf import settings

class ThemeManager(models.Manager):

def by_author(self, user):

"""

A convenience method for retrieving the themes a user has authored.

Since the only time we'll be retrieving themes by author is when

they're being edited, this also limits the query to those themes

that haven't yet been submitted for review.

"""

return self.filter(author=self, status=self.model.EDITING)

def get_current_theme(self, user):

return SelectedTheme.objects.get(user=user).theme

有了这个管理器,就可以很容易地检索特定用户的主题,包括用户可以编辑的主题和用户在浏览站点时应该使用的主题。拥有这些快捷方式有助于使视图更简单,让他们专注于他们真正要做的事情。一个站点范围的主题的全部意义在于它被用于每一个视图,所以很明显需要做一些其他的事情来适应它。

支持站点范围的主题

单个视图已经有足够多的事情要操心了,不应该负责管理主题。相反,需要一种方法来检索用户选择的主题——或者默认主题——并将其自动应用于视图使用的任何模板。理想情况下,所有这些都应该在不对视图进行任何更改的情况下发生,因此需要做的额外工作很少。

这是最适合上下文处理器的工作,这是本章前面描述的概念。通过使用上下文处理器,每个使用RequestContext的视图将自动访问正确的主题。这使得总是使用RequestContext这个通常很好的建议变成了一个绝对的要求。正如我们将在下一节看到的,模板将明确依赖于可用的主题,而不使用RequestContext将违反这一假设。

这个过程所需的上下文处理器相当简单,但是它必须提供一些特定的特性。它必须确定当前用户是否登录,识别用户选择的主题,如果没有选择主题或者用户没有登录,则返回默认主题,并且它必须返回正确的主题,以便可以将它添加到模板的上下文中。这些代码将被放在一个名为context_processors.py的模块中,与 Django 内部使用的约定保持一致。

from django.conf import settings

from themes.models import Theme

def theme(request):

if hasattr(request, 'user') and request.user.is_authenticated():

# A valid user is logged in, so use the manager method

theme = Theme.objects.get_current_theme(request.user)

else:

# The user isn't logged in, so fall back to the default

theme = Theme.objects.get(is_default=True)

name = getattr(settings, 'THEME_CONTEXT_NAME', 'theme')

return {name: theme}

注意测试中使用了hasattr()来查看用户是否登录。这看起来可能没有必要,但是通过在测试中添加这个简单的条件,它允许在没有中间件需求的情况下使用这个上下文处理器。否则,它总是需要django.contrib.auth.middleware.AuthenticationMiddleware,将user属性放在请求上。如果没有使用这个中间件,每个用户将会收到默认的主题。

另外,请注意上下文变量的名称是由另一个新设置驱动的,这次称为THEME_CONTEXT_NAME。这默认为'theme',因此没有必要显式提供名称,除非这会导致与其他特性冲突。这是一个反复出现的主题(双关语),因为对于一个必须与自身之外的大量事物交互的应用,比如用户模型和模板上下文,确保将冲突保持在最低限度是很重要的。

有了这个文件,剩下的唯一事情就是将'themes.context_processors.theme'添加到TEMPLATE_CONTEXT_PROCESSORS设置中,以确保它被应用到所有的模板中。一旦主题对模板可用,仍然需要确保模板可以访问和使用它。

设置模板以使用主题

主题的最终目标是对页面的组件进行重新排序,所以确定什么是“组件”是很重要的。就 Django 模板而言,这意味着一个标记块,由{% block %}模板标记标识。页面的每个组件可以在单独的块中定义,将每个位分隔到自己的空间中。

使用 Django 的模板继承,可以在一个模板中定义块,用另一个模板的内容填充这些块。这样,特定于页面的模板可以定义每个块中的内容,而基本模板可以指定这些块呈现的位置,以及它们周围放置的其他标记。这将是对页面的重要部分进行重新排序的一个很好的方法,只要有一种方法可以动态地指定基本模板放置所有块的位置。

Django 通过{% extends %}标签支持模板继承,该标签使用一个参数来标识要扩展的基本模板。通常,这是用作基础的模板的硬编码名称。它还可以接受一个上下文变量,该变量包含一个用作基本模板的字符串。如果该上下文变量指向一个模板实例,Django 将使用它,而不是费力地在其他地方查找模板。

在模板中利用这一点很容易;把{% extends theme %}放在模板的顶部就可以了。如果你已经为你的站点明确地指定了一个THEME_CONTEXT_NAME,确保将theme更改为你为该设置输入的任何值。这仍然只是一部分。仍然需要获取模板来利用主题中定义的块。

没有通用的方法可以做到这一点,因为每个站点都有自己的模板继承设置,以及自己的一组每个页面都需要填充的模块。通常情况下,这些块将用于页面标题、导航、页面内容和页脚,但不同的网站可能有不同的需求。

此外,一个站点可能有更多的块不能被重新排列,而是被定义在其他块的内部。目前我们不会考虑这些,因为主题只与可以移动的方块有关。考虑一个具有以下可定制模块的应用:

  • logo—网站的标志,作为图像
  • title—当前页面的标题
  • search—搜索框,可能带有高级选项
  • navigation—用于浏览网站的链接或其他界面的集合
  • sidebar—与当前页面相关的一点内容
  • content—当前页面的主体,无论是产品列表、新闻稿、搜索结果还是联系表单
  • 版权免责声明,以及一些职位空缺、投资者关系和联系信息的链接

每个主题都必须定义所有这些块,以确保整个网站得到显示,所以明确地概述它们是很重要的。网站上的每个模板都需要定义要放入这些块中的内容,以便总是有东西放在正确的位置。这些块中有许多并没有指定给任何特定的页面,所以模板继承在这里也起到了拯救作用。

通过在主题和单个页面之间放置另一个模板层,可以为所有页面自动填充一些块,而将其他块留给单个页面填充。单个页面模板仍然具有最终的权威,如果需要,可以用新内容覆盖任何块。这就留下了确保模板确实定义了站点继承方案所需的所有块的问题。

验证和保护主题

任何时候一个网站接受用户的输入,它必须被仔细检查,以确保它满足一定的要求,并保持在可接受的范围内。主题也不例外,但是用户可编辑的模板也代表了一个非常真实的安全风险。Django 采取措施确保模板不能执行任何修改数据库的常用函数,但是模板可以做很多其他的事情。

默认情况下,只有 Django 自己的数据修改方法通过使用alters_data属性从模板中得到保护。任何应用的模型都可能定义对数据库进行更改的其他方法,如果这些方法没有用alters_data标记,它们就可以在模板中使用。即使是只读访问,如果不加以控制,也会成为一个问题。每个页面都使用一个主题,许多页面将通过模型关系访问大量对象。

有太多的方法可以访问应该保持隐私的东西,以至于没有黑名单方法可以指望是完整的。相反,白名单方法是必要的,其中主题只允许使用 Django 的模板系统提供的一小部分功能。诀窍是确定解决这类问题的正确方法。

从表面上看,正则表达式似乎是可行的。毕竟,Django 本身使用一个正则表达式来解析模板并将它们分解成节点,所以编写一个更有限的表达式来保护模板肯定是微不足道的。目前可能是这样,但是请记住 Django 正在不断改进,未来可能会给模板带来新的语法。

不管这种可能性有多大,如果真的发生了,无论我们如何精心制作正则表达式,都无法预测将来会包含什么新语法。任何逃过这一保护的东西都有可能损害网站或泄露机密信息。模板语法保持不变的希望太大了。

相反,我们将依赖 Django 自己的正则表达式将模板编译成一个节点列表,就像平常一样。然后,一旦它被编译成一个nodelist,就很容易看到这些节点,以确保它们都在做正确的事情。利用这一点,表单可以很容易地验证模板是否定义了所有正确的块。主题模板必须:

  • THEME_EXTENDS设置引用的模板继承。
  • 提供一个块,其名称由THEME_CONTAINER_BLOCK设置引用。
  • THEME_BLOCKS设置中引用的所有块填充该块。
  • 在任何THEME_BLOCKS块中不提供内容。
  • 除了在THEME_BLOCKS设置中提到的那些,不要提供其他模块。
  • 不包含任何其他标签,只有文本。

from django import forms

from django import template

from django.template.loader_tags import BlockNode, ExtendsNode

from django.conf import settings

from theme import models

class ThemeForm(forms.ModelForm):

title = forms.CharField()

body = forms.CharField(widget=forms.Textarea)

def clean_body(self):

try:

tpl = template.Template(self.cleaned_data['body'])

except template.TemplateSyntaxError as e:

# The template is invalid, which is an input error.

raise forms.ValidationError(unicode(e))

if [type(n) for n in tpl.nodelist] != [ExtendsNode] or \

tpl.nodelist[0].parent_name != settings.THEME_EXTENDS:

# No 'extends' tag was found

error_msg = u"Template must extend '%s'" % settings.THEME_EXTENDS

raise forms.ValidationError(error_msg)

if [type(n) for n in tpl.nodelist[0].nodelist] != [BlockNode] or \

tpl.nodelist[0].nodelist[0].name != settings.THEME_CONTAINER_BLOCK:

# Didn't find exactly one block tag with the required name

error_msg = u"Theme needs exactly one '%s' block" % \

settings.THEME_CONTAINER_BLOCK

raise forms.ValidationError(error_msg)

required_blocks = list(settings.THEME_BLOCKS[:])

for node in tpl.nodelist[0].nodelist[0].nodelist:

if type(node) is BlockNode:

if node.name not in required_blocks:

error_msg = u"'%s' is not valid for themes." % node.name)

raise forms.ValidationError(error_msg)

required_blocks.remove(node.name)

if node.nodelist:

error_msg = u"'%s' block must be empty." % node.name)

raise forms.ValidationError(error_msg)

elif type(node) is template.TextNode:

# Text nodes between blocks are acceptable.

pass

else:

# All other tags, including variables, are invalid.

error_msg = u"Only 'extends', 'block' and plain text are allowed."

raise forms.ValidationError(error_msg)

if required_blocks:

# Some blocks were missing from the template.

blocks = ', '.join(map(repr, required_blocks))

error_msg =  u"The following blocks must be defined: %s" % blocks

raise forms.ValidationError(error_msg)

class Meta:

model = models.Theme

主题示例

即使有了应用,也可能很难理解如何编写一个主题来与站点协同工作。考虑一个使用此themes应用的站点,具有以下设置:

THEME_EXTENDS = 'base.html'

THEME_CONTEXT_NAME = 'theme'

THEME_CONTAINER_BLOCK = 'theme'

THEME_BLOCKS = (

'title'

'sidebar'

'links'

)

位于继承链根的base.html模板可能如下所示:

<html>

<head>

<title>{% block title %}{% endblock %}</title>

<link rel="stylesheet" type="text/css" href="/style.css"/>

</head>

<body>{% block theme %}{% endblock %}</body>

</html>

然后可以编写一个主题来满足应用的需求:从base.html开始扩展,提供一个theme块并用空的titlesidebarlinks块填充它。与其他模板不同,这段代码将作为Theme模型的一个实例存储在数据库中。

{% extends 'base.html' %}

{% block theme %}

<h1>{% block title %}{% endblock %}</h1>

<ul id="links">{% block links %}{% endblock %}</ul>

<div id="content">{% block content %}{% endblock %}</div>

{% endblock %}

现在,可以为网站的其余部分编写单独的模板,从theme变量扩展并填充titlesidebarlinks块。考虑一个房地产网站的根的模板:

{% extends theme %}

{% block title %}Acme Real Estate{% endblock %}

{% block links %}

<li><a href="{% url home_page %}">Home</a></li>

<li><a href="{% url property_list %}">Properties</a></li>

<li><a href="{% url about_page %}">About</a></li>

{% endblock %}

{% block content %}

<p>Welcome to Acme Real Estate!</p>

{% endblock %}

有了所有这些模板,加载站点的根目录将产生一个完整的 HTML 文档,如下所示:

<html>

<head>

<title>Acme Real Estate</title>

<link rel="stylesheet" type="text/css" href="/style.css"/>

</head>

<body>

<h1>Acme Real Estate</h1>

<ul id="links">

<li><a href="/">Home</a></li>

<li><a href="/properties/">Properties</a></li>

<li><a href="/about/">About</a></li>

</ul>

<div id="content">

<p>Welcome to Acme Real Estate!</p>

</div>

</body>

</html>

现在怎么办?

视图和模板相结合来决定什么内容应该发送给用户,但是它仍然必须到达浏览器。Django 能流利地讲 HTTP,所以有很多方法可以定制这个旅程。

Footnotes 1

http://prodjango.com/tags/

2

http://prodjango.com/jinja/

3

http://prodjango.com/custom-user/

七、处理 HTTP

Abstract

超文本传输协议(HTTP)是网络通信的基本语言。Web 服务器和 Web 浏览器都在使用它,还有各种处理 Web 的专业工具。

超文本传输协议(HTTP)是网络通信的基本语言。Web 服务器和 Web 浏览器都在使用它,还有各种处理 Web 的专业工具。

Python 社区已经做了大量的工作来标准化与 HTTP 交互的应用的行为,最终产生了 PEP-333,1Web 服务器网关接口(WSGI)。因为 Django 遵循 WSGI 规范,所以本章列出的许多细节都是遵循 PEP-333 的直接结果。

请求和回应

因为 HTTP 是一种无状态协议,其核心是请求和响应的概念。客户端向服务器发出一个请求,服务器返回一个包含客户端请求的信息的响应,或者返回一个错误,指出请求无法实现的原因。

虽然请求和响应遵循详细的规范,但是 Django 提供了一对 Python 对象,旨在使协议更容易在您自己的代码中处理。协议的基本工作知识是有用的,但是大多数细节是在幕后处理的。本节描述了这些对象,以及说明应参考的规范相关部分的注释。

对象

正如在第四章中所描述的,每个 Django 视图都接收一个对象作为它的第一个参数,这个对象表示传入的 HTTP 请求。这个对象是HttpRequest类的一个实例,它封装了关于请求的各种细节,以及一些用于执行有用功能的实用方法。

基本的HttpRequest类存在于django.http中,但是单个的服务器连接器将定义一个子类,该子类具有特定于所使用的 Web 服务器的附加属性或被覆盖的方法。任何被覆盖的方法或属性的行为应该与这里记录的一样,任何附加信息最好记录在服务器接口本身的代码中。

HttpRequest.method

HTTP 规范概述了各种可用于描述正在执行的请求类型的动词。这通常被称为 its 方法,不同的请求方法对如何处理它们有特定的期望。在 Django 中,用于请求的方法被表示为HttpRequest对象的method属性。它将作为标准字符串包含在内,方法名全部用大写字母。

每种方法都描述了服务器应该如何处理 URL 所标识的资源。大多数 Web 应用只实现 GET 和 POST,但是其他一些应用也值得在这里解释一下。关于这些以及这里没有列出的其他内容的更多细节可以在 HTTP 规范、 2 以及网络上的许多其他资源中找到。

  • 删除—请求删除资源。Web 浏览器不实现这个方法,所以它的使用仅限于 Web 服务应用。在典型的 Web 浏览器应用中,这样的操作是通过 POST 请求完成的,因为 GET 请求不允许有副作用,比如删除资源。
  • 获取—检索由 URL 指定的资源。到目前为止,这是 Web 上最常见的请求类型,因为每个标准的 Web 页面检索都是通过 GET 请求完成的。正如在“安全方法”一节中提到的,GET 请求被认为对服务器没有副作用;他们应该检索指定的资源,不做任何其他事情。
  • HEAD—检索有关资源的一些信息,但不获取全部内容。具体来说,对 HEAD 请求的响应应该返回与 GET 请求完全相同的头,只是响应体中没有任何内容。Web 浏览器不实现这种方法,但是由于服务器端操作本质上只是一个没有响应体的 GET 请求,所以很少会被遗漏。在 Web 服务应用中,HEAD 请求可以是一种低带宽的方式来检索有关资源的信息,如资源是否存在、上次更新时间或内容大小。
  • POST—请求以某种与 URL 指定的资源相关的方式存储附加数据。这可能意味着对博客帖子或新闻文章的评论,对问题的回答,对基于网络的电子邮件的回复或任何其他相关情况。这个定义只在 Web 服务环境中有效,在 Web 服务环境中可以区分 PUT 和 POST。在标准的 Web 浏览器中,只有 GET 和 POST 是可靠可用的,所以 POST 用于任何修改服务器上信息的情况。使用 POST 从表单提交数据只不过是官方 HTTP 规范中的一个脚注,但却是这种方法最常见的用法。
  • PUT—请求将附加数据存储在 URL 指定的资源中。这可以被视为“创建”或“替换”操作,具体取决于资源是否已经存在。不过,这种方法传统上在 Web 浏览器中不可用,所以它的使用仅限于 Web 服务应用。在标准的 Web 浏览器中,PUT 指定的操作是通过 POST 请求来完成的。
“安全”方法

正如前面提到的,各种类型的 HTTP 请求之间有一个重要的区别。该规范将 GET 和 HEAD 称为“安全”方法,它们只检索由 URL 指定的资源,而不对服务器做任何更改。明确地说,处理 GET 或 HEAD 请求的视图不应该做任何修改,除了那些与检索页面相关的修改。

安全方法的目标是允许在不同的时间多次发出相同的请求,而不会产生任何负面影响。这种假设允许书签和浏览器历史使用 GET 请求,而不会在多次发出请求时警告用户。允许更改的一个示例是更新指示页面被查看次数的计数。

“等幂”方法

除了安全方法之外,HTTP 规范还将 PUT 和 DELETE 描述为“幂等的”,这意味着,即使它们旨在对服务器进行更改,这些更改也足够可靠,以至于多次调用具有相同主体的相同请求将总是进行相同的更改。

在 PUT 的情况下,资源将在第一次执行请求时创建,并且每个后续请求将简单地用最初提交的相同数据替换资源,从而使其保持不变。对于 DELETE,最初删除资源后的每个后续请求都会导致一个错误,表明资源不存在,因此每次都保持资源的状态不变。另一方面,POST 需要对每个请求进行修改或添加。为了表示这种情况,当 POST 请求执行多次时,Web 浏览器会显示一条消息,警告用户后续请求可能会导致问题。

HttpRequest.path

该属性包含所请求的完整路径,没有附加任何查询字符串参数。这可以用来识别被请求的资源,而不依赖于哪个视图将被调用或者它将如何表现。

访问提交的数据

任何时候请求进来,都可能伴随着 Web 浏览器提供的各种数据。处理这些信息是使网站具有动态性和交互性的关键,因此 Django 使之变得简单而灵活。正如有许多方法可以向 Web 服务器提交数据一样,一旦数据到达,也有许多方法可以访问这些数据。

大多数浏览器发送的使用标准查询字符串格式 3 的数据被自动解析成一种特殊类型的字典类QueryDict。这是MultiValueDict的一个不可变子类,这意味着它的功能很像一个字典,但是增加了一些选项来处理字典中每个键的多个值。

QueryDict最重要的细节是它是用来自传入请求的查询字符串实例化的。有关如何访问QueryDict中的值的详细信息,请参见第九章中的MultiValueDict的详细信息。

HttpRequest。得到

如果请求是通过 GET 方法传入的,那么它的GET属性将是一个QueryDict,包含 URL 的查询字符串部分中包含的所有值。当然,虽然对于什么时候可以使用GET从 URL 中获取参数没有技术限制,但是干净 URL 的目标限制了它最有利的情况。

特别是,将标识资源的参数与自定义资源检索方式的参数分开是非常重要的。这是一个微妙但重要的区别。考虑下面的例子:

  • /book/pro-django/chapter07/
  • /news/2008/jun/15/website-launched/
  • /report/2008/expenses/?ordering=category

正如您所看到的,发送到 GET 请求视图的大部分数据应该放在 URL 本身,而不是查询字符串中。这将有助于搜索引擎更有效地索引它们,同时也使用户更容易记住它们并与他人交流。与许多其他原则一样,这不是一条绝对的规则,所以在工具箱中保留查询字符串和GET属性,但是要小心使用它们。

HttpRequest。邮政

如果请求是使用标准 HTML 表单的 PUT 或 POST 方法,这将是一个包含表单提交的所有值的QueryDict。无论编码类型如何,有无文件,所有标准表单都将填充POST属性。

然而,HTTP 规范允许这些请求以任何格式提供数据,因此如果传入的数据不符合查询字符串的格式,HttpRequest.POST将为空,数据将必须通过HttpRequest.raw_post_data直接读入。

HttpRequest。文件

如果传入的 PUT 或 POST 请求包含任何上传的文件,这些文件将存储在FILES属性中,该属性也是一个QueryDict,每个值是一个django.core.files.uploadedfile.UploadedFile对象。这是稍后在第九章中描述的File对象的子类,提供了一些特定于上传文件的额外属性。

  • content_type—与文件相关联的Content-Type,如果提供的话。Web 浏览器通常根据文件名的最后一部分进行分配,尽管 Web 服务调用可以根据内容的实际类型更准确地指定这一点。
  • charset—为上传文件内容指定的字符集。
HttpRequest.raw_post_data

每当请求中包含数据时,就像 PUT 和 POST 一样,raw_post_data属性提供对这些内容的访问,而不需要任何解析。对于大多数网站来说,这通常是不必要的,因为GETPOST属性更适合最常见的请求类型。Web 服务可以接受任何格式的数据,许多使用 XML 作为数据传输的主要手段。

HttpRequest.META

当请求进来时,有大量与请求相关的信息没有出现在查询字符串中,并且在请求的GETPOST属性中不可用。相反,关于请求来自哪里以及如何到达服务器的数据存储在请求的META属性中。META中可用值的详细信息可在 PEP-333 中找到。

此外,每一个请求都伴随着许多头,这些头描述了客户机想要知道的各种选项。HTTP 规范中明确规定了这些类型的头可以包含什么, 4 但是它们通常控制诸如首选语言、允许的内容类型和关于 Web 浏览器的信息之类的事情。

这些头文件也存储在META中,但是其格式与最初略有不同。所有的 HTTP 头名称都变成大写,以HTTP_为前缀,所有的破折号都用下划线代替。

  • Host变成了HTTP_HOST
  • Referer变成了HTTP_REFERER
  • X-Forwarded-For变成了HTTP_X_FORWARDED_FOR

HttpRequest。饼干

因为每个 HTTP 请求都是客户机和服务器之间的一个新连接,所以 cookies 被用作识别发出多个请求的客户机的一种方式。简而言之,cookies 只不过是向 Web 浏览器发送名称和相关值的一种方式,浏览器在每次向网站发出新请求时都会将名称和相关值发送回来。

虽然 cookie 是在流程的响应阶段设置的,如HttpResponse所述,但是从传入请求中读取 cookie 的任务非常简单。请求的COOKIES属性是一个标准的 Python 字典,将 cookies 的名称映射到之前发送的值。

请记住,该字典将包含浏览器发送的所有 cookies 的条目,即使它们是由同一服务器上的另一个应用设置的。本章后面的HttpResponse部分介绍了浏览器如何决定随特定请求发送哪些 cookies 以及如何控制该行为的具体规则。

HttpRequest.get_signed_cookie(密钥[,…])

如果您存储在 cookie 中的信息在被篡改时可能会被用来对付您,您可以选择对您的 cookie 进行签名,并在读取 cookie 时验证这些签名。签名本身是使用本章稍后描述的HttpResponse.set_signed_cookie()方法提供的,但是当在请求中读取它们时,您将需要使用这个方法。

您可以使用几个附加参数来控制 cookie 检索的行为:

  • default=RAISE_ERROR—此参数允许您指定在请求的密钥未找到或无效时应返回的默认值。这相当于向标准字典的get()方法传递一个默认值。如果您不提供一个值,这个方法将在密钥丢失时产生一个标准的KeyError,或者在签名无效时产生一个标准的django.core.signing.BadSignature
  • salt=''—这是对set_signed_cookie()方法中相同参数的补充。它允许您在应用的不同方面使用同一个密钥,也许是在多个域上,而没有签名在不同用途之间重复使用的风险。这必须与您在设置 cookie 时提供的值相匹配,以便签名检查匹配。
  • max_age=None—默认情况下,cookie 签名也有一个与之关联的过期时间,以避免它们被重复使用超过预期时间。如果您提供一个超过给定 cookie 年龄的max_age,您将得到一个django.core.signing.SignatureExpired异常。默认情况下,这不会在验证签名时检查到期日期。

HttpRequest.get_host()

许多服务器配置允许单个 Web 应用响应发送到多个不同域名的请求。为了帮助解决这些情况,传入请求的get_host()方法允许视图识别 Web 浏览器用来访问网站的名称。

除了用于发出请求的主机名之外,如果服务器被配置为在非标准端口上响应,则从该方法返回的值将包括端口号。

HttpRequest.get_full_path()

除了主机信息,get_full_path()方法还返回 URL 的完整路径部分;协议和域信息之后的所有内容。这包括用于确定使用哪个视图的完整路径,以及提供的任何查询字符串。

http request . build _ absolute _ uri(location = None)

此方法为所提供的位置(如果有)生成一个绝对 URL。如果没有显式提供位置,则返回请求的当前 URL,包括查询字符串。如果提供了位置,方法的确切行为取决于传入的值。

  • 如果该值包含完全限定的 URL(包括协议),则该 URL 已经是绝对的,并按提供的方式返回。
  • 如果该值以正斜杠(/)开头,它将被附加到当前 URL 的协议和域信息中,然后返回。这将为所提供的路径生成一个绝对 URL,而不必对服务器信息进行硬编码。
  • 否则,该值被假定为相对于请求的当前 URL 的路径,并且这两者将使用 Python 的urlparse.urljoin()实用函数连接在一起。

HttpRequest.is_secure()

如果请求使用安全套接字层(SSL)协议,这个简单的方法返回True,如果请求不安全,则返回False

HttpRequest.is_ajax()

对于“Web 2.0”站点很有用,如果请求有一个值为“XMLHttpRequest”的X-Requested-With头,这个方法返回True。大多数设计用来调用服务器的 JavaScript 库都会提供这个头,提供了一种方便的方法来识别它们。

HttpRequest.encoding

这是一个简单的属性,表示在访问前面描述的GETPOST属性时使用的编码。如果设置了一个编码,那么这些字典中的值将被强制转换为使用这种编码的unicode对象。默认情况下,它的值是None,访问值时将使用默认编码utf-8

在大多数情况下,该属性可以保持不变,大多数输入都使用默认编码进行正确转换。特定的应用可能有不同的需求,所以如果应用需要不同编码的输入,只需将该属性设置为能够正确解码这些值的值。

HttpResponse

在请求被接收和处理之后,每个视图负责返回一个响应——一个HttpResponse的实例。这个对象清晰地映射到实际的 HTTP 响应,包括头,并且是控制发送回 Web 浏览器的内容的唯一方法。像请求的表亲一样,HttpResponse住在django.http,但是有几个快捷方式可以更容易地创建响应。

创建响应

与请求不同,视图的作者可以完全控制如何创建响应,允许多种选择。标准的HttpResponse类的实例化相当简单,但是接受三个参数来定制它的行为。这些都不是必需的;本节稍后描述的选项可以用其他方式设置这些值。

  • content—接受文本或其他内容作为请求的主体。
  • status—设置请求发送的 HTTP 状态码 5
  • content_type—控制与请求一起发送的Content-Type报头。如果提供了这个,确保它在适当的时候也包含了charset值。

>>> from django.http import HttpResponse

>>> print HttpResponse()

Content-Type: text/html; charset=utf-8

>>> print HttpResponse(content_type='application/xml; charset=utf-8')

Content-Type: application/xml; charset=utf-8

>>> print HttpResponse('content')

Content-Type: text/html; charset=utf-8

content

还有一个mimetype参数,用于向后兼容旧的 Django 应用,但是应该使用content_type。不过,记住mimetype仍然很重要,因为这意味着statuscontent_type应该被指定为关键字参数,如果提供的话。

对标题的字典访问

一旦创建了响应,就很容易使用标准的字典语法定制将与其内容一起发送出去的头。这非常简单,正如您所期望的那样。与标准字典唯一值得注意的不同是,所有的键比较都不区分大小写。

>>> from django.http import HttpResponse

>>> response = HttpResponse('test content')

>>> response['Content-Type']

'text/html; charset=utf-8'

>>> response['Content-Length']

Traceback (most recent call last):

...

KeyError: 'content-length'

>>> response['Content-Length'] = 12

>>> for name, value in response.items():

...     print '%s is set to %r' % (name, value)

...

Content-Length is set to '12'

Content-Type is set to 'text/html; charset=utf-8'

对内容的类似文件的访问

除了在创建响应对象时将正文内容指定为字符串的能力之外,许多知道如何编写打开文件的第三方库也可以创建内容。Django 的HttpResponse实现了一些文件协议方法——最著名的是write()——这使得它可以被许多这样的库视为只写文件。当使用 Django 在视图中动态生成二进制内容(如 PDF 文件)时,这种技术尤其有用。

关于对响应体的类文件访问,需要注意的一件重要事情是,并不是所有的文件协议方法都实现了。这意味着某些库,比如 Python 自己的zipfile.ZipFile类,需要这些额外的方法,将会失败,并带有一个AttributeError,指示哪个方法丢失了。这是故意的,因为 HTTP 响应不是真正的文件,所以没有可预测的方法来实现这些方法。

http 响应.状态 _ 代码

此属性包含代表发送到客户端的响应类型的数字状态代码。如前所述,这可以在实例化响应对象时立即设置,但作为一个标准的对象属性,它也可以在响应创建后的任何时候设置。

这应该只设置为已知的 HTTP 响应状态代码。有关有效状态代码的详细信息,请参见 HTTP 规范。这种状态可以在实例化响应时设置,但也可以设置为子类的类属性,Django 就是这样配置它的许多专门化响应的。

HttpResponse.set_cookie(key,value=''[,…])

当希望跨多个请求存储值时,cookies 是首选工具,它通过特殊的头将值传递给 Web 浏览器,然后在后续请求时将值发送回服务器。通过用一个键和一个值调用set_cookie(),发送到客户端的 HTTP 响应将包含一个单独的头,告诉浏览器存储什么以及何时将其发送回服务器。

除了键和值之外,set_cookie()还可以接受一些额外的参数,用于配置浏览器何时应该将 cookie 发送回服务器。虽然追求可读性建议使用关键字来指定这些参数,但是这个列表使用它们的位置顺序。在 HTTP 状态管理的官方规范中可以找到关于每个选项允许的值的更多细节。 6

  • max_age=None—对应于规范中的max-age选项,指定 cookie 应该保持活动的秒数。
  • 不是所有的浏览器都像官方规范要求的那样接受和尊重max-age,而是遵循 Netscape 制定的早期模式。expires属性获取 cookie 到期的确切日期,而不是以秒为单位的偏移量。指定日期的格式如下:Sun, 15-Jun-2008 12:34:56 GMT
  • path='/'—指定浏览器将此 cookie 发送回服务器的基本路径。也就是说,如果被请求的 URL 的路径以此处指定的值开始,浏览器将随请求一起发送 cookie 的值。
  • domain=None—类似于path,它指定了 cookie 将被发送到的域。如果保留为None,cookie 将被限制到发布它的同一个域,而提供一个值将允许更大的灵活性。
  • secure=False—如果设置为True,则表示 cookie 包含敏感信息,只能通过安全连接(如 SSL)发送到服务器。

>>> response = HttpResponse()

>>> response.set_cookie('a', '1')

>>> response.set_cookie('b', '2', max_age=3600)

>>> response.set_cookie('c', '3', path='/test/', secure=True)

>>> print response.cookies

Set-Cookie: a=1; Path=/

Set-Cookie: b=2; Max-Age=3600; Path=/

Set-Cookie: c=3; Path=/test/; secure

请记住,只有在响应通过网络后,才会在浏览器中设置 cookie。这意味着在浏览器的下一次请求之前,cookie 的值在请求对象上不可用。

COOKIES AND SECURITY

尽管 cookies 是跨多个 HTTP 请求维护状态的非常有用的方法,但是它们存储在用户的计算机上,有知识的用户可以访问它们并修改它们的内容。Cookies 本身是不安全的,不应用于存储敏感数据或控制用户如何访问网站的数据。

解决这个问题的典型方法是只在 cookie 中存储一个引用,它可以用来从服务器上的某个地方检索“真正的”数据,比如用户无权访问的数据库或文件。本章末尾的“应用技术”一节提供了一种在 cookies 中安全存储数据的替代方法,这样他们的数据实际上是可信的。

HttpResponse.delete_cookie(key,path='/',domain=None)

如果一个 cookie 已经被发送到 Web 浏览器,并且不再需要或者已经无效,那么可以使用delete_cookie()方法来指示浏览器删除它。如上所述,这里提供的路径和域必须与现有的 cookie 相匹配,以便正确删除它。

它通过设置一个新的 cookie 来做到这一点,将max-age设置为0,将expires设置为Thu, 01-Jan-1970 00:00:00 GMT。这会导致浏览器覆盖任何匹配相同keypathdomain的现有 cookie,然后立即使其过期。

httpresponse . cookies

除了能够在响应阶段显式设置和删除 cookie 之外,您还可以查看将被发送到 Web 浏览器的 cookie。cookies属性使用 Python 的标准Cookie模块, 7 ,属性本身是一个SimpleCookie对象,其行为很像一个字典,每个值都是一个Morsel对象。

使用 cookie 的名称作为键,可以检索代表特定 cookie 值的Morsel,以及相关的选项。这个对象可以用作字典来引用这些附加选项,而它的value属性包含为 cookie 设置的值。使用这个字典甚至可以访问已删除的 cookie,因为这个过程涉及到设置一个将立即过期的新 cookie。

>>> len(response.cookies)

3

>>> for name, cookie in response.cookies.items():

...     print '%s: %s (path: %s)' % (name, cookie.value, cookie['path'])

...

a: 1 (path: /)

b: 2 (path: /test/)

c: 3 (path: /)

httpresponse . set _ signed _ cookie(key,value,salt=''[,…])

这就像set_cookie()一样工作,除了它在将值发送到浏览器之前也加密签名。因为 cookie 存储在浏览器中,这确保了用户在再次访问您的站点之前不会修改这些 cookie 中的值。您仍然不想在 cookie 中存储敏感信息,但这允许您放心地在 cookie 中存储登录用户名等信息,而用户无法将其用作攻击媒介。

它采用与set_cookie()相同的参数,只增加了一个参数:salt。默认情况下,Django 使用您的settings.SECRET_KEY来生成签名,这在大多数情况下是没问题的,因为带有特定键的 cookie 只可能用于一个目的。在其他情况下,salt参数允许您为您当前拥有的任何用途制作一个签名。

例如,如果您使用一个 Django 安装提供多个域,您可以使用域名作为签名的 salt,这样用户就不能在不同的域上重用一个域的签名。不同的 salts 确保签名是不同的,因此当在您的视图中检索 cookie 时,复制的签名将无法通过签名测试。

HttpResponse.content

此属性提供对响应正文的字符串内容的访问。这可以被读取或写入,并且在中间件处理的响应阶段特别有用。

专业回应对象

由于有几种常见的 HTTP 状态代码,Django 提供了一组定制的HttpResponse子类,它们的status_code属性已经相应地设置好了。和HttpResponse本身一样,这些都住在django.http。其中一些采用了与标准HttpResponse不同的一组参数,这些差异也在这里列出。

  • HttpResponseRedirect—采用单个参数,即浏览器将重定向到的 URL。它还将status_code设置为 302,指示资源所在的“发现”状态。
  • HttpResponsePermanentRedirect—采用单个参数,即浏览器将重定向到的 URL。它将status_code设置为 301,表示资源被永久地移动到指定的 URL。
  • HttpResponseNotModified—将status_code设置为 304,表示“未修改”状态,当响应没有改变与请求相关的条件时,用于响应条件 GET。
  • HttpResponseBadRequest—将status_code设置为 400,表示视图无法理解请求中使用的语法的“错误请求”。
  • HttpResponseForbidden—将status_code设置为 403,“禁止”,其中请求的资源确实存在,但是请求用户没有访问它的权限。
  • HttpResponseNotFound—可能是所有定制类中最常见的,它将status_code设置为 404,“未找到”,其中请求中的 URL 没有映射到已知资源。
  • HttpResponseNotAllowed—将status_code设置为 405,“不允许”,表示请求中使用的方法对于 URL 指定的资源无效。
  • HttpResponseGone—将status_code设置为 410,“消失”,表示由 URL 指定的资源不再可用,并且不能在任何其他 URL 上找到。
  • HttpResponseServerError—将status_code设置为 500,“服务器错误”,每当视图遇到不可恢复的错误时使用。

Web 浏览器不支持其中的一些专用响应,但是它们对于 Web 服务应用都非常有用,在 Web 服务应用中有更广泛的选项可用。在站点范围内设置这些状态通常更有意义,因此单个视图不必担心直接管理它们。为此,Django 提供了 HTTP 中间件。

编写 HTTP 中间件

Django 自己创建一个HttpRequest,每个视图负责创建一个HttpResponse,应用通常需要对每个传入请求或传出响应执行特定的任务。流程的这一部分称为中间件,是将高级处理注入流程的有用方式。

中间件处理的常见示例包括压缩响应内容、拒绝访问某些类型的请求或来自某些主机的请求,以及记录请求及其相关响应。虽然这些任务可以在单独的视图中完成,但是这样做不仅需要大量的样板文件,还需要每个视图了解将要应用的中间件的每一部分。

这也意味着添加或删除 HTTP 处理需要触及整个项目中的每一个视图。这不仅是一个维护问题,而且如果您的项目使用任何第三方应用,还会导致额外的维护问题。毕竟,更改第三方代码会限制您在将来升级代码时避免不必要的麻烦。Django 通过在请求/响应周期的独立部分执行中间件操作来解决这些问题。

每个中间件都只是一个 Python 类,它至少定义了以下方法之一。这门课没有其他要求;也就是说,它不必继承任何提供的基类,包含任何特定的属性或以任何特定的方式实例化。只要在一个重要的位置提供这个类,站点就能够激活它。

中间件可以在四个不同的点上与 Django 的 HTTP 处理挂钩,执行它需要的任何任务。通过在中间件类上指定一个方法,可以简单地控制流程的每个部分。记住,这只是 Python,所以任何有效的 Python 在中间件中也是有效的。

middleware class . process _ request(self,request)

一旦传入的 HTTP 请求变成了一个HttpRequest对象,中间件就有机会改变事情的处理方式。这个钩子甚至在 Django 分析 URL 以决定使用哪个视图之前就出现了。

作为标准的 Python,process_request()方法可以执行任何任务,但是常见的任务包括禁止访问某些客户端或请求类型,为上下文处理器使用的请求添加属性,或者基于请求的细节返回之前缓存的响应。

这个方法可以更改请求的任何属性,但是要记住,任何更改都会影响 Django 在流程的其余部分处理请求的方式。例如,因为这个方法在 URL 解析之前被调用,它可以修改request.path来将请求重定向到一个完全不同的视图。虽然像这样的行为通常是所期望的,但它可能是一个意想不到的副作用,所以在修改请求时要小心。

middleware class . process _ view(self,request,view,args,kwargs)

该方法在 URL 被映射到视图并从中提取参数之后,但在实际调用视图之前被调用。除了请求之外,传递给此方法的参数如下:

  • view—将被调用的视图功能。这是实际的函数对象,而不是名称,无论视图是使用字符串还是可调用的。
  • args—包含将传递给视图的位置参数的元组。
  • kwargs—包含将传递给视图的关键字参数的字典。

既然已经从 URL 中提取了视图的参数,就有可能根据配置应该获得的内容来验证这些参数。这在开发过程中非常有用,可以用来验证一切都配置正确。简单地建立一个中间件类来打印出argskwargs变量以及request.path。然后,如果一个视图出现任何问题,开发服务器的控制台将有一个便捷的方法来识别或排除潜在的问题。

这似乎是对即将执行的视图进行详细记录的绝佳机会,因为视图函数对象也是可用的。虽然这是真的,但是在视图上装饰器的普遍使用使事情变得复杂了。具体来说,传递给这个方法的视图函数通常是由装饰器创建的包装函数,而不是视图本身。

这意味着第二章中详述的内省特性不能可靠地用于将位置参数与它们在函数定义中的名称对齐。尽管如此,仍然有一些好处,因为只要装饰者使用在第九章的中描述的特殊的wraps装饰者,你仍然能够访问视图的模块和名称。

class ArgumentLogMiddleware(object):

def process_view(request, view, args, kwargs):

print 'Calling %s.%s' % (view.__module__, view.__name__)

print 'Arguments: %s' % (kwargs or (args,))

middleware class . process _ response(自身、请求、响应)

视图执行后,新的响应对象可供中间件查看并进行必要的修改。在这里,中间件可以缓存响应以备将来使用,压缩响应体以加快网络传输,或者修改将与响应一起发送的头和内容。

它接收原始请求对象以及视图返回的响应对象。此时,请求对于 HTTP 循环已经没有任何用处了,但是如果使用它的一些属性来决定如何处理响应,那么它还是很有用的。在被方法返回之前,响应对象可以在这个阶段被修改,而且经常被修改。

process_response()方法应该总是返回一个HttpResponse对象,不管之前对它做了什么。大多数情况下,这将是最初给出的响应,只是有一些小的修改。有时,返回一个完全不同的响应可能更有意义,比如重定向到一个不同的 URL。

middleware class . process _ exception(自身,请求,异常)

如果在请求处理过程的任何部分出错,包括中间件方法,通常会抛出一个异常。这些异常中的大多数将被发送到process_exception()以记录或以特殊方式处理。传递给该方法的异常参数是抛出的异常对象,它可用于检索有关出错的具体细节。

这个阶段的一个常见任务是以特定于当前使用的站点的方式记录异常。异常的字符串表示及其类型通常就足够了,尽管它的确切用途将取决于引发的异常。通过将原始请求的细节与异常的细节结合起来,您可以生成有用且可读的日志。

在中间件和视图装饰器之间做出选择

第四章展示了视图如何使用装饰器在视图执行之前或之后执行额外的工作,热心的读者会注意到中间件可以执行类似的功能。视图装饰者可以访问传入的请求以及视图生成的响应。他们甚至可以访问视图函数和将要传递给它的参数,并且可以将视图包装在一个try块中,以处理任何引发的异常。

那么是什么使它们不同,什么时候应该使用其中一个而不是另一个呢?这是一个相当主观的话题,没有一个答案可以满足所有情况。每种方法都有优点和缺点,这应该有助于您决定对于特定的应用采用哪种方法。

范围上的差异

中间件和视图装饰器之间最显著的区别之一是覆盖了多少站点。中间件在一个站点的settings.py中被激活,因此它覆盖了任何 URL 上的所有请求。这个简单的事实提供了一些优势:

  • 许多操作——比如缓存或压缩——对于站点上的每个请求都应该自然发生;中间件使得这些任务很容易实现。
  • 未来对站点的添加会自动被现有的中间件覆盖,而不必为它们提供的行为做任何特殊的让步。
  • 第三方应用不需要任何修改就可以利用中间件的行为。

另一方面,decorator 应用于单独的函数,这意味着每个视图都必须手动添加 decorator。这使得 decorator 的管理更加耗时,但是一些操作——比如访问限制或专门的缓存需求——更适合于站点的有限部分,在那里 decorator 可以发挥巨大的作用。

配置选项

中间件类被引用为包含类的导入路径的字符串,这不允许任何直接的方式来配置它们的任何特性。大多数接受选项中间件都是通过特定于该中间件的自定义设置来接受选项的。这确实提供了一种定制中间件工作方式的方法,但是就像中间件本身一样,根据定义,这些设置是站点范围的。没有为个人视图定制它们的空间。

如第二章所示,装饰器可以被编写成在应用于一个函数时接受配置选项,视图装饰器也不例外。每个视图可以有一组单独的选项,或者用一组预配置的参数用curry创建一个全新的装饰器。

使用中间件作为装饰者

考虑到中间件和装饰器之间的相似性,Django 提供了一个实用程序来将现有的中间件类转换成装饰器。这使得代码可以在整个站点中重用,在任何情况下都可以使用最好的工具。

django.utils.decorators中,特殊的decorator_from_middleware()函数将一个应该应用于单个视图的中间件类作为唯一的参数。返回值是一个完美的函数修饰器,可以应用于任意数量的视图。

允许配置选项

由于装饰者可以接受选项来配置他们的行为,我们需要一种方法让中间件类利用这种灵活性。在中间件类上提供一个接受额外参数的__init__()方法将允许从头开始编写一个类,既可以用作中间件,也可以用作视图装饰器。

需要记住的一点是,中间件最常见的调用是不带任何参数的,所以您定义的任何附加参数都必须使用默认值。如果不这样做,那么无论何时当它被用作标准中间件时,都会导致一个TypeError,并且单独使用decorator_from_middleware(),它不接受任何参数。

class MinimumResponseMiddleware(object):

"""

Makes sure a response is at least a certain size

"""

def __init__(self, min_length=1024):

self.min_length = min_length

def process_response(self, request, response):

"""

Pads the response content to be at least as

long as the length specified in __init__()

"""

response.content = response.content.ljust(self.min_length)

当用作中间件时,该类将填充所有响应,长度至少为 1,024 个字符。为了让单个视图获得这个最小长度的具体值,我们可以转而使用decorator_from_middleware_with_args()。它将在修饰视图时接受参数,并将这些参数传递给中间件类的__init__()方法。

另外,请注意,如果一个中间件类已经被定义为中间件和装饰器,那么任何使用装饰器的视图实际上都将为每个请求调用中间件两次。对于某些应用,比如在请求对象上设置属性的应用,这不是问题。对于其他人,尤其是那些修改外发响应的人,这可能会带来很多麻烦。

HTTP 相关信号

因为请求不受任何应用代码的控制,所以使用信号来通知应用代码所有请求/响应周期的开始和完成。像所有的信号一样,这些只是简单的Signal对象,它们存在于django.core.signals。关于信号、信号如何工作以及如何使用信号的更多信息,参见第九章。

django . core . signals . request _ started

每当从外部收到一个请求时,这个信号被触发,没有任何附加参数。它在进程的早期触发,甚至在HttpRequest对象被创建之前。如果没有任何参数,它的用途是有限的,但它确实提供了一种方法,在任何中间件有机会访问请求对象之前,在收到请求时通知应用。

这种方法的一个潜在用途是为其他信号注册新的侦听器,它应该只在通过 HTTP 传入的请求期间运行。这与其他信号可能由于一些非 HTTP 事件而被触发的情况形成对比,例如一个调度的作业或一个命令行应用。

django . core . signals . request _ finished

一旦视图生成了响应并且中间件得到了处理,这个信号就在将响应发送回发送原始请求的客户机之前触发。像request_started一样,它没有向监听器提供任何参数,所以它的使用相当有限,但是它可以作为一种方法来断开在request_started触发时连接的任何监听器。

django . core . signals . got _ request _ exception

如果在处理请求的过程中出现异常,但没有在其他地方显式处理,Django 会触发只有一个参数的got_request_exception信号:正在处理的请求对象。

这与中间件的process_exception()方法不同,后者只在视图执行过程中发生错误时触发。许多其他异常也会触发这个信号,比如 URL 解析或任何其他中间件方法中的问题。

应用技术

通过在协议处理中提供如此多的钩子,Django 使得为应用修改 HTTP 流量的各种选项成为可能。在这个领域,每个应用都有自己的需求,这取决于它接收的流量类型和它期望提供的接口类型。因此,下面的例子更多地解释了如何挂接 Django 的 HTTP 处理,而不是详尽地列出定制这种行为可以做些什么。

自动签署 Cookies

Django 对签名 cookie 的支持很方便,但是它要求您调用不同的方法来设置和检索 cookie,以确保签名被正确地应用和验证。您不能简单地访问请求上的cookies属性而不失去签名的安全性优势。然而,通过使用定制的中间件,完全可以做到这一点:使用通常为未签名的 cookies 保留的简单访问方法,自动添加和验证签名。

概括地说,这个中间件将负责几项任务:

  • 在传出请求中签署 cookies
  • 验证和删除传入请求的 cookies
  • 管理这些签名的 salt 和过期选项

前两个任务可以相当简单地完成,通过检查请求和响应来寻找 cookie,并调用 cookie 方法的有符号变体来管理签名。让我们从设置响应 cookies 开始。

签名传出响应 Cookies

中间件可以从process_response()方法开始,该方法需要找到视图设置的任何 cookies,并将签名添加到它们的值中。

class SignedCookiesMiddleware(object):

def process_response(self, request, response):

for (key, morsel) in response.cookies.items():

response.set_signed_cookie(key, morsel.value

max_age=morsel['max-age']

expires=morsel['expires']

path=morsel['path']

domain=morsel['domain']

secure=morsel['secure']

)

return response

这种方法在设置新的 cookie 时使用原始 cookie 的所有属性,因此除了用于设置它的方法之外,它是完全相同的。使用set_signed_cookie()将在幕后做所有适当的事情。

被删除的 cookies 也会出现在response.cookies中,尽管它们没有值,也不需要签名。这些可以通过它们的0max-age来识别,这可以用来忽略它们,只对应用重要的实际值进行签名。

class SignedCookiesMiddleware(object):

def process_response(self, request, response):

for (key, morsel) in response.cookies.items():

if morsel['max-age'] == 0:

# Deleted cookies don't need to be signed

continue

response.set_signed_cookie(key, morsel.value

max_age=morsel['max-age']

expires=morsel['expires']

path=morsel['path']

domain=morsel['domain']

secure=morsel['secure']

)

return response

正在验证传入的请求 Cookies

处理传入的请求也相当简单。process_request()方法是这部分流程的入口点,它只需找到所有传入的 cookies,并使用get_signed_cookie()来检查签名,并从值中删除这些签名。

class SignedCookiesMiddlewar

e(object):

def process_request(self, request):

for key in request.COOKIES:

request.COOKIES[key] = request.get_signed_cookie(key)

读取 cookies 比编写它们更简单,因为我们不必处理所有的单个参数;它们已经是饼干的一部分了。但是,这段代码仍然有一个问题。如果任何签名丢失、无效或过期,get_signed_cookie()将引发一个异常,我们需要以某种方式处理它。

一种选择是简单地让错误通过,希望它们会在其他代码中被捕获,但是因为您的视图和其他中间件甚至不知道这个中间件正在签署 cookies,所以它们不太可能处理签名异常。更糟糕的是,如果您没有处理这些异常的代码,它们将会一直出现在您的用户面前,通常以 HTTP 500 错误的形式出现,这根本不能解释这种情况。

相反,这个中间件可以直接处理异常。由于只有具有有效签名的值才能传递给视图,一个显而易见的方法是简单地从请求中删除所有无效的 cookies。异常会随着生成这些异常的 cookies 一起消失。您的视图将只看到有效的 cookie,正如它们所期望的那样,任何无效的 cookie 都不会再存在于请求中。用户可以随时清除他们的 cookie,所以依赖于 cookie 的视图应该总是处理丢失 cookie 的请求,所以这种方法非常适合视图已经在做的事情。

支持这种行为只需要捕捉相关的异常并删除那些引发异常的 cookies。

from django.core.signing import BadSignature, SignatureExpired

class SignedCookiesMiddleware(object):

def process_request(self, request):

for (key, signed_value) in request.COOKIES.items():

try:

request.COOKIES[key] = request.get_signed_cookie(key)

except (BadSignature, SignatureExpired):

# Invalid cookies should behave as if they were never sent

del request.COOKIES[key]

作为装饰者签署饼干

到目前为止,SignedCookiesMiddleware在设置和检索签名的 cookies 时还没有使用任何特定于签名的选项。对于打算在整个站点上使用的中间件来说,默认设置通常已经足够好了。但是,由于中间件也可以用作装饰器,所以我们还需要考虑对单个视图的定制。这就是 salt 和 expiration 设置有用的地方。

如本章前面所示,decorator_from_middleware()可以向中间件的__init__()方法提供参数,这样就为定制saltmax_age参数提供了一个途径。一旦在__init__()中接受了这些参数,各个钩子方法就可以适当地合并它们。

from django.core.signing import BadSignature, SignatureExpired

class SignedCookiesMiddleware(object):

def __init__(self, salt='', max_age=None):

self.salt = salt

self.max_age = max_age

def process_request(self, request):

for (key, signed_value) in request.COOKIES.items():

try:

request.COOKIES[key] = request.get_signed_cookie(key

salt=self.salt

max_age=self.max_age)

except (BadSignature, SignatureExpired):

# Invalid cookies should behave as if they were never sent

del request.COOKIES[key]

def process_response(self, request, response):

for (key, morsel) in response.cookies.items():

if morsel['max-age'] == 0:

# Deleted cookies don't need to be signed

continue

response.set_signed_cookie(key, morsel.value

salt=self.salt

max_age=self.max_age or morsel['max-age']

expires=morsel['expires']

path=morsel['path']

domain=morsel['domain']

secure=morsel['secure']

)

return response

现在您可以使用decorator_from_middleware_with_args()创建一个装饰器,并提供saltmax_age参数来为每个视图定制装饰器的行为。

from django.utils.decorators import decorator_from_middleware_with_args

signed_cookies = decorator_from_middleware_with_args(SignedCookiesMiddleware)

@signed_cookies(salt='foo')

def foo(request, ...):

...

现在怎么办?

请求和响应周期是 Django 应用用来与外界通信的主要接口。同样重要的是后台可用的实用程序集合,它们允许应用执行最基本的任务。

Footnotes 1

http://prodjango.com/pep-333/

2

http://prodjango.com/http-methods/

3

http://prodjango.com/query-string/

4

http://prodjango.com/http-headers/

5

http://prodjango.com/http-status-codes/

6

http://prodjango.com/cookie-spec/

7

http://prodjango.com/r/cookie-module/

八、后端协议

Abstract

作为一个框架,Django 的目的是提供一组内聚的接口,使最常见的任务变得更容易。其中一些工具完全包含在 Django 本身中,很容易保持一致性。许多其他功能是——或者至少可能是——由外部软件包提供的。

作为一个框架,Django 的目的是提供一组内聚的接口,使最常见的任务变得更容易。其中一些工具完全包含在 Django 本身中,很容易保持一致性。许多其他功能是——或者至少可能是——由外部软件包提供的。

尽管 Django 本身支持这些不同特性的一些最常见的软件包,但还有更多,尤其是在公司环境中。除了开发人员对一种类型的数据库比对另一种类型的数据库的偏好之外,许多其他服务器已经被现有的应用使用,这些应用不容易被转换以使用不同的东西。

因为这些类型的问题在现实生活中确实会出现,Django 提供了简单的方法来引用这些特性,而不用担心是什么实现在后台实际发生了这些问题。这个相同的机制还允许您用第三方代码替换掉许多这些低级功能,以支持连接到其他系统,或者只是定制某些方面的行为。

本章列出的部分有双重目的。除了记录 Django 针对这些特性的通用 API 之外,每一节还将描述如何编写新的后端来实现这些特性。这不仅包括要声明什么类和方法,还包括包结构可能是什么样子,以及拼图的每一部分应该如何表现。

数据库访问

连接到数据库是现代 Web 应用最基本的需求之一,有多种选择。目前,Django 支持一些更流行的开源数据库引擎,包括 MySQL、PostgreSQL 和 SQLite,甚至一些商业产品,如 Oracle。

考虑到不同数据库系统的独特特性和 SQL 不一致性,Django 需要在它的模型和数据库本身之间增加一个额外的层,这个层必须为使用的每个数据库引擎专门编写。每个受支持的选项在 Django 中都作为包含这个中间层的独立 Python 包提供,但是其他数据库也可以通过外部提供这个层来支持。

虽然 Python 提供了一个访问数据库的标准化 API,但每个数据库系统都以稍微不同的方式解释基本的 SQL 语法,并在此基础上支持不同的特性集,因此这一节将重点关注 Django 提供的与模型访问数据库的方式挂钩的领域。这就把在每种情况下制定正确查询的本质细节留给了读者。

Django、db、backends

这引用了后端包的base模块,从这里可以访问整个数据库。以这种方式访问数据库后端确保了统一、一致的界面,而不管后台使用的是哪个数据库包。

Django 做了大量的工作来使这种级别的访问变得不必要,但是在不使事情过于复杂的情况下,它只能做到这一步。当 ORM 无法提供一些必要的功能时——例如,在纯 SQL 中根据另一列的值更新一列——总是可以直接找到源代码,查看真正发生了什么,并调整标准行为或完全替换它。

因为这实际上只是一个后端特定模块的别名,所以本章列出的完整导入路径只有在试图以这种方式访问数据库时才有效。当实现一个新的后端时,包路径将特定于该后端。例如,如果一个用于连接 IBM 的 DB2 2 的后端被放在一个名为db2的包中,这个模块实际上应该位于db2/base.py

数据库包装器

数据库后端的主要特性之一是DatabaseWrapper,这个类充当 Django 和数据库库本身特性之间的桥梁。所有数据库特性和操作都要通过这个类,特别是在django.db.connection提供的一个实例。

使用DATABASE_OPTIONS设置作为关键字参数的字典,自动创建DatabaseWrapper的实例。这个类没有任何强制的参数集,所以记录后端接受什么参数是很重要的,这样开发人员就可以相应地定制它。

DatabaseWrapper类中有一些属性和方法定义了后端行为的一些更一般的方面。其中大多数都在一个基类中进行了适当的定义,以使这变得更容易。通过子类化django.db.backends.BaseDatabaseWrapper,可以继承一些明智的默认行为。

尽管单个后端可以自由地用任何合适的自定义行为覆盖它们,但有些行为必须总是由后端的DatabaseWrapper明确定义。在这种情况下,以下部分将直接陈述这一要求。

数据库包装器.功能

这个对象通常是一个被指定为django.db.backends.DatabaseFeatures的类的实例,它包含的属性表明后端是否支持 Django 可以利用的各种数据库相关特性。虽然这个类在技术上可以被命名为任何东西,因为它只作为DatabaseWrapper的一个属性被访问,但是最好还是与 Django 自己的命名约定保持一致,以避免混淆。

DatabaseWrapper本身一样,Django 提供了一个基类,为这个对象上所有可用的属性指定默认值。位于django.db.backends.BaseDatabaseFeatures,这可以用来大大简化特定后端中的特性定义。只需覆盖与所讨论的后端不同的任何特性定义。

以下是受支持功能及其默认支持状态的列表:

  • allows_group_by_pk—指示GROUP BY子句是否可以使用主键列。如果是这样,Django 可以在这些情况下使用它来优化查询;默认为False
  • can_combine_inserts_with_and_without_auto_increment_pk—当一次插入多条记录时,该属性指示后端是否可以支持将一些具有自动递增主键的值的记录与其他没有值的记录一起插入。这默认为False,Django 将在将记录插入数据库之前从数据中删除这些主键值。
  • can_defer_constraint_checks—指示数据库是否允许删除记录而不首先使指向该记录的任何关系无效;默认为False
  • can_distinct_on_fields—表示数据库是否支持使用DISTINCT ON子句只检查某些字段的唯一性。这默认为True,所以如果数据库不支持这个子句,一定要覆盖下一节中描述的distinct_sql()方法,以便在请求字段时引发一个异常。
  • can_introspect_foreign_keys—指示数据库是否为 Django 提供了一种方法来确定正在使用哪些外键;默认为True
  • can_return_id_from_insert—指示后端是否可以在插入记录后立即提供新的自动递增的主键 ID。默认为False;如果设置为True,您还需要提供下一节描述的return_insert_id()功能。
  • can_use_chunked_reads—指示数据库是否可以迭代部分结果集,而不必一次全部读入内存。默认为True;如果False,Django 将把所有结果加载到内存中,然后再把它们传递回应用。
  • empty_fetchmany_value—指定当提取多行时,数据库库返回什么值来指示没有更多数据可用;默认为空列表。
  • has_bulk_insert—表示后端是否支持在一条 SQL 语句中插入多条记录;默认为False
  • has_select_for_update—表示数据库是否支持SELECT FOR UPDATE查询,即在使用行时锁定该行;默认为False
  • has_select_for_update_nowait—如果您使用SELECT FOR UPDATE,并且另一个查询已经有一个锁,一些后端允许您指定一个NOWAIT选项立即失败,而不是等待锁被释放。此属性指示数据库是否支持此功能;默认为False
  • interprets_empty_strings_as_nulls—表示数据库是否将空字符串视为与NULL相同的值;默认为False
  • needs_datetime_string_cast—表示从数据库中检索日期后,是否需要将日期从字符串转换为datetime对象;默认为True
  • related_fields_match_type—指示数据库是否要求关系字段与其相关字段的类型相同。这专门用于PositiveIntegerFieldPositiveSmallIntegerField类型;如果True,将使用相关字段的实际类型来描述关系;如果默认为False,Django 将使用IntegerField来代替。
  • supports_mixed_date_datetime_comparisons—指示数据库是否支持在查找记录时使用timedeltadatedatetime进行比较;默认为True。如果设置为True,确保也提供下一节描述的date_interval_sql()方法。
  • supports_select_related—指示后端是否允许QuerySet提前拉入相关信息,以减少许多情况下的查询数量。它默认为True,但是在处理非关系数据库时可以设置为False,在非关系数据库中,“相关”的概念实际上并不适用。
  • supports_tablespaces—表示该表是否支持表空间。它们不是 SQL 标准的一部分,所以默认为False。如果将其设置为True,请务必执行下一节中描述的tablespace_sql()方法。
  • update_can_self_select—指示数据库是否能够对当前正在用UPDATE查询修改的表执行SELECT子查询;默认为True
  • uses_autocommit—表示后端是否允许数据库直接管理自动提交行为;默认为False
  • uses_custom_query_class—指示后端是否提供其自己的Query类,该类将用于定制如何执行查询;默认为False
  • uses_savepoints—指示除了完整事务之外,数据库是否支持保存点。保存点允许在更细粒度的基础上回滚数据库查询,而不需要在出错时撤销整个事务。该属性默认为False;将其设置为True还需要实现下一节中描述的savepoint_create_sql()savepoint_commit_sql()savepoint_rollback_sql(sid)方法。

除了在测试中,这个类还有一些 Django 不直接使用的附加属性。如果您试图使用这些特性,Django 将简单地传递原始数据库错误。这些属性仅在测试中使用,以确认数据库确实应该为相关操作引发错误。

  • allow_sliced_subqueries—表示后端是否可以对子查询执行切片操作;默认为True
  • allows_primary_key_0—表示后端是否允许 0 作为主键列的值;默认为True
  • has_real_datatype—指示数据库是否具有表示实数的本地数据类型;默认为False
  • ignores_nulls_in_unique_constraints—当在具有跨多个列的唯一约束的表上检查重复项时,一些数据库将考虑NULL值并防止重复项,而其他数据库将忽略它们。该属性默认为True,这表明如果只有重复的列包含NULL值,数据库将允许重复的条目。
  • requires_explicit_null_ordering_when_grouping—指示当使用GROUP BY子句来防止数据库尝试对记录进行不必要的排序时,数据库是否需要额外的ORDER BY NULL子句;默认为False
  • requires_rollback_on_dirty_transaction—如果某个事务由于某种原因无法完成,该属性表示该事务是否需要回滚才能开始新的事务;默认为False
  • supports_1000_query_parameters—指示后端是否支持最多 1000 个传递到查询中的参数,尤其是在使用IN操作符时;默认为True
  • supports_bitwise_or—顾名思义,这个表示数据库是否支持按位OR操作;默认为True
  • supports_date_lookup_using_string—表示在查询datedatetime字段时,是否可以使用字符串而不是数字;默认为True
  • supports_forward_references—如果数据库在事务结束时检查外键约束,一条记录将能够引用另一条尚未添加到事务中的记录。默认情况下这是True,但是如果数据库改为为事务中的每条记录检查这些约束,您需要将它设置为False
  • supports_long_model_names—这一条更加简单明了,表明数据库是否允许表名比您通常预期的要长。这默认为True,主要用于测试 MySQL,它只支持 64 个字符的表名。
  • supports_microsecond_precision—指示datetimetime字段在数据库级别是否支持微秒;默认为True
  • supports_regex_backreferencing—表示数据库的正则表达式引擎是否支持使用分组和那些组的反向引用;默认为True
  • supports_sequence_reset—表示数据库是否支持重置序列;默认为True
  • supports_subqueries_in_group_by—指示数据库是否支持从子查询中选择,同时使用GROUP BY子句执行聚合;默认为True
  • supports_timezones—表示在与数据库中的datetime字段交互时,是否可以提供具有时区的datetime对象;默认为True
  • supports_unspecified_pk—如果模型使用主键而不是默认的自动递增选项,每个实例通常需要指定一个主键。如果数据库甚至在没有主键的情况下保存实例,您需要将其设置为True,这样 Django 就可以跳过对该行为的测试。
  • test_db_allows_multiple_connections—指示仅测试数据库是否支持多个连接。这默认为True,因为大多数数据库都支持它,但其他数据库可能会使用内存数据库之类的东西进行测试,这可能不支持多个连接。

数据库包装器

这是大多数特定于数据库的特性的入口,主要是处理每个数据库在处理特定类型的 SQL 子句时的各种差异。每个数据库供应商都有自己的一套需要支持的特殊语法,在后端定义这些语法可以让 Django 无需担心这些细节。

像前面描述的情况一样,后端只需要编写那些偏离标准的操作。同样位于django.db.models.backendsBaseDatabaseOperations,为许多这些操作提供了默认行为,而其他的必须由后端自己实现。下面的列表解释了它们的用途和默认行为。

  • autoinc_sql(table, column)—返回创建自动递增主键所需的 SQL。如果数据库有一个本地支持的字段,那么将使用“创建新结构”一节中描述的creation模块选择该字段,并且该方法应该返回None而不是任何 SQL 语句,这也是默认行为。
  • bulk_batch_size(fields, objs)—批量插入记录时,您会发现有些数据库有限制,要求将记录拆分成多个批次。给定要插入的字段和包含这些字段值的对象,此方法返回要在单个批处理中插入的记录数。默认实现只是返回对象的数量,因此总是使用一个批处理来插入任意数量的记录。
  • cache_key_culling_sql()—返回用于选择要剔除的缓存关键字的 SQL 模板。返回的模板字符串应该包含一个%s占位符,它将是缓存表的名称。它还应该包含一个%%s引用,这样以后就可以用应该剔除的键之前的最后一个键的索引来替换它。
  • compiler(compiler_name)—根据给定的编译器名称返回 SQL 编译器。默认情况下,该方法将根据BaseDatabaseOperations对象的compiler_module属性导入一个模块,并在该模块中查找给定的compiler_namecompiler_module被设置为"django.db.models.sql.compiler",但是如果您想使用自己的编译器而不覆盖这个方法,您可以覆盖它。
  • date_extract_sql(lookup_type, field_name)—返回只提取部分日期的 SQL 语句,以便与过滤器参数进行比较。lookup_type将是"year""month""day"中的一个,而field_name是包含要检查的日期的表格列的名称。这没有默认行为,必须由后端定义以避免出现NotImplementedError
  • date_interval_sql(sql, connector, timedelta)—返回一个 SQL 子句,该子句将执行带有datedatetime列和timedelta值的操作。sql参数将包含用于datedatetime列的必要 SQL,而connector将包含将与timedelta值一起使用的运算符。这个方法负责格式化表达式,以及使用数据库的词汇表描述timedelta
  • date_trunc_sql(lookup_type, field_name)—返回一条 SQL 语句,该语句删除了超出lookup_type所提供的特定性的日期部分。可能的值与date_extract_sql()的值相同,但不同之处在于,例如,如果lookup_type"month",它将返回一个指定月份和年份的值,而date_extract_sql()将返回不带年份的月份。同样像date_extract_sql()一样,这不是默认行为,必须实现。
  • datetime_cast_sql()—返回将datetime值强制转换为数据库库使用的任何格式所需的 SQL,以返回 Python 中真正的datetime对象。返回值将被用作 Python 格式的字符串,它将只接收字段名,在字符串中被引用为%s。默认情况下,它只返回"%s",这对于不需要任何特殊类型转换的数据库来说很好。
  • deferrable_sql()—返回附加到约束定义所需的 SQL,以使该约束最初被延迟,以便在事务结束之前不会被检查。这将被附加在约束定义之后,因此如果需要空格,返回值必须在开头包含空格。默认情况下,这将返回一个空字符串。
  • distinct_sql(fields)—返回一个 SQL 子句来选择唯一的记录,也可以根据字段名称列表进行选择。当fields为空时,默认实现返回"DISTINCT",当fields被填充时,默认实现引发NotImplementedError,所以如果数据库支持基于有限字段集的唯一性检查,一定要覆盖这个实现。
  • drop_foreignkey_sql()—返回将删除外键引用的 SQL 片段,作为ALTER TABLE语句的一部分。引用的名称将在后面自动追加,因此只需指定命令本身。例如,默认返回值仅仅是"DROP CONSTRAINT"
  • drop_sequence_sql(table)—返回一条 SQL 语句,从指定的表中删除自动递增序列。这与autoinc_sql()形成了某种配对,因为如果序列是显式创建的,那么只需要显式删除它。默认情况下,这将返回None,表示不采取任何行动。
  • end_transaction_sql(success=True)—返回结束打开的事务所需的 SQL。success参数指示交易是否成功,并可用于确定采取什么行动。例如,如果success被设置为True,则默认实现返回"COMMIT;",否则返回"ROLLBACK;"
  • fetch_returned_insert_id(cursor)—返回支持获取该信息的后端最后插入的记录的 ID。默认实现调用cursor.fetchone()[0]
  • field_cast_sql(db_type)—返回一个 SQL 片段,用于将指定的数据库列类型转换为某个值,该值可以更准确地与WHERE子句中的筛选器参数进行比较。返回值必须是 Python 格式的字符串,唯一的参数是要转换的字段的名称。默认返回值是"%s"
  • force_no_ordering()—返回可在ORDER BY子句中使用的名称列表,以从查询中删除所有排序。默认情况下,这将返回一个空列表。
  • for_update_sql(nowait=False)—返回一个 SQL 子句,该子句将在从数据库中选择数据时请求锁定。nowait参数指示是否包含必要的子句,以便在锁已经就位的情况下立即失效,而不是等待锁被释放。
  • fulltext_search_sql(field_name)—返回一个 SQL 片段,用于对指定字段进行全文搜索(如果支持的话)。返回的字符串还应该包含一个用于搜索用户指定值的%s占位符,该占位符将在该方法之外自动加引号。如果数据库不支持全文搜索,那么默认行为是通过引发一个带有适当消息的NotImplementedError来表明这一点。
  • last_executed_query(cursor, sql, params)—返回发送到数据库的最后一个查询,与发送时完全相同。默认情况下,这个方法必须通过用params提供的参数替换sql参数中的占位符来重新构建查询,这对于所有后端都是正确的,不需要任何额外的工作。一些后端可能有更快或更方便的快捷方式来检索最后一个查询,因此也提供了数据库光标,作为使用该快捷方式的一种方式。
  • last_insert_id(cursor, table_name, pk_name)—返回最后一个INSERT插入到数据库中的行的 ID。默认情况下,这只是返回cursor.lastrowid,正如 PEP-249 所指定的,但是其他后端可能有其他方法来检索这个值。为了帮助相应地访问它,该方法还接收插入行的表的名称和主键列的名称。
  • lookup_cast(lookup_type)—返回将值转换为可与指定的lookup_type一起使用的格式所需的 SQL。返回值还必须包含一个用于要转换的实际值的%s占位符,默认情况下,它只返回"%s"
  • max_in_list_size()—返回可在单个IN子句中使用的项目数。默认返回值None,表示这些项目的数量没有限制。
  • max_name_length()—返回数据库引擎允许用于表名和列名的最大字符数。默认情况下,这将返回None,表示没有限制。
  • no_limit_value()-返回应该用于指示无穷大极限的值,在指定没有极限的偏移时使用。一些数据库允许无限制地使用偏移量,在这些情况下,这个方法应该返回None。默认情况下,这会引发一个NotImplementedError,并且必须由后端实现,以允许无限制地使用偏移量。
  • pk_default_value()—返回发出INSERT语句时要使用的值,以指示主键字段应该使用其默认值—即递增序列—而不是某个指定的 ID;默认为"DEFAULT"
  • prep_for_like_query(x)—返回x的修改形式,适用于查询的WHERE子句中的LIKE比较。默认情况下,这会对x中的任何百分号(%)、下划线(_)或双反斜杠(\\)进行转义,并在适当的时候加上额外的反斜杠。
  • prep_for_ilike_query(x)—就像prep_for_like_query(),但是不区分大小写的比较。默认情况下,这是prep_for_like_query()的精确副本,但是如果数据库以不同的方式对待不区分大小写的比较,这可以被覆盖。
  • process_clob(value)—返回 CLOB 列引用的值,以防数据库需要一些额外的处理来生成实际值。默认情况下,它只返回提供的值。
  • query_class(DefaultQueryClass)—如果后端提供了一个自定义的Query类,如DatabaseWrapper.features.uses_custom_query_class所示,该方法必须根据提供的DefaultQueryClass返回一个自定义的Query类。如果uses_custom_query_classFalse,这个方法永远不会被调用,所以默认行为是简单地返回None
  • quote_name(name)—返回给定name的格式副本,带有适用于数据库引擎的引号。所提供的名称可能已经被引用过一次,因此该方法还应该注意检查这一点,在这种情况下不要添加额外的引号。因为在查询中引用名字没有既定的标准,所以这必须由后端实现,否则会引发一个NotImplementedError
  • random_function_sql()—返回生成随机值所需的 SQL 默认为"RANDOM()"
  • regex_lookup(lookup_type)—返回用于对列执行正则表达式匹配的 SQL。返回值应该包含两个%s占位符,第一个用于列名,另一个用于要匹配的值。查找类型可以是regexiregex,区别在于区分大小写。默认情况下,这会引发一个NotImplementedError,表明数据库后端不支持正则表达式。然而,对于简单的情况,可以使用下一节描述的DatabaseWrapper.operators字典来支持regexiregex
  • return_insert_id()—返回一个子句,该子句可在INSERT查询的末尾使用,以返回新插入记录的 ID。默认情况下,这只是返回None,不会向查询添加任何内容。
  • savepoint_create_sql(sid)—返回用于创建新保存点的 SQL 语句。sid参数是保存点的名称,以便以后引用。
  • savepoint_commit_sql(sid)—明确提交由sid参数引用的保存点。
  • savepoint_rollback_sql(sid)—根据sid参数引用的保存点回滚事务的一部分。
  • set_time_zone_sql()—返回可用于设置数据库连接时区的 SQL 模板。模板应该接受一个%s值,该值将被替换为要使用的时区。默认情况下,这会返回一个空字符串,表明数据库不支持时区。
  • sql_flush(style, tables, sequences)—返回从指定结构中删除所有数据所需的 SQL,同时保持结构本身不变。因为这在不同的数据库引擎之间是如此不同,所以默认行为会引发一个NotImplementedError,并且必须由后端实现。
  • sequence_reset_by_name_sql(style, sequences)—返回重置sequences列表中命名的自动递增序列所需的 SQL 语句列表。像autoinc_sql()drop_sequence_sql()一样,这只对维护自动 id 的独立序列的数据库有用,如果不需要,可以返回一个空列表,这是默认行为。
  • sequence_reset_sql(style, model_list)—像sequence_reset_by_name_sql(),—返回重置自动递增序列所需的 SQL 语句列表,但是指定的列表包含 Django 模型,而不是序列名称。这也与返回空列表的默认行为相同。
  • start_transaction_sql()—返回用于输入新事务的 SQL 默认为"BEGIN;"
  • sql_for_tablespace(tablespace, inline=False)—返回 SQL 来声明一个表空间,如果数据库不支持它们,则返回None,这是默认设置。
  • validate_autopk_value(value)—验证给定值是否适合用作数据库中的序列 ID。例如,如果数据库不允许零作为有效 id,那么该值应该引发一个ValueError。默认情况下,这只是返回一个值,表明它是有效的。
  • value_to_db_date(value)—将date对象转换为适用于数据库中DateField列的对象。
  • value_to_db_datetime(value)—将datetime对象转换为适用于DateTimeField列的值。
  • value_to_db_time(value)—将time对象转换为可用于数据库的TimeField列的值。
  • value_to_db_decimal(value)—将Decimal对象转换为数据库可以放入DecimalField列的值。
  • year_lookup_bounds(value)—返回表示给定年份的下限和上限的两项列表。value参数是一个int年份,每个返回值是一个表示完整日期和时间的字符串。第一个返回值是被视为所提供年份的一部分的最小日期和时间,而第二个返回值是被视为同一年的一部分的最大日期和时间。
  • year_lookup_bounds_for_date_feld(value)—还返回一个 2 项列表,表示作为value提供的年份的日期和时间的上限和下限。默认情况下,这取决于year_lookup_bounds(),但是如果数据库不能将完整的日期/时间值与DateField进行比较,这一点可以被覆盖。

比较运算符

许多可以在数据库中进行的比较遵循一种简单的格式,一个值后跟某种运算符,然后再跟另一个值进行比较。因为这是一种常见的情况,并且使用起来非常简单,Django 使用了一种更简单的方法来定义这些类型的比较操作符。

DatabaseWrapper对象的另一个属性operators包含一个字典,将各种查找类型映射到实现它们的数据库操作符。这非常依赖于基本结构,因为虽然这个字典的键是查找类型,但是值是应该放在被比较的字段名称之后的 SQL 片段。

例如,考虑由标准的=操作符处理"exact"查找的常见情况,这将由如下的字典处理:

class DatabaseWrapper(BaseDatabaseWrapper):

operators = {

"exact": "= %s"

}

然后用 Django 支持的其他操作符来填充这个字典。

获取光标

将所有这些特定于数据库的特性与 Django 的面向对象数据库 API 结合起来,可以提供一个可能性的世界,但它们都是为了涵盖最常见的情况而设计的。数据库支持各种各样的附加功能,这些功能要么不常用,要么在不同的实现中完全不同。Django 没有试图在所有数据库中支持所有这些特性,而是提供了直接访问数据库本身的便利。

DatabaseWrappercursor()方法直接从用于连接数据库本身的第三方库中返回一个数据库游标。为了与标准 Python 策略保持一致,这个 cursor 对象与 PEP-249 兼容,因此它甚至可以与其他数据库抽象库一起使用。因为这个对象的属性和方法的行为不在 Django 的控制之内——通常在不同的实现之间变化很大——所以最好查阅完整的 PEP 和您的数据库库文档,以获得关于如何使用它的详细信息。

创建新的结构

Django 的数据库连接提供的一个更方便的特性是能够完全基于 Python 中声明的模型定义自动创建表、列和索引。与强大的数据库查询 API 一起,这是避免在整个应用中使用 SQL 代码的一个关键特性,保持了它的整洁和可移植性。

虽然 SQL 语法本身在创建数据结构方面已经相当好地标准化了,但是各个字段类型可用的名称和选项在不同的实现中有很大的不同。这就是 Django 的数据库后端的用武之地,它提供了 Django 的基本字段类型到特定数据库的适当列类型的映射。

这个映射存储在后端包的creation模块中,该模块必须包含一个DatabaseCreation类,该类是django.db.backends.creation.BaseDatabaseCreation的子类。这个类包含一个名为data_types的属性,其中包含一个字典,该字典的键与来自各种Field子类的可用返回值以及将作为列定义传递给数据库的字符串值相匹配。

该值也可以是 Python 格式的字符串,它将被赋予一个字段属性字典,以便可以使用自定义的字段设置来确定如何创建列。例如,CharField就是这样传递max_length属性的。虽然许多字段类型有共同的属性,但对列类型最有用的属性可能是每个单独字段特有的。请查阅字段的源代码,以确定哪些属性可用于此映射。

有许多基本字段类型可用作内部列类型:

  • AutoField—自动递增的数值字段,用于模型中未明确定义的主键。
  • BooleanField—仅代表两个可能值的字段:开和关。如果数据库没有表示这种情况的单独的列,也可以使用单字符的CharField来存储"1""0"来模拟这种行为。
  • CharField—包含有限自由格式文本的字段。通常,这在数据库中使用可变长度的字符串类型,使用额外的max_length属性来定义存储值的最大长度。
  • CommaSeparatedIntegerField—包含整数列表的字段,通常表示 id,存储在由逗号分隔的单个字符串中。因为列表存储为字符串,所以在数据库端也使用可变长度的字符串类型。虽然一些数据库可能有更智能和有效的方法来存储这种类型的数据,但字段的代码仍然需要一个数字字符串,所以后端应该总是返回一个数字。
  • DateField—标准日期,没有任何相关的时间信息。大多数数据库应该有一个日期列类型,所以这应该很容易支持。只需确保所使用的列类型在检索时返回 Python datetime.date
  • DateTimeField—日期,但附有相关的时间信息,不包括时区。同样,大多数合理的数据库将很容易支持这一点,但要确保从数据库中检索时 Python 库返回一个datetime.datetime
  • DecimalField—固定精度的十进制数。这是使用字段属性定义数据库列的另一个例子,因为max_digitsdecimal_places字段属性应该控制数据库列的对等项。
  • FileField—存储在别处的文件的名称和位置。Django 不支持将文件作为二进制数据存储在数据库中,所以它的文件由相对路径和名称引用,存储在关联的列中。因为那是文本,所以它再次使用了标准的可变长度文本字段,该字段也利用了max_length字段属性。
  • FilePathField—存储系统中文件的名称和路径。该字段在许多方面与FileField相似,但这是为了允许用户从现有文件中选择,而FileField的存在是为了允许保存新文件。因为实际存储的数据本质上是相同的格式,所以它以相同的方式工作,使用由max_length属性指定的可变长度字符串。
  • FloatField—包含浮点数的字段。数据库是否在内部存储固定精度的数字并不重要,只要 Python 库为存储在列中的值返回一个float即可。
  • IntegerField—包含有符号 32 位整数的字段。
  • BigIntegerField—包含有符号 64 位整数的字段。
  • IPAddressField—互联网协议(IP)地址,使用当前的 IPv4 3 标准,用 Python 表示为字符串。
  • GenericIPAddressField—使用原始 IPv4 标准或较新的 IPv6 4 标准的 IP 地址。
  • NullBooleanField—一个布尔字段,也允许将NULL值存储在数据库中。
  • PositiveIntegerField—包含无符号 32 位整数的字段。
  • PositiveSmallIntegerField—包含无符号 8 位整数的字段。
  • SmallIntegerField—包含有符号 8 位整数的字段。
  • TextField—不限长度的文本字段,或者至少是数据库提供的最大文本字段。max_length属性对该字段的长度没有影响。
  • TimeField—表示一天中的时间的字段,没有任何相关的日期信息。数据库库应该为该列中的值返回一个datetime.time对象。

反思现有结构

除了能够基于模型信息创建新的表结构之外,还可以使用现有的表结构来生成新的模型。这不是一个完美的过程,因为有些模型信息没有存储在表自己的定义中,但是对于必须使用现有数据库的新项目来说,这是一个很好的起点,这些新项目通常与正在淘汰的遗留应用一起运行。

为此,后端应该提供一个名为introspection.py的模块,该模块为一个DatabaseIntrospection类提供了许多方法来检索关于表结构的各种细节。每个方法接收一个活动的数据库游标;下面的列表中记录了这些方法的所有参数和返回值,以及另一个用于根据基础列类型选择正确字段类型的映射。

  • get_table_list(cursor)—返回数据库中存在的表名列表。
  • get_table_description(cursor, table_name)—给定使用get_table_list()找到的特定表的名称,这将返回一个元组列表,每个元组描述表中的一列。对于光标的description属性:(name, type_code, display_size, internal_size, precision, scale, null_ok),每个元组都遵循 PEP-249 的标准。这里的type_code是数据库用来标识列类型的内部类型,这将被本节末尾描述的反向映射所使用。
  • get_relations(cursor, table_name)—给定一个表名,这将返回一个字典,详细说明该表与其他表的关系。每个键都是该列在所有列的列表中的索引,而关联的值是一个 2 元组。第一项是相关字段根据其表列的索引,第二项是相关表的名称。如果数据库没有提供一种简单的方法来访问这些信息,那么这个函数可以抛出NotImplementedError,关系将被从生成的模型中排除。
  • get_key_columns(cursor, table_name)—给定一个表名,这将返回与其他表相关的列的列表以及这些引用如何工作。列表中的每一项都是一个元组,由列名、它引用的表以及被引用的表中的列组成。
  • get_indexes(cursor, table_name)—给定表的名称,这将返回以任何方式索引的所有字段的字典。字典的键是列名,而值是附加字典。每个值的字典包含两个键:'primary_key''unique',每个键不是True就是False。如果两者都是False,则该列仍然由于在外部字典中而被指示为已索引;它只是一个普通的索引,没有主键或唯一约束。像get_relations()一样,如果没有简单的方法来获取这些信息,这也会引发NotImplementedError

除了前面的方法,自省类还提供了一个名为data_types_reverse的字典,它映射从get_table_description()返回的字典中的type_code值。关键字是作为type_code返回的任何值,不管它是字符串、整数还是其他什么。这些值是字符串,包含支持相关列类型的 Django 字段的名称。

数据库客户端

这个类位于数据库后端的client.py模块中,负责调用由DATABASE_ENGINE指定的当前数据库的命令行接口(shell)。这是使用manage.py dbshell命令调用的,允许用户在必要时手动管理底层表的结构和数据。

这个类只包含一个方法runshell(),它没有参数。然后,该方法负责读取给定后端的适当数据库设置,并配置对数据库 shell 程序的调用。

数据库错误和完整性错误

{{ backend }}.base拉进来的这些类允许异常被容易地处理,同时仍然能够交换出数据库。IntegrityError应该是DatabaseError的子类,所以如果错误的确切类型不重要,应用可以只检查DatabaseError

符合 PEP-249 的第三方库已经有这些可用的类,所以它们通常可以被分配到base模块的名称空间中,并正常工作。唯一需要对它们进行子类化或直接定义的时候是,如果所使用的库的行为方式与 Django 支持的其他数据库不同。请记住,这是关于整个框架的一致性。

证明

虽然用户名和密码的组合是一种非常常见的身份验证方法,但它远不是唯一可用的方法。其他方法,如 OpenID,使用完全不同的技术,甚至不包括用户名或密码。此外,一些使用用户名和密码的系统可能已经将这些信息存储在不同的数据库或结构中,而不是 Django 默认查看的数据库或结构中,因此仍然需要进行一些额外的处理,以根据正确的数据验证凭证。

为了解决这些情况,Django 的认证机制可以用定制代码来代替,支持任何需要使用的系统。事实上,多种身份验证方案可以一起使用,如果不能产生有效的用户帐户,每一种都可以退回到下一种。这完全由分配给AUTHENTICATION_BACKENDS设置的一组导入路径控制。它们会按照从第一个到最后一个的顺序被尝试,只有所有后端都返回None才会被认为认证失败。每个身份验证后端只是一个标准的 Python 类,它提供了两个特定的方法。

获取用户(用户标识)

任何时候预先知道用户的 ID,无论是从会话变量、数据库记录还是其他地方,认证后端负责将该 ID 转换成可用的django.contrib.auth.models.User实例。对于不同的后端来说,ID 的含义可能是不同的,所以这个参数的确切类型也可能根据所使用的后端而变化。对于 Django 附带的默认设置django.contrib.auth.backends.ModelBackend,这是存储用户信息的数据库 ID。对其他人来说,它可能是用户名、域名或其他任何东西。

认证(* *凭证)

当不知道用户的 ID 时,有必要要求一些凭证,使用这些凭证可以识别和检索适当的User帐户。在默认情况下,这些凭据是用户名和密码,但其他凭据可能使用 URL 或一次性令牌。在现实世界中,后端不会接受使用**语法的参数,而是只接受那些对它有意义的参数。然而,因为不同的后端将采用不同的凭证集,所以没有适合所有情况的单一方法定义。

PASSING INFORMATION TO CUSTOM BACKENDS

您可能已经从前面几节中注意到,传递到身份验证后端的数据在很大程度上取决于所使用的后端。默认情况下,Django 从其登录表单传入用户名和密码,但是其他表单可以提供适合该表单的任何其他凭证。

存储用户信息

身份验证的一个不明显的方面是,无论出于何种目的,所有用户都必须在 Django 中被表示为django.contrib.auth应用中的User对象。Django 并不严格要求将这作为一个框架,但是大多数应用——包括提供的管理界面——都希望用户存在于数据库中,并与该模型建立关系。

对于调用外部服务进行身份验证的后端,这意味着复制 Django 数据库中的每个用户,以确保应用正常工作。从表面上看,这听起来像是一场维护噩梦;不仅需要复制每个现有用户,还需要添加新用户,并且对用户信息的更改也应该反映在 Django 中。如果所有这些都必须由所有用户手工管理,这肯定会是一个相当大的问题。

但是请记住,认证后端的唯一真正要求是它接收用户的凭证并返回一个User对象。在这两者之间,都只是标准的 Python,Django 的整个模型 API 都是公开的。一旦用户在后台通过了身份验证,后端可以简单地创建一个新的User(如果还没有的话)。如果确实存在,它甚至可以用“真实”用户数据库中更新的任何新信息来更新现有记录。这样,一切都可以保持同步,而不必为 Django 做任何特别的事情。只需使用您已经在使用的任何系统来管理您的用户,让您的身份验证后端处理剩下的工作。

文件

Web 应用通常将大部分时间花在处理数据库中的信息上,但是有很多原因导致应用可能也需要直接处理文件。无论是用户上传头像或演示文稿、即时生成图像或其他静态内容,还是定期备份日志文件,文件都可能成为应用中非常重要的一部分。和许多其他东西一样,Django 既提供了一个处理文件的单一接口,也为额外的后端提供了一个 API 来提供额外的功能。

基本文件类

无论来源、目的或用途如何,Django 中的所有文件都表示为django.core.files.File的实例。这与 Python 自己的文件对象非常相似,但是做了一些添加和修改,以便用于 Web 和大型文件。File的子类可以改变幕后发生的事情,但是下面的 API 是所有文件类型的标准。以下属性适用于所有File对象:

  • File.closed—表示文件是否已关闭的布尔值。实例化时,所有的File对象都是打开的,可以立即访问它的内容。close()方法将此设置为True,在再次访问文件内容之前,必须使用open()重新打开文件。
  • File.DEFAULT_CHUNK_SIZE—通常是文件类的一个属性,而不是它的一个实例,它决定了chunks()方法应该使用多大的块。
  • File.mode—打开文件的访问模式;默认为'rb'
  • File.name—文件的名称,包括相对于其打开位置的任何给定路径。
  • File.size—文件内容的大小,以字节为单位。

以下方法也适用于File对象:

  • File.chunks(chunk_size=None)—遍历文件内容,生成一个或多个较小的块,以避免大文件填满服务器的可用内存。如果没有提供chunk_size,将使用默认为 64 KB 的DEFAULT_CHUNK_SIZE
  • File.close()—关闭文件,使其内容不可访问。
  • File.flush()—将任何新的挂起内容写入实际的文件系统。
  • File.multiple_chunks(chunk_size=None)—如果文件足够大,需要多次调用chunks()来检索全部内容,则返回True;如果可以一次读取全部内容,则返回Falsechunk_size的论证和chunks()中的一样。请注意,此时这实际上不会读取文件;它根据文件的size确定值。
  • File.open(mode=None)—如果文件先前已经关闭,则重新打开该文件。mode参数是可选的,默认为文件上次打开时使用的模式。
  • File.read(num_bytes=None)—从文件中检索一定数量的字节。如果在没有 size 参数的情况下调用,这将读取文件的剩余部分。
  • File.readlines()—以行列表的形式检索文件内容,如文件中出现的换行符(\r\n)所示。这些换行符留在列表中每一行的末尾。
  • File.seek(position)—将文件的内部位置移动到指定位置。所有的读写操作都是相对于这个位置的,所以这允许文件的不同部分被相同的代码访问。
  • File.tell()—返回内部指针的位置,以文件开头的字节数表示。
  • File.write(content)—将指定内容写入文件。这仅在文件以写模式打开时可用(该模式以'w'开始)。
  • File.xreadlines()readlines()的生成器版本,一次生成一行,包括换行符。为了与 Python 自身从xreadlines()的转变保持一致,该功能也是通过迭代File对象本身来提供的。

处理上传

当接受来自用户的文件时,事情变得有点棘手,因为在您的代码有机会检查它们之前,这些文件不应该与您的其他文件保存在一起。为了方便起见,Django 对待上传文件的方式有所不同,使用上传处理程序来决定应该用File的哪个子类来表示它们。每个上传处理器都有机会在上传过程中介入并改变 Django 的进程。

上传处理程序是用FILE_UPLOAD_HANDLERS设置指定的,它采用一系列导入路径。在处理上传的文件时,Django 依次调用每个处理程序的各种方法,这样它们就可以在数据进来时检查数据。不需要直接完成所有这些,因为它是由 Django 的请求处理代码自动处理的,但是新上传处理程序的 API 提供了足够的机会来定制如何管理传入文件。

  • FileUploadHandler.__init__(request)—每当有附加文件的请求传入时,处理程序被初始化,并且传入的请求被传入,以便处理程序可以决定它是否需要处理请求的文件。例如,如果它被设计为将上传的细节写入开发服务器的控制台,它可能会检查DEBUG设置是否是True以及request.META['REMOTE_ADDR']是否在INTERNAL_IPS设置中。如果处理程序应该总是处理每个请求,这不需要手动定义;继承的缺省值将满足大多数情况。
  • FileUploadHandler.new_file(field_name, file_name, content_type, content_length, charset=None)—这是为请求中提交的每个文件调用的,具有关于文件的各种细节,但没有其实际内容。field_name是用于上传文件的表单字段名称,而file_name是浏览器报告的文件本身的名称。content_typecontent_lengthcharset都是文件内容的属性,但是应该有所保留,因为不访问文件内容就无法验证它们。虽然没有严格要求,但是这个方法的主要功能是在调用received_data_chunk()时为文件内容留出一个存储位置。对于使用什么类型的存储,或者使用什么属性没有要求,所以几乎任何东西都是公平的。常见的例子是临时文件或StringIO对象。此外,该方法提供了一种方式来决定是否应该启用某些功能,例如由content_type确定的自动生成的图像缩略图。
  • FileUploadHandler.receive_data_chunk(raw_data, start)—这是仅有的两个必需方法之一,在文件的整个处理过程中被重复调用,每次接收文件内容的一部分作为raw_data,其中start是文件中找到该内容的偏移量。每次调用的数据量基于处理程序的chunk_size属性,默认为 64KiB。一旦这个方法完成了对数据块的处理,它还可以控制其他处理程序如何处理这些数据。这是由方法是否返回任何数据决定的,任何返回的数据都被传递给下一个处理程序。如果它返回None,Django 将简单地对下一个数据块重复这个过程。
  • FileUploadHandler.file_complete(file_size)—作为对new_file()的补充,当 Django 在请求中找到文件的结尾时,这个方法被调用。因为这也是唯一可以确定文件总大小的时候,Django 给每个处理程序一个机会来决定如何处理这些信息。这是上传处理程序中唯一需要的方法,如果文件是由这个处理程序处理的,它应该返回一个UploadedFile对象。返回的UploadedFile将被关联的表单用作用于上传文件的字段的内容。如果处理程序没有对文件做任何事情,不管出于什么原因,它都会返回None。但是,要小心这一点,因为至少有一个上传处理程序必须返回一个用于表单的UploadedFile
  • FileUploadHandler.upload_complete()—当每个文件加载完成时调用file_complete(),当所有上传的文件处理完毕后,每个请求调用一次upload_complete()。如果处理程序在处理所有文件时需要设置任何临时资源,这个方法是清理自身的地方,为应用的其余部分释放资源。

请注意,这些方法实现的许多特性依赖于一个方法知道前一个方法已经做出了什么决定,但是没有明显的方法来持久保存这些信息。由于处理程序是针对每个传入的请求和流程文件一次实例化一个,因此可以简单地在处理程序对象本身上设置自定义属性,将来的方法调用可以回读这些属性以确定如何继续。

例如,如果__init__()self.activated设置为Falsereceive_data_chunk()可以读取该属性,以确定它是否应该处理它接收到的块,或者只是将它们传递给队列中的下一个处理程序。new_file()也可以设置相同或相似的属性,因此这些类型的决定可以基于每个文件和每个请求做出。

因为每个处理程序都是独立工作的,所以对于使用哪些属性或它们的用途没有任何标准。相反,各种已安装的上传处理程序之间的交互是通过在各种情况下引发大量异常来处理的。上传处理程序的正确操作不需要使用其中的任何一个,但是它们可以很大程度上定制多个处理程序如何协同工作。和FileUploadHandler一样,这些在django.core.files.uploadhander都有。

  • StopUpload—告诉 Django 停止处理上传中的所有文件,防止所有处理程序处理比它们已经处理的更多的数据。它还接受一个可选参数connection_reset,一个布尔值,指示 Django 是否应该停止,而不读取输入流的剩余部分。这个参数的缺省值False意味着 Django 将在把控制传递回表单之前读取整个请求,而True将在没有全部读取的情况下停止,导致在用户浏览器中显示“连接重置”消息。
  • SkipFile—告知上传过程停止处理当前文件,但继续处理列表中的下一个文件。如果请求中的单个文件有问题,这是更合适的行为,这不会影响可能同时上传的任何其他文件。
  • StopFutureHandlers—只有从new_file()方法中抛出时才有效,这表示当前上传处理程序将直接处理当前文件,在此之后任何其他处理程序都不应接收任何数据。任何在引发该异常的处理程序之前处理数据的处理程序都将继续按照它们原来的顺序执行,这取决于它们在FILE_UPLOAD_HANDLERS设置中的位置。

存储文件

所有文件存储操作都由位于django.core.files.storageStorageBase实例处理,默认存储系统由DEFAULT_FILE_STORAGE设置中的导入路径指定。存储系统包含处理文件存储和检索的方式和位置的所有必要功能。通过使用这一额外的层,可以交换所使用的存储系统,而不必对现有代码进行任何更改。这在从开发转移到生产时尤其重要,因为生产服务器通常有存储和服务静态文件的特殊需求。

为了提高这种灵活性,Django 提供了一个 API 来处理文件,这个 API 超越了 Python 提供的标准open()函数和相关的file对象。在这一章的前面,Django 的File对象被描述,解释了处理单个文件的可用特性。但是,当希望存储、检索或列出文件时,存储系统有一套不同的工具可用。

  • Storage.delete(name)—从存储系统中删除文件。
  • Storage.exists(name)—返回一个布尔值,表明指定的名称是否引用了存储系统中已存在的文件。
  • Storage.get_valid_name(name)—返回适用于当前存储系统的给定名称的版本。如果它已经有效,它将被原封不动地返回。这是仅有的两种默认实现方法之一,它将返回适用于本地文件系统的文件名,与操作系统无关。
  • Storage.get_available_name(name)—给定一个有效的名称,这将返回它的一个版本,该版本实际上可用于写入新文件,而不会覆盖任何现有文件。作为另一个具有默认行为的方法,这将在所请求的名称后面添加下划线,直到找到一个可用的名称。
  • Storage.open(name, mode='rb', mixin=None)—返回一个打开的File对象,通过该对象可以访问文件的内容。mode接受与 Python 的open()函数相同的所有参数,允许读写访问。可选的mixin参数接受一个与存储系统提供的File子类一起使用的类,以支持返回文件的附加特性。
  • Storage.path(name)—返回文件在本地文件系统上的绝对路径,可以使用 Python 的内置open()函数直接访问文件。这是为了方便文件存储在本地文件系统的常见情况。对于其他存储系统,如果没有可以访问文件的有效文件系统路径,这将引发一个NotImplementedError。除非您使用的库只接受文件路径,而不接受打开的文件对象,否则您应该始终使用Storage.open()打开文件,它适用于所有存储系统。
  • Storage.save(name, content)—将给定的内容保存到存储系统,最好以给定的名称保存。这个名称在保存之前将通过get_valid_name()get_available_name(),这个方法的返回值将是实际用来存储内容的名称。提供给该方法的content参数应该是一个File对象,通常是文件上传的结果。
  • Storage.size(name)—返回由name引用的文件的大小,以字节为单位。
  • Storage.url(name)—返回一个绝对 URL,文件的内容可以通过 Web 浏览器直接访问。
  • listdir(path)—返回由path参数指定的目录的内容。返回值是一个包含两个列表的元组:第一个列表用于位于该路径的目录,第二个列表用于位于同一路径的文件。

默认情况下,Django 附带了FileSystemStorage,顾名思义,它将文件存储在本地文件系统中。通常这意味着服务器的硬盘驱动器,但是有很多方法可以将其他类型的文件系统映射到本地路径,所以已经有很多可能性了。然而,还有更多的存储选项可用,并且有很多方法可以定制现有选项的行为。通过对StorageBase进行子类化,可以提供许多其他选项。

存储系统必须提供许多东西,从这些方法中的大多数开始。其中一个方法get_available_name(),严格来说不需要由新的存储类提供,因为它的默认实现适用于许多情况;覆盖它是一个偏好问题,而不是需求问题。另一方面,get_valid_name()方法有一个适合大多数后端的默认行为,但是有些可能有不同的文件命名需求,需要一个新的方法来覆盖它。

另外两种方法open()save()还有进一步的要求。根据定义,这两者都需要对每个不同的存储系统进行特殊处理,但是在大多数情况下不应该直接覆盖它们。除了存储和检索文件所必需的逻辑之外,它们还提供了额外的逻辑,这种逻辑应该得到维护。相反,他们将与实际存储机制的交互分别委托给了_open()_save(),这两者有着更简单的期望。

  • Storage._open(name, mode='rb')namemode参数与open()相同,但它不再有mixin逻辑来处理,因此_open()可以专注于返回一个适合访问所请求文件的File对象。
  • Storage._save(name, content)—这里的参数和save()一样,但是这里提供的名字会已经经过get_valid_name()get_available_name(),内容保证是File实例。这使得_save()方法可以专注于将文件内容提交给具有给定名称的存储系统。

除了提供这些方法之外,大多数定制存储系统还需要提供一个带有read()write()方法的File子类,这些方法旨在以最有效的方式访问底层数据。chunks()方法在内部遵从read(),因此不需要做任何事情来使应用处理大文件时更加内存友好。记住,不是所有的文件系统都允许只读写文件的一部分,所以在这些情况下,File子类可能还需要采取额外的步骤来最小化内存使用和网络流量。

会话管理

当用户偶然浏览一个网站时,为他们临时跟踪一些信息通常是有用的,即使还没有与他们相关联的User账户。这可以从他们第一次访问网站的时间到购物车。在这些情况下,典型的解决方案是会话——由存储在浏览器端 cookie 中的键引用的服务器端数据存储。Django 内置了对会话的支持,并留有一点配置空间。

大部分会话过程都是恒定的:识别没有会话的用户,分配一个新的密钥,将该密钥存储在 cookie 中,稍后检索该密钥,并且一直像字典一样工作。对于键的名称和使用时间有一些基本的设置,但是为了跨多个页面视图持久保存任何信息,键被用来引用存储在服务器上某个地方的一些数据,这就是大部分定制的来源。

Django 使用SESSION_ENGINE设置来确定哪个数据存储类应该自己处理实际数据。Django 自带了三个数据存储,涵盖了文件、数据库记录和内存缓存等常用策略,但是在不同的环境中还有其他选项,甚至 stock 类也可能需要额外的定制。为了适应这一点,SESSION_ENGINE接受完整的导入路径,允许将会话数据存储放在任何 Django 应用中。这个导入路径指向一个包含名为SessionStore的类的模块,它提供了完整的数据存储实现。

像大多数 Django 的可切换后端一样,有一个基本实现提供了大部分特性,留给子类的细节很少。对于会话,基类是SessionBase,位于django.contrib.sessions.backends.base。它处理会话密钥生成、cookie 管理、字典访问,并仅在必要时访问数据存储。这使得自定义的SessionStore类只需要实现五个方法,它们组合起来完成整个过程。

  • SessionStore.exists(session_key)—如果提供的会话密钥已经存在于数据存储中,则返回True,或者如果它可用于新会话,则返回False
  • SessionStore.load()—从数据存储使用的任何存储机制加载会话数据,返回表示该数据的字典。如果不存在会话数据,这将返回一个空字典,一些后端可能需要在返回之前保存新字典。
  • SessionStore.save()—使用当前会话密钥作为标识符,将当前会话数据提交到数据存储。这还应该使用会话的到期日期或期限来确定会话何时失效。
  • SessionStore.delete(session_key)—从数据存储中删除与给定密钥相关联的会话数据。
  • SessionStore.create()—创建新会话并返回它,以便外部代码可以向它添加新值。该方法负责创建新的数据容器,生成唯一的会话密钥,将该密钥存储在会话对象中,并在返回之前将空容器提交给后端。

此外,为了帮助会话数据存储访问完成工作所需的信息,Django 还提供了一些由SessionBase管理的附加属性。

  • session_key—存储在客户端 cookie 中的随机生成的会话密钥。
  • _session—包含与当前会话密钥相关联的会话数据的字典。
  • get_expiry_date()—返回一个表示会话何时到期的datetime.datetime对象。
  • get_expiry_age()—返回会话到期前的秒数。

通过在SessionBase的子类上实现五个方法,几乎可以在任何地方存储会话数据。尽管这些数据没有绑定到一个User对象,但它仍然是特定于浏览网站的个人的。为了存储对每个人都有用的临时信息,还需要一些其他的东西。

贮藏

当应用有大量很少改变的信息要处理时,在服务器上缓存这些信息通常是有用的,这样就不必在每次访问时都生成这些信息。这可以节省服务器上的内存使用、每个请求的处理时间,并最终帮助应用在相同的时间内处理更多的请求。

有很多方法可以访问 Django 的缓存机制,这取决于需要缓存多少信息。在线文档 5 涵盖了关于如何设置站点范围的缓存和每个视图的缓存的许多一般情况,但是较低级别的细节需要更多的解释。

指定后端

在 Django 中指定缓存后端的工作方式与本章中讨论的其他后端有很大不同。尽管需要考虑多个配置选项,但只有一个设置可以控制所有选项。这个设置CACHE_BACKEND使用 URI 语法 6 以一种可以被可靠解析的方式接受所有必要的信息。它可以分成三个独立的部分,每个部分都有自己的要求。

CACHE_BACKEND = '{{ scheme }}://{{ host }}/?{{ arguments }}'

  • scheme 部分指定应该使用哪个后端代码来提供缓存。Django 提供了四个后端,涵盖了大多数情况——dbfilelocmemmemcached7 、——这些都在网上有很好的文档记录,涵盖了大多数情况。对于自定义后端,这部分设置还可以接受一个模块的完整导入路径,该模块实现下一节中描述的协议。
  • 主机指定缓存实际应该存储在哪里,其格式将根据所使用的后端而有所不同。例如,db期望一个单一的数据库名称,file期望一个完整的目录路径,memcached期望一个服务器地址列表,而locmem根本不需要任何东西。宿主还可以通过尾部斜杠包含,这有助于可读性,因为它使整个设置看起来更像 URI。
  • 参数是可选的,可以用来定制在后端如何进行缓存。它们是使用查询字符串格式提供的,所有后端都需要一个参数:timeout,即项目从缓存中移除之前的秒数。对于大多数后端(包括 Django 提供的除了memcached之外的所有后端),还有两个参数可用:max_entries,剔除旧项目之前应该存储在缓存中的项目总数;以及cull_frequency,它控制当到达max_entries时从缓存中清除多少项。
  • 关于cull_frequency要意识到的一件重要的事情是,它的值实际上并不是项目应该被移除的频率。相反,该值用于一个简单的公式1 / cull_frequency,该公式确定有多少项受到影响。因此,如果你想一次清除 25%的条目,这相当于 1/4,所以你可以将cull_frequency=4作为参数传递给缓存后端,而一半(1/2)的条目需要传递cull_frequency=2。从本质上来说,cull_frequency是缓存必须被剔除的次数,以确保所有项目都被清除。

手动使用缓存

除了标准的站点范围和每个视图的缓存选项之外,直接使用缓存也很简单,存储特定的值,以便以后可以检索它们,而不必对不经常更改的数据执行昂贵的操作。这个低级 API 可以通过位于django.core.cachecache对象以通用形式获得。这个对象的大部分有用性来自三种方法— get()set()delete()—它们的工作方式与您的预期大致相同。

>>> cache.set('result', 2 ** 16 – 64 * 4)

>>> print cache.get('result')

65280

>>> cache.delete('result')

>>> print cache.get('result')

None

有一些关于这些方法的细节需要更多的解释,还有一些额外的方法被证明是有用的。下面是可用方法的完整列表,以及它们的功能细节。

  • CacheClass.set(key, value, timeout=None)—使用提供的key在缓存中设置指定的value。默认情况下,值从缓存中过期的超时时间由传递到CACHE_BACKEND设置中的超时时间决定,但是可以通过指定一个不同的超时时间作为该方法的参数来覆盖。
  • CacheClass.get(key, default=None)—该方法返回指定key的缓存中包含的值。通常,如果缓存中不存在关键字,cache.get()会返回None,但有时None是缓存中的有效值。在这些情况下,只需将default设置为某个不应该存在于缓存中的值,该值将被返回而不是None
  • CacheClass.delete(key)—删除与给定键相关的值。
  • CacheClass.get_many(keys)—给定一个键列表,它返回一个相应的键值列表。对于一些后端,像memcached,这可以提供一个比每个单独的键调用cache.get()更快的速度。
  • CacheClass.has_key(key)—如果指定的键在缓存中已经有值,则该方法返回True,如果该键未设置或已经过期,则返回False
  • CacheClass.add(key, value, timeout=None)—此方法仅尝试使用指定的值和超时时间向缓存添加新的键。如果给定的键已经存在于缓存中,此方法不会将缓存更新为新值。

使用缓存时,一个常见的习惯用法是首先检查缓存中是否已经存在某个值,如果不存在,则计算该值并将其存储在缓存中。然后,可以从缓存中检索该值,而不管它是否在缓存中,从而使代码变得简单明了。为了使这一点更加 Pythonic 化,cache对象的功能也有点像字典,支持in操作符作为has_key()方法的别名。

def get_complex_data(complex_data):

if 'complex-data-key' not in cache:

# Perform complex operations to generate the data here.

cache.set('complex-data-key', complex_data)

return cache.get('complex-data-key')

模板加载

虽然第六章展示了当一个视图或其他代码请求一个模板呈现时,它只是传入一个名称和一个相对路径,实际的模板检索是由特殊的加载器完成的,每个加载器以不同的方式访问模板。通过向TEMPLATE_LOADERS设置提供一个或多个导入路径,Django 不需要预先知道如何存储模板或者将模板存储在哪里。

Django 附带了三个模板加载器,代表了模板最常见的使用方式,在某些配置中从文件系统加载文件。当这些选项不够用时,添加您自己的模板加载器来以最适合您的环境的方式定位和检索模板是相当简单的。

这实际上是最容易编写的可插拔接口之一,因为它实际上只是一个函数。甚至没有任何关于函数应该调用什么的假设,更不用说它应该在哪个模块中,或者它应该属于哪个类了。TEMPLATE_LOADERS中的条目直接指向函数本身,因此不需要其他结构。

load_template_source(模板名,模板目录=无)

虽然加载器可以被称为任何东西,但是 Django 为它所有的模板加载器使用的名称是load_template_source,所以为了便于理解,通常最好坚持这个约定。这通常也放在它自己的模块中,但是同样,必须显式地提供导入路径,所以只要确保它的位置被很好地记录就行了。

第一个参数显然是要加载的模板的名称,它通常只是一个标准的文件名。这不必映射到一个实际的文件,但是视图通常会使用文件名请求模板,所以由模板加载器将这个名称转换成模板使用的任何引用。这可能是数据库记录、指向外部存储系统的 URL,或者您的站点可能用来存储和加载模板的任何东西。

load_template_source()的第二个参数是搜索模板时使用的目录列表。在 Django 内部,这通常是不提供的,所以使用默认的None,表明应该使用TEMPLATE_DIRS设置。使用文件系统的加载器应该始终遵循这种行为,以保持与其他模板加载器工作方式的一致性。如果加载器从其他地方获取模板,这个参数可以被忽略。

模板加载器内部发生的事情在不同的模板加载器之间会有很大的不同,这取决于每个加载器如何定位模板。一旦找到模板,加载器必须返回一个包含两个值的元组:一个字符串形式的模板内容,一个字符串表示找到模板的位置。第二个值用于为新的Template对象生成origin参数,这样如果出现问题,就很容易找到模板。

如果给定的名称与加载程序所知道的任何模板都不匹配,它应该引发TemplateDoesNotExist异常,如第六章中的所述。这将指示 Django 移动到列表中的下一个模板加载器,或者如果没有更多的加载器可以使用,就显示一个错误。

load_template_source

如果 Python 环境没有模板加载器运行的要求,Django 还为加载器提供了一种方式来表明它不应该被使用。如果模板加载程序依赖于尚未安装的第三方库,这将非常有用。给函数添加一个is_usable属性,设置为TrueFalse,将告诉 Django 是否可以使用模板加载器。

load_template(模板名,模板目录=无)

除了简单地加载模板的源代码之外,这个方法还负责返回一个能够被呈现的模板。默认情况下,从load_template_source()返回的源代码是由 Django 自己的模板语言处理的,但是这给了你一个机会用其他东西完全替换它。这仍然应该在内部使用load_template_source()方法来获取模板的代码,这样用户就可以将在哪里找到模板的决定与应该如何解释这些模板的决定分开。

返回值只需要一个方法就能正常工作:render(context)。这个render()方法接受一个模板上下文,并返回一个由模板源代码生成的字符串。这里传入的上下文很像一个标准字典,但是 Django 的上下文实际上是一堆字典,所以如果您打算将这个上下文传入另一个呈现的模板,您可能需要先将其展平为一个字典。

flat_dict = {}

for d in context.dicts:

flat_dict.update(d)

之后,您将拥有一个包含所有值的字典,这通常适用于大多数模板语言。

上下文处理器

当一个模板被渲染时,它会被传递一个变量上下文,用来显示信息和做出基本的表示决策。如果使用一种特殊类型的上下文RequestContext,它可以从django.template和标准的Context一起获得,Django 运行一个上下文处理器列表,每个处理器都有机会向模板的上下文添加新的变量。这不仅是向站点上使用的每个模板添加公共变量的好方法,而且是基于来自传入的HttpRequest对象的信息提供信息的非常简单的方法。

上下文处理器的接口非常简单;它只不过是一个标准的 Python 函数,将一个请求作为唯一的参数,并返回一个要添加到模板上下文中的数据字典。它不应该引发异常,如果不需要添加新的变量,根据指定的请求,它应该只返回一个空字典。这里有一个示例上下文处理器来添加一个包含请求用户 IP 地址的ip_address变量。

def remote_addr(request):

return {'ip_address': request.META['REMOTE_ADDR']}

Note

在代理和负载平衡器之后并不可靠,因为它的值将是代理的值,而不是真正的远程 IP 地址。如果您正在使用这些类型的软件,请确保使用适合您的环境的值。

安装上下文处理器就像在CONTEXT_PROCESSORS设置列表中添加一个字符串一样简单,每个条目都是一个完整的 Python 导入路径,包括它末尾的函数名。另外,记住上下文处理器只在使用RequestContext渲染模板时被调用。因为上下文处理器接受传入的请求作为参数,所以没有这个信息就无法调用它们。

应用技术

本章中描述的工具有许多不同的可用用途,但是有几个简单的例子可以说明它们如何很好地用于一些常见的需求。用一撮盐和一根欧芹把它们做成你自己的。如果事先不了解应用的工作环境,任何可以给出的例子,从定义上来说,都是相当抽象的,但是它们应该是如何很好地使用这些技术的一个很好的概述。

扫描传入文件中的病毒

对于允许用户上传文件以分发给其他用户的网站,人们非常信任这些传入文件的质量。与任何形式的用户输入一样,这种信息中肯定存在一定程度的不信任,因为总有人想对你的网站及其用户造成伤害。

当希望让用户共享特定类型的文件时,使用旨在理解这些文件的第三方库进行验证通常很容易。另一方面,共享任意文件打开了一个充满其他可能性的世界,其中许多会将您的站点及其用户置于风险之中。防病毒是这种应用安全性的重要组成部分,Django 的上传处理程序使这成为一项极其简单的任务。

对于这个例子,我们将使用一个优秀的开源病毒扫描应用,ClamAV, 8 ,它是为在服务器中使用而设计的,以及 pyclamd, 9 一个用于与 ClamAV 交互的 Python 库。总之,它们提供了一个易于使用的接口,可以在任何传入的文件被传递给应用的其他部分之前对其进行扫描。如果发现了病毒,可以在病毒对任何人造成伤害之前,立即将恶意文件从输入流中删除。

import pyclamd

from django.core.files import uploadhandler

from django.conf import settings

# Set up pyclamd to access running instance of clamavd, according to settings

host = getattr(settings, 'CLAMAV_HOST', 'localhost')

port = getattr(settings, 'CLAMAV_PORT', 3310)

pyclamd.init_network_socket(host, port)

class VirusScan(uploadhandler.FileUploadHandler):

def receive_data_chunk(self, raw_data, start): try:

if pyclamd.scan_stream(raw_data):

# A virus was found, so the file should

# be removed from the input stream.

raise uploadhandler.SkipFile()

except pyclamd.ScanError:

# Clam AV couldn't be contacted, so the file wasn't scanned.

# Since we can't guarantee the safety of any files

# no other files should be processed either.

raise uploadhander.StopUpload()

# If everything went fine, pass the data along

return raw_data

def file_complete(self, file_size):

# This doesn't store the file anywhere, so it should

# rely on other handlers to provide a File instance.

return None

您的应用可能有更具体的要求,比如向用户解释发现了哪种病毒,以及他们应该考虑在尝试与他人共享文件之前清理自己的系统。这个例子的关键是实现这种类型的行为有多容易,表面上看起来可能非常困难。

现在怎么办?

尽管有很多关于访问这些不同类型的后端协议的知识需要学习,但是很好地使用它们需要大量的想象力。关于如何以及为什么访问或替换这些低级接口,这样的书只能说这么多,所以由您来决定什么最适合您的环境和应用。

虽然本章讨论了如何使用和检修 Django 基础设施的主要部分,但有时只需要一个简单的实用程序来替换或避免大量冗余代码。了解两者的区别很重要,下一章将概述 Django 核心发行版中提供的许多基本实用程序。

Footnotes 1

http://prodjango.com/pep-249/

2

http://prodjango.com/db2/

3

http://prodjango.com/ipv4/

4

http://prodjango.com/ipv6/

5

http://prodjango.com/caching/

6

http://prodjango.com/uri/

7

http://prodjango.com/memcached/

8

http://prodjango.com/clamav/

9

http://prodjango.com/pyclamd/

九、常用工具

Abstract

虽然 Django 的目标是为您构建自己的 Web 应用提供一个基础,但是该框架有自己的基础,将所有这些联系在一起。这些通用工具和特性有助于一切保持一致并更易于维护,您自己的应用也可以利用这些好处。毕竟,Django 中可用的东西对任何使用它的人都是可用的。

虽然 Django 的目标是为您构建自己的 Web 应用提供一个基础,但是该框架有自己的基础,将所有这些联系在一起。这些通用工具和特性有助于一切保持一致并更易于维护,您自己的应用也可以利用这些好处。毕竟,Django 中可用的东西对任何使用它的人都是可用的。

核心异常(django.core.exceptions)

虽然 Python 自带了一组可以在各种情况下引发的异常,但 Django 在此基础上引入了足够的复杂性,值得进一步研究。由于 Django 服务于特定的受众,这些异常更加专门化,但是它们仍然可以被核心代码之外的其他代码使用。前面已经提到了其中一些异常,因为它们更具体地处理了一个特定的 Django 特性,但是它们在其他情况下也很有用,下面几节将对此进行解释。

配置不正确

这是大多数新用户遇到的第一个异常,因为当应用的模型设置不正确、找不到视图或发生许多其他常见配置错误时,就会出现这个异常。它通常在执行manage.py validation的过程中出现,帮助用户识别和纠正发现的任何错误。

并非所有的应用都需要任何特定的配置,但是那些需要的应用可以很好地利用这个例外,因为大多数用户以前都见过它。这可能有用的常见情况包括缺少或不正确的设置、使用的 URL 配置没有附带的INSTALLED_APPS条目、自定义模型字段的参数无效以及缺少必需的第三方库。

要记住的最重要的事情是,不仅要指出出错的地方,还要指出用户应该如何修复它。通常情况下,异常表明某些代码出现了错误,并且几乎没有办法通知用户如何修复它。然而,对于一个应用的配置,有有限数量的可接受的方式来设置它,并且这个错误应该被用作将用户引向正确方向的一种方式。

例如,如果一个应用被设计为处理音频文件,它可能需要诱变剂的存在, 1 一个完善的 Python 库,用于从这样的文件中提取信息。在models.py的顶部简单导入这个库,可能会用到它,可以识别这个库是否安装正确,如果不正确,指导用户如何操作。

from django.core.exceptions import ImproperlyConfigured

try:

import mutagen

except ImportError:

raise ImproperlyConfigured("This application requires the Mutagen library.")

未使用的中间件

第七章描述了如何使用中间件来调整 HTTP 的处理方式,但是一个有趣的副作用是并不是所有的中间件都是有用的。虽然每个项目都可以选择通过MIDDLEWARE_CLASSES设置来设置那些必要的中间件,但是开发和生产之间或者不同开发人员的计算机之间仍然存在差异。

每个中间件都有能力决定其环境是否适合使用,并指出是否存在问题。当第一次需要时,中间件类被自动实例化,在第一个请求开始时,这是检查发生的地方。通过覆盖该类的__init__()方法,中间件可以立即检查是否一切都设置好了,以正常工作并做出相应的反应。

具体来说,这种反应是,如果一切正常,不做任何事情就返回,或者提高MiddlewareNotUsed。如果被抛出,Django 将总是捕捉到这个异常,并认为这个异常意味着这个类应该从每个请求所应用的中间件列表中删除。

这是一个很重要的区别,因为不能告诉 Django 完全不要使用中间件,而是由每个单独的方法来决定它是否应该执行。虽然这可以工作,但它会在每个请求上占用宝贵的时间和内存,检查一些只能确定一次的东西。通过将中间件完全排除在列表之外,它根本不会消耗任何额外的周期或内存。

返回了多个对象

当从数据库中检索对象时,通常希望只返回一行。每当查询是主键时,情况总是如此,但是在某些应用中,slugs(甚至可能是日期)可以是唯一的。Django 用 QuerySet 的get()方法支持这种情况,如果它匹配多个结果,它就可以中断应用的整个执行。

Note

Django 的SlugField几乎总是被设置为unique=True,因为它用于标识 URL 中的对象。

由于get()应该从数据库中返回一条记录,所以匹配多条记录的查询会被标记为异常MultipleObjectsReturned。其他类型的查询不会出现这种情况,因为在大多数情况下会出现多条记录。捕捉此异常在许多方面都很有用,从显示更有用的错误信息到移除意外的重复项。

ObjectDoesNotExist

get()期望的另一面是总是返回一行;也就是说,要想成功,总要有一个行。如果一个期望某行存在的查询发现没有这样的行,Django 相应地用ObjectDoesNotExist响应。它的工作方式与MultipleObjectsReturned非常相似,不同之处仅在于它被举起的位置。

简称为DoesNotExist,这个子类避免了额外的导入,因为当调用get()方法时,使用它的类通常已经被导入了。此外,通过被称为DoesNotExist并作为模型类的属性,它看起来像是完全可读的英语:Article.DoesNotExist

权限被拒绝

大多数应用都有某种形式的权限来防止对受限资源的访问;这遵循规则的模式,但有例外。规则是试图访问资源的用户将确实拥有正确的权限,因此任何没有正确权限的用户都将导致一个异常—这次是PermissionDenied。这是一种指出问题并停止处理视图其余部分的便捷方式,因为如果用户没有正确的权限,视图本身可能会做出无效的更改。

Django 还在其请求处理程序中自动捕获这个异常,将它用作返回 HTTP 403 Forbidden响应而不是通常的200 OK的指令。这将向客户端表明所提供的凭证没有足够的权限来请求资源,并且用户不应该在没有纠正这种情况的情况下重试。Django 自己的管理应用默认提供了这种行为,但也可以在任何其他应用中使用。

像其他异常一样,PermissionDenied既可以被引发也可以被捕获,尽管返回一个特殊 HTTP 响应代码的默认行为在大多数情况下是合适的。如果需要一些其他行为,很容易创建一个中间件,在process_view()阶段捕获这个异常,可能会将用户重定向到一个表单,在那里他们可以联系站点管理员,请求访问页面的权限。

from django.core.exceptions import PermissionDenied

from django.http import HttpResponseRedirect

from django.core.urlresolvers import reverse

class PermissionRedirectMiddleware(object):

def __init__(self, view='request_permission', args=None, kwargs=None):

self.view = view

self.args = args or ()

self.kwargs = kwargs or {}

def process_view(self, request, view, args, kwargs):

try:

response = view(request, *args, **kwargs)

except PermissionDenied:

url = reverse(self.view, args=self.args, kwargs=self.kwargs)

return HttpResponseRedirect(url)

如第七章所述,在MIDDLEWARE_CLASSES中添加一个对此的引用或使用decorator_from_middleware()创建一个装饰器,当用户的权限对原始请求无效时,只需将用户重定向到另一个页面。即使没有这个异常的自定义处理程序,但是在用户不满足适当权限的任何视图中提出这个异常也是非常有用的。这种反应会导致所有其他类似情况下的处理方式,帮助你的网站尽可能的有凝聚力和一致性。

可疑操作

虽然用户通常会遵守规则,按照预期的方式使用你的网站,但任何合理的开发者都会为那些不遵守规则的人做好准备。Django 采取了许多预防措施来防止对管理接口之类的东西的未经授权的访问,并提供了装饰器来限制对应用视图的访问,但是还需要考虑一些更微妙的事情。

例如,sessions 框架需要担心用户为了劫持另一个用户的会话而修改会话 ID。这些类型的事情本身不属于身份验证或权限范围,而是用户试图规避这些通常的保护。识别这种情况何时发生很重要,这样就可以适当地处理它。

为了全面识别这些问题,Django 提供了一个SuspiciousOperation异常,这种异常可以在任何时候发生。在许多情况下,这是在同一个应用中被抛出和捕获的,但是提供它是为了能够进入应用并只使用引发异常的部分。在其他情况下,它被暴露给其他应用,以最有意义的方式进行处理。

第七章中的签名 cookies 应用是一个很好的例子,说明可疑活动可以很容易地被识别和处理。如果一个 cookie 没有有效的签名,很明显有可疑的事情发生,签名验证代码会产生一个SuspiciousOperation来表示它。因为它被设计成一个无需干预的中间件,所以它还提供了代码来捕捉这个异常,并通过在请求到达视图之前从请求中删除有问题的 cookie 来执行一个更有用的功能。但是由于其他应用有可能在中间件之外签名和验证值,所以提出一个准确识别正在发生什么的异常是有用的。

验证错误

模型和表单都可以在进一步处理数据之前验证数据,当数据无效时,Django 会抛出一个ValidationError。这在任何需要验证数据的时候都很有用,即使是在那些上下文之外。例如,如果您有一个处理 JSON 数据的应用,您可能希望提供不同于模型和表单工作方式的验证,并且除了单个字段之外,您还可能希望验证整个对象。您可以通过重用其他领域中使用的相同的ValidationError来保持与 Django 的一致性。

当实例化一个ValidationError时,可以传入一些不同种类的对象,通常是指无效的数据。通常,您会传入一个可打印的对象,比如一个字符串或者可以使用__str__()方法转换成字符串的东西。您还可以传入一个这样的对象列表,或者一个键和值都可以打印的字典,这允许您将几个错误合并到一个异常中。当在这些情况下打印ValidationError时,它的内部代码会自动执行必要的强制,以确保得到字符串。

Note

字典列表的特殊处理仅限于列表和字典。其他类型的序列和映射将被视为标准的可打印对象,而不需要查看它的单个值。此外,它只会查看第一层的数据,所以如果您将数据嵌套在一个列表中,例如,Django 只会将值从外部列表中取出;任何内部列表都将被强制转换为字符串。

视图不存在

在解析 URL 时,很可能传入的 URL 匹配 URL 配置中的模式,但不匹配任何已知的视图。这可能有多种原因,包括一个真正丢失的视图,但也经常是由于一个错误导致视图没有被正确加载。毕竟,Django 只有在 Python 可以解析视图并将其作为函数加载的情况下,才能识别出合适的视图。当这些情况发生时,Django 会抛出ViewDoesNotExist来尽可能地指出哪里出错了。

通常不需要手动捕捉这个错误或对它做任何特殊处理,因为 Django 已经尽可能好地处理了它。在开发中,使用DEBUG=True,它会显示一个有用的错误页面,详细说明尝试了哪个视图,以及一条 Python 错误消息,指出为什么不能加载它。在生产中,这种详细程度是不安全的,所以它退回到标准的 HTTP 500 错误,通知后台的管理员。

Text Modification (django.utils.text)

从本质上讲,网络是一种书面媒体,使用文本来传达绝大多数的想法。通常,这些文本是作为模板和数据库内容的组合提供的,但是在发送给用户之前,通常需要进行一些处理。在标题中使用时可能需要大写,在电子邮件中使用时需要换行,或者进行其他修改。

压缩字符串

这个简单的实用程序使用 gzip 格式压缩输入字符串。这允许您以浏览器能够在另一端解压缩的格式传输内容。

>>> from django.utils.text import compress_string

>>> compress_string('foo')

'\x1f\x8b\x08\x00s={Q\x02\xffK\xcb\xcf\x07\x00!es\x8c\x03\x00\x00\x00'

很明显,这个例子看起来没有被压缩,但这仅仅是压缩如何处理小字符串的一个假象。在这些情况下,压缩算法所需的头和簿记足以使字符串更长。当您提供一个更长的字符串时,比如一个文件或一个呈现的模板,您将在这个函数的输出中看到一个更小的字符串。

Note

如果您使用它向浏览器发送内容,您还需要发送一个头来告诉浏览器如何处理它。

Content-Encoding: gzip

压缩序列(序列)

这与compress_string()非常相似,但是它将按照提供的顺序压缩单个项目。与其简单地返回所有压缩内容的字符串,compress_sequence()实际上是一个生成器,逐段生成内容。输出中的第一项是 gzip 头,接下来是每个输入字符串的压缩版本,最后是 gzip 页脚。

>>> for x in text.compress_sequence(['foo', 'bar', 'baz']):

...     print repr(x)

...

'\x1f\x8b\x08\x00\x16={Q\x02\xff'

'J\xcb\xcf\x07\x00\x00\x00\xff\xff'

'JJ,\x02\x00\x00\x00\xff\xff'

'JJ\xac\x02\x00\x00\x00\xff\xff'

"\x03\x00\xaa'x\x1a\t\x00\x00\x00"

get_text_list(items,last_word='or ')

向用户显示项目列表有多种方式,每种方式适用于不同的情况。不要在每一行中列出每一项,用简单的英语将列表显示为逗号分隔的列表,例如“红、蓝、绿”,通常是有用的这似乎是一项艰巨的任务,但是get_text_list()大大简化了它。只需传入一个条目列表作为第一个参数,并传入一个可选的连接词作为第二个参数,它将返回一个字符串,其中包含由逗号分隔的条目,并在末尾加上连接词。

>>> from django.utils.text import get_text_list

>>> 'You can use Python %s' % get_text_list([1, 2, 3])

u'You can use Python 1, 2 or 3'

>>> get_text_list(['me', 'myself', 'I'], 'and')

u'me, myself and I'

javascript_quote(s,quote_double_quotes=False)

当将字符串写出到 JavaScript 时,无论是在源代码中还是在 JavaScript 对象表示法(JSON)、 2 的响应代码中,对于特殊字符都必须考虑某些因素。这个函数以 JavaScript 可以理解的方式正确地转义这些特殊字符,包括 Unicode 字符。

>>> from django.utils.text import javascript_quote

>>> javascript_quote('test\ning\0')

'test\\ning\x00'

normalize_newlines(文本)

当应用需要处理来自未知来源的文本内容时,很可能会在 Windows、Apple 和 Unix 风格的系统上生成输入。这些不同的平台对于使用什么字符来编码行尾有不同的标准,当应用需要对它们进行任何文本处理时,这会导致问题。给定这样的输入,normalize_newlines()寻找常见的行尾替换,并将它们全部转换成 Python 期望的 Unix 风格的\n

>>> from django.utils.text import normalize_newlines

>>> normalize_newlines(u'Line one\nLine two\rLine three\r\nLine four')

u'Line one\nLine two\nLine three\nLine four'

电话 2 数字(电话)

企业通常将电话号码作为单词来提供,以便于记忆。如果像这样的电话号码作为应用的输入提供,它们通常只有在直接显示给用户时才有用。如果应用必须将这些数字作为自动化系统的一部分使用,或者向经常打电话的员工显示这些数字,那么将它们作为原始数字而不是营销文本使用会更有用。通过用phone2numeric()传递电话号码,你可以确保你总是得到一个真实的电话号码来工作。

>>> from django.utils.text import phone2numeric

>>> phone2numeric(u'555-CODE')

u'555-2633'

资本重组(文本)

给定一个可能已经转换为小写的字符串,可能是为了搜索或其他比较,通常需要在向用户显示之前将其转换回常规的大小写混合。recapitalize()函数就是这样做的,将句尾标点符号后面的字母大写,比如句号和问号。

>>> from django.utils.text import recapitalize

>>> recapitalize(u'does this really work? of course it does.')

u'Does this really work? Of course it does. '

Caution

尽管 Django 为国际观众提供了许多功能,但recapitalize()功能只适用于基本的英文文本。其他语言中使用的标点符号可能无法正确识别,导致大写输出不正确。

slugify(值)

Slugs 是一种适合在 URL 中使用的字符串,通常是文章标题的精简版本。Slugs 由小写字母、代替空格的连字符以及缺少标点符号和其他非单词字符组成。slugify()函数接受一个文本值,并执行必要的转换,使其适合用作 URL slug。

>>> from django.utils.text import slugify

>>> slugify(u'How does it work?')

u'how-does-it-work'

智能拆分(文本)

最初是作为一种解析模板标签参数的方法开发的,smart_split()获取一个字符串并在空格处将其分开,同时仍然完整地保留引用的段落。这是为任何其他应用解析参数的好方法,因为它提供了很大的灵活性。它可以识别单引号和双引号,安全地处理转义引号,并在遇到任何引用段落的开头和结尾保持引号不变。

>>> from django.utils.text import smart_split

>>> for arg in smart_split('arg1 arg2 arg3'):

...     print arg

arg1

arg2

arg3

>>> for arg in smart_split('arg1 "arg2\'s longer" arg3'):

...     print arg

arg1

"arg2's longer"

arg3

unescape_entities(文本)

HTML 可以包含一些实体,这些实体可以更容易地表示某些国际字符和其他特殊字形,这些字符很难在大多数英文键盘上键入或使用本地英文字符编码进行传输。这些在手动编辑 HTML 时很有用,但是如果您使用像 UTF-8 这样的宽文本编码,您可以通过网络发送原始字符,而不是依赖浏览器在事后转换它们。通过将字符串传递给这个函数,任何 HTML 实体都将被转换为适当的 Unicode 码位。

>>> from django.utils.text import unescape_entities

>>> unescape_entities('&ldquo;Curly quotes!&rdquo;')

u'\u201cCurly quotes!\u201d'

unescape_string_literal

当编写包含撇号或引号的字符串时,您通常需要通过在它们前面放置反斜杠来转义这些字符,以避免意外地使用它们来终止字符串。因为为此目的使用了反斜杠,所以还需要对字符串中要包含的任何文字反斜杠进行转义。

通常,Python 会直接解释这些,并为您提供一个包含原始字符的字符串,没有额外的反斜杠。在某些情况下,比如在模板中,字符串不是由 Python 直接处理的,而是作为包含反斜杠的字符串传递到代码中。您可以使用unescape_string_literal()获得 Python 通常会提供的等价字符串。

>>> from django.utils.text import unescape_string_literal

>>> unescape_string_literal("'string'")

'string'

>>> unescape_string_literal('\'string\'')

'string'

换行(文本,宽度)

这将获取指定的文本,并根据需要插入换行符,以确保没有一行超出所提供的宽度。它确保不分解单词,并且保持现有的换行符不变。不过,它希望所有的换行符都是 Unix 风格的,所以如果您不能控制文本的来源,最好先运行文本,以确保它正常工作。

>>> from django.utils.text import wrap

>>> text = """

... This is a long section of text, destined to be broken apart.

... It is only a test.

... """

>>> print wrap(text, 35)

This is a long section of text

destined to be broken apart.

It is only a test.

截断文本

另一个常见的需求是截断文本以适应更小的空间。无论您是限制字数还是字符数,也无论您是否需要在截断时考虑 HTML 标签,Django 都有一个Truncator类可以完成这项工作。您可以通过简单地传递您想要截断的文本来实例化它。

>>> from django.utils.text import Truncator

为了这个例子,我们必须首先配置 Django 不要使用它的国际化系统。如果您使用的是manage.py shell,这已经为您完成了,但是如果您只是在项目之外使用 Python,您将需要对此进行配置。在实际的应用中,您不需要执行这个步骤。

>>> from django.conf import settings

>>> settings.configure(USE_I18N=False)

现在我们有了一个能够像这样处理文本转换的环境。

>>> truncate = Truncator('This is short, but you get the idea.')

从那里,实际的操作由任何可用的方法提供。

Truncator.chars(num,truncate='…')

此方法将文本限制为不超过所提供的数量,而不考虑原始文本中的任何单词或句子。

>>> truncate.chars(20)

u'This is short, bu...'

Note

结果字符串有 20 个字符长,包括省略号。如您所见,truncate参数可以改变字符串末尾使用的字符数,而chars()在决定保留多少字符串时会考虑到这一点。对于truncate值的不同设置将改变原始字符串的剩余部分。这个行为是chars()方法独有的。

truncate参数指定结果字符串应该如何格式化。默认情况下,它会在后面附加三个句点,起到省略号的作用。您可以提供任何其他字符串,它将被追加到截断的字符串,而不是句点。

>>> truncate.chars(20, truncate='--')

u'This is short, but--'

您还可以通过使用名为truncated_text的占位符来指定格式字符串,从而更加灵活地控制文本输出。这允许您将截断的文本放在字符串中的任何位置。

>>> truncate.charts(20, truncate='> %(truncated_text)s...')

u'> This is short, ...'

Truncator.words(num,truncate='…',html=False)

此方法将字符串的长度限制为指定的字数,而不是单个字符。这通常是更可取的,因为它避免了在单词中间断开。因为单词可以有不同的长度,所以产生的字符串比使用chars()时更难预测。

>>> truncate.words(5)

u'This is short, but you...'

>>> truncate.words(4, truncate='--')

u'This is short, but--'

还要注意,truncate参数不再改变字符串被截断的方式。您的文本将减少到指定的字数,之后将应用truncate参数。

html参数控制该方法是否应该避免将 HTML 属性作为单独的单词,因为它们由空格分隔。对于普通文本,默认的False更好,因为它需要做的工作更少,但是如果你要输出一个可能包含 HTML 标签的字符串,你会想要使用True来代替。

>>> truncate = Truncator('This is <em class="word">short</em>, but you get the idea.')

>>> truncate.words(4)

u'This is <em class="word">short</em>,...'

>>> truncate.words(4, html=True)

u'This is <em class="word">short</em>, but...'

>>> truncate.words(3)

u'This is <em...'

>>> truncate.words(3, html=True)

u'This is <em class="word">short</em>,...'

使用html=True的另一个优点是它会小心地关闭标签,否则当字符串被截断时标签会保持打开。

>>> truncate = Truncator('This is short, <em>but you get the idea</em>.')

>>> truncate.words(5)

u'This is short, <em>but you...'

>>> truncate.words(5, html=True)

u'This is short, <em>but you...</em>'

数据结构(django.utils.datastructures)

在处理任何复杂的系统时,经常需要处理非常特殊结构的数据。这可能是一个项目的顺序列表,一个键到值的映射,一个类别的层次树,这些的任意组合或者其他完全不同的东西。虽然 Django 并没有假装为应用可能需要的每种数据安排提供对象,但是框架本身需要一些特定的东西,并且这些东西也对基于它的所有应用可用。

字典包装

这是一个为特定目的设计的数据结构的好例子,在现实世界中可能有其他用途。这种特殊类型的字典的目标是,如果请求的键与一个基本标准匹配,就提供一种在检索时转换值的方法。

在实例化字典时,可以提供一个函数和一个前缀字符串。每当您请求一个以前缀开头的键时,DictWrapper将去掉前缀,并在返回它之前对相关的值调用提供的函数。除此之外,它就像一个标准的字典。

>>> from django.utils.datastructures import DictWrapper

>>> def modify(value):

...     return 'Transformed %s' % value

>>> d = DictWrapper({'foo': 'bar'}, modify, 'transformed_')

>>> d['foo']

'bar'

>>> d['transformed_foo']

'Transformed: bar'

不变列表

列表和元组之间的区别通常用它们的内容来描述。列表可以包含任意数量的对象,所有对象都应该是相同的类型,这样您就可以遍历它们,并像处理所有其他项目一样处理每个项目。本质上,列表是值的集合。

另一方面,元组本身是一个整数值,其中的每一项都有特定的含义,由它的位置来表示。任何特定类型的元组都有相同数量的值。例如,空间中的三维点可以由包含 x、y 和 z 坐标的 3 项元组来表示。每个这样的点都有相同的三个值,并且总是在相同的位置。

两者之间的一个关键技术区别是元组是不可变的。为了改变一个元组,实际上需要用改变后的值创建一个新的元组。这种不变性是一个有用的安全网,可以确保序列不会在您的控制下发生变化,而且它还可以略微提高性能,因为元组是更简单的数据结构。但是,它们并不打算用作不可变的列表。

对于那些既有列表语义,又想获得不变性好处的情况,Django 提供了一个替代方案:ImmutableList。它是 tuple 的一个子类,但是它也包含了列表中所有可变的方法。唯一的区别是这些方法都引发一个AttributeError,而不是改变值。这是一个微妙的区别,但它确实给你机会利用元组,同时仍然使用列表的语义。

MergeDict

当需要一起访问多个字典时,典型的方法是创建一个新字典,其中包含这些字典的所有键和值。这适用于简单的应用,但是可能有必要保持底层字典的可变性,以便对它们的更改反映在组合字典中。下面展示了标准字典是如何分解的。

>>> dict_one = {'a': 1, 'b': 2, 'c': 3}

>>> dict_two = {'c': 4, 'd': 5, 'e': 6}

>>> combined = dict(dict_one, **dict_two)

>>> combined['a'], combined['c'], combined['e']

(1, 4, 6)

>>> dict_one['a'] = 42

>>> combined['a']

1

这说明了一种简单的合并字典的方法,利用了dict()可以接受字典和关键字参数的事实,将它们合并成一个新的字典。多亏了在第二章中详细描述的**语法,这使得它成为了一种获得期望结果的便捷方式,但是这个例子也显示了它从哪里开始失败。

第一,它只接受两本词典;添加更多将需要调用dict()不止一次,每次添加一个新的字典。也许更重要的是,对源字典的更新不会反映在组合结构中。清楚地说,这通常是一件好事,但是在像组合了request.GETrequest.POSTrequest.REQUEST这样的情况下,对底层字典所做的更改也应该显示在组合输出中。

为了方便这一切,Django 使用了自己的类,该类在许多方面都像字典一样,但是在后台透明地访问多个字典。通过这种方式可以访问的词典数量没有限制。在实例化对象时,只需根据需要提供尽可能多的字典,它们将按照提供的顺序被访问。因为它存储对真实字典的引用并访问它们,而不是创建一个新的字典,所以对底层字典的修改会反映在组合中。

>>> from django.utils.datastructures import MergeDict

>>> dict_one = {'a': 1, 'b': 2, 'c': 3}

>>> dict_two = {'c': 4, 'd': 5, 'e': 6}

>>> combined = MergeDict(dict_one, dict_two)

>>> combined['a'], combined['c'], combined['e']

(1, 3, 6)

>>> dict_one['a'] = 42

>>> combined['a']

42

因为键在内部字典中的检查顺序与它们被传递给MergeDict的顺序相同,所以在第二个例子中combined['c']3,而在第一个例子中是4

多元价值预测

在另一个极端,有时让字典中的每个键潜在地引用多个值是有用的。由于 Web 浏览器将数据作为一系列名称/值对发送到服务器,没有任何更正式的结构,因此一个名称可能会被发送多次,每次可能会有不同的值。字典被设计成将一个名字映射到一个值,所以这是一个挑战。

从表面上看,解决方案似乎很简单:只需在每个键下存储一个值列表。再深入一点,一个问题是绝大多数应用对每个键只使用一个值,所以总是使用一个列表会让每个人做更多的工作。相反,大多数情况下应该能够使用单个键来访问单个值,同时仍然允许那些需要它们的应用访问所有的值。

Django 使用MultiValueDict来处理这种情况,基于大多数其他框架在这种情况下的默认行为。默认情况下,访问一个MultiValueDict中的键会返回以该名称提交的最后一个值。如果所有的值都是必需的,那么一个单独的getlist()方法可以返回完整的列表,即使它只包含一项。

>>> from django.utils.datastructures import MultiValueDict

>>> d = MultiValueDict({'a': ['1', '2', '3'], 'b': ['4'], 'c': ['5', '6']})

>>> d['a'], d['b'], d['c']

('3', '4', '6')

>>> d.getlist('a')

['1', '2', '3']

>>> d.getlist('b')

['4']

>>> d.getlist('c')

['5', '6']

Caution

这不会自动将每个值强制转换为一个列表。如果为任何值传入单个项,该值将按预期返回,但getlist()将返回传入时的原始值。这意味着getlist()将只返回单个项目,而不是包含单个项目的列表。

>>> d = MultiValueDict({'e': '7'})

>>> d['e']

'7'

>>> d.getlist('e')

'7'

排序直接

Python 字典的一个更难理解的特性是它们在技术上是无序的。检查各种各样的字典可能看起来会产生一些模式,但是它们是不可靠的,因为它们在 Python 实现之间会有所不同。这有时会是一个相当大的绊脚石,因为很容易意外地依赖字典的隐式排序,只在你最意想不到的时候发现它从你的下面改变了。

需要一个可靠有序的字典是很常见的,这样 Python 代码和模板在遇到字典时就能知道会发生什么。在 Django 中,这个特性是由SortedDict提供的,它跟踪它的键被添加到字典中的顺序。利用该功能的第一步是传入一个有序的键/值对序列。然后保留这个顺序,以及任何后续键被赋予新值的顺序。

>>> from django.utils.datastructures import SortedDict

>>> d = SortedDict([('c', '1'), ('d', '3'), ('a', '2')])

>>> d.keys()

['c', 'd', 'a']

>>> d.values()

['1', '3', '2']

>>> d['b'] = '4'

>>> d.items()

[('c', '1'), ('d', '3'), ('a', '2'), ('b', '4')]

功能实用程序(django.utils.functional)

Python 将函数视为一级对象。它们有一些明显不同于其他对象的属性和方法,但是核心语言对待它们就像对待其他对象一样。这种处理允许函数的一些非常有趣的用法,比如在运行时设置属性和在一个列表中组装函数,以便按顺序执行。

cache _ property(func)

属性是最简单的描述符之一,因为在访问属性时,通常情况下只需调用一个方法。如果它依赖于其他属性或外部因素,这对于确保其值始终是最新的非常有用。每次访问属性时,都会调用方法并产生一个新值。

>>> class Foo(object):

...     @property

...     def bar(self):

...         print('Called the method!')

...         return 'baz'

...

>>> f = Foo()

>>> f.bar

Called the method!

'baz'

>>> f.bar

Called the method!

'baz'

不过,有时候,你有一个不会改变的价值,但它的生产成本可能会很高。如果不需要,你不想产生这个值,但是你也不想产生一次以上。为了解决这种情况,您可以使用@cached_property装饰器。将此应用于方法将导致该方法在第一次访问属性时被调用,但它会将结果存储在对象上,以便每次后续访问都将只获得存储的值,而不是再次调用该方法。

>>> from django.utils.functional import cached_property

>>> class Foo(object):

...     @cached_property

...     def bar(self):

...         print('Called the method!')

...         return 'baz'

...

>>> f = Foo()

>>> f.bar

Called the method!

'baz'

>>> f.bar

'baz'

咖喱(func)

通常有必要采用一个带有复杂参数集的函数并对其进行简化,这样调用它的代码就不需要总是提供所有的参数。最明显的方法是尽可能提供默认值,如第二章所述。然而,在许多情况下,在编写函数时没有合理的默认值,或者默认值可能不适合情况的需要。通常,您可以使用您需要的任何参数值来调用该函数,这对于大多数需求来说都很好。

不过,有时函数的参数是在与实际需要调用时不同的时间确定的。例如,传递一个函数以便以后使用是很常见的,无论是作为实例方法还是回调,甚至是模块级的函数。当使用的函数接受的参数多于以后提供的参数时,必须提前指定剩余的参数。

从 Python 2.5 开始,这个功能通过functools.partial函数在标准库中提供。虽然与 Python 捆绑在一起很方便,但它只对后续安装有用,而 Django 支持已经存在很久的 Python 版本。相反,Django 在django.utils.functional.curry提供了自己的实现。

curry 的第一个参数总是一个 callable,它不会马上被调用,而是被藏起来以后用。除此之外,所有的位置和关键字参数也被保存,并在适当的时候应用于提供的 callable。返回值是一个新的函数,当被调用时,它将使用原始参数和随后调用中提供的任何参数来执行原始的可调用函数。

>>> from django.utils.functional import curry

>>> def normalize_value(value, max_value, factor=1, comment='Original'):

...     """

...     Normalizes the given value according to the provided maximum

...     scaling it according to factor.

...     """

...     return '%s (%s)' % (float(value) / max_value * factor, comment)

>>> normalize_value(3, 4)

'0.75 (Original)'

>>> normalize_value(3, 4, factor=2, comment='Double')

'1.5 (Double)'

>>> percent = curry(normalize_value, max_value=100, comment='Percent')

>>> percent(50)

'0.5 (Percent)'

>>> percent(50, factor=2, comment='Double')

'1.0 (Double)'

>>> tripled = curry(normalize_value, factor=3, comment='Triple')

>>> tripled(3, 4)

'2.25 (Triple)'

惰性(func,*resultclasses)

根据环境的不同,有些值可以用不同的方式表示。一个常见的例子是可翻译的文本,其中的内部值通常是英语,但也可以使用用户选择的不同语言来表示。具有这种行为的对象被认为是懒惰的,因为它们不会立即被填充,而是在以后需要的时候被填充。

您可以使用这个lazy()函数创建一个惰性对象。它接受的主要参数是一个可以产生最终值的函数。该函数不会被立即调用,而是简单地存储在一个Promise对象中。然后这个承诺可以在整个框架代码中传递,比如 Django,它不关心对象是什么,直到它最终到达关心对象的代码。当试图访问承诺时,将调用该函数并返回值。事实上,每次访问对象时都会调用该函数,每次都有机会使用环境来改变返回值。

这个过程中有趣的部分是承诺如何决定它是否被访问。当简单地传递一个对象时,该对象本身无法访问保存对它的引用的代码。但是,当它的属性被访问时,它可以做出反应。因此,当您的代码试图访问一个承诺的属性时,这将成为生成承诺值的新表示的线索。

lazy()函数的其余参数有助于这部分过程。您指定的resultclasses应该包含您的函数可以返回的所有不同类型的对象。每个类都有一组属性和方法,promise 可以监听这些属性和方法。当其中任何一个被访问时,promise 将调用它的存储函数返回一个新值,然后返回最初请求的那个值的属性。

如果没有例子,这可能特别难以理解。翻译是一个常见的例子,但是另一个有用的例子是在处理日期和时间时。具体来说,社交网络通常会根据事件发生的时间来显示特定事件的日期和时间,而不是绝对日期。Django 有一个实用程序可以立即计算这个值,但是您也可以用它来创建一个 lazy 对象。然后,每次显示时,您的代码可以根据需要计算时差。

就像我们之前看到的,这个例子要求我们首先配置 Django 不要使用它的国际化系统,如果你没有使用manage.py shell

>>> from django.conf import settings

>>> settings.configure(USE_I18N=False)

现在系统被配置为使用timesince()功能。位于django.utils.timesince中,您可以简单地传入一个datedatetime对象,它将返回一个字符串,该字符串包含从现在到您传入的日期之间的时间长度的可读表示。

>>> import datetime

>>> from django.utils.timesince import timesince

>>> then = datetime.datetime.now() - datetime.timedelta(minutes=1)

>>> since = timesince(then)

>>> since

u'1 minute'

>>> print(since)

1 分钟

这就是它通常的工作方式,立即返回持续时间。然后剩下一个只有在函数被调用时才有效的字符串。惰性对象在需要的时候会像字符串一样工作,但是在需要产生值的时候会计算函数。

>>> from django.utils.functional import lazy

>>> lazy_since = lazy(timesince, str)(then)

>>> lazy_since

<django.utils.functional.__proxy__ at 0x...>

>>> print(lazy_since)

1 minute

# Wait a few minutes...

>>> print(lazy_since)

5 minutes

allow_lazy(func,* resultclasses)

这个装饰器提供了另一种处理懒惰选项的方法,就像上一节中描述的那样。大多数函数在实际对象上操作,不知道任何关于懒惰对象的延迟加载行为,并且将直接访问对象的属性。如果向这样的函数提供一个惰性对象,它会立即触发值,如果函数只是简单地转换值,这可能不是很有用。

>>> def bold(value):

...     return u'<b>%s</b>' % value

...

>>> bold(lazy_since)

u' 10 分钟'如果新的函数调用也可以是惰性的就更好了,如果你能在不改变函数代码的情况下做到这一点就更好了。这就是allow_lazy()发挥作用的地方。您可以将此应用于任何函数,这样当您调用该函数时,它将检查是否有任何传入的参数是惰性的。如果它们中的任何一个实际上是懒惰对象,包装器将介入并返回一个由原始函数支持的新的懒惰对象。否则,原始函数将立即在提供的非惰性参数上运行。

>>> from django.utils.functional import allow_lazy

>>> lazy_bold = allow_lazy(bold, str)

>>> lazy_bold(lazy_since)

<django.utils.functional.__proxy___ at 0x...>

>>> lazy_bold(since)

u'<b>1 minute</b>'

>>> print lazy_bold(lazy_since)

u'<b>2 minutes</b>

lazy_property(fget=None,fset=None,fdel=None)

属性是围绕简单属性访问包装自定义行为的一种非常有用的方式。例如,您可以使用属性按需生成属性值,或者在属性值更改时更新相关信息。然而,它们的一个潜在问题是,当它们第一次被添加到一个类中时,它们包装了特定的函数。子类可以继承每个属性的行为,但是它总是使用提供给原始装饰者的函数。子类可以覆盖属性行为的唯一方法是创建一个全新的属性,完全替换属性的每个方面。

>>> class Foo(object):

...     def _get_prop(self):

...         return 'foo'

...     prop = property(_get_prop)

...

>>> class Bar(Foo):

...     def _get_prop(self):

...         return 'bar'

...

>>> Foo().prop

'foo'

>>> Bar().prop

'foo'

为了允许子类更容易地覆盖特定的属性行为,您可以使用lazy_property()函数创建您的属性。这将自动查看哪个子类正在访问该属性,并使用您添加的任何被覆盖的函数,否则将返回到原始函数。

>>> from django.utils.functional import lazy_property

>>> class Foo(object):

...     def _get_prop(self):

...         return 'foo'

...     prop = lazy_property(_get_prop)

...

>>> class Bar(Foo):

...     def _get_prop(self):

...         return 'bar'

...

>>> Foo().prop

'foo'

>>> Bar().prop

'bar'

内存(func、cache、num_args)

当处理大量信息时,函数通常需要进行某些基本计算,其中唯一的真实变量(即,从一个调用到下一个调用发生变化的值)是传入的参数。重用第七章提到的一个术语,这个行为使得函数幂等;给定相同的参数,无论函数被调用多少次,结果都是一样的。事实上,这是该术语最初的数学含义,它被借用来与 HTTP 方法一起使用。

幂等性在人类和计算机之间提供了一种有趣的分离。虽然人类可以很容易地识别函数何时是幂等的,并学会记住结果,而不是每次都继续执行该函数(还记得学习乘法表吗?),电脑就没那么幸运了。他们会高兴地一次又一次地使用这个函数,从来没有意识到它花费了多少不必要的时间。在数据密集型应用中,这可能是一个大问题,在这种情况下,一个函数可能需要很长时间来执行,或者用相同的参数执行数百次或数千次。

一个程序有可能走我们人类小时候学过的捷径,但这需要一点帮助。Django 通过同样位于django.utils.functionalmemoize()功能提供这种帮助。它只接受任何一个标准函数,并返回一个包装器,记录正在使用的参数,并将它们映射到函数为这些参数返回的值。然后,当这些相同的参数再次传入时,它只需查找并返回之前计算的值,而无需再次运行原始函数。

除了要调用的函数之外,memoize()还有另外两个参数,用来决定如何管理返回值的缓存。

  • cache—存储值的字典,关键字是传递给函数的参数。任何类似字典的对象都可以在这里工作,因此,例如,可以围绕 Django 的低级缓存编写一个字典包装器——在第八章中有所描述——并让多个线程、进程甚至整个机器共享同一个内存化缓存。
  • num_args—字典缓存中组合形成关键字的参数的数量。这通常是函数接受的参数总数,但如果有不影响返回值的可选参数,这个数字可能会更低。

>>> from django.utils.functional import memoize

>>> def median(value_list):

...     """

...     Finds the median value of a list of numbers

...     """

...     print 'Executing the function!'

...     value_list = sorted(value_list)

...     half = int(len(value_list) / 2)

...     if len(value_list) % 2:

...         # Odd number of values

...         return value_list[half]

...     else:

...         # Even number of values

...         a, b = value_list[half - 1:half + 1]

...         return float(a + b) / 2

>>> primes = (2, 3, 5, 7, 11, 13, 17)

>>> fibonacci = (0, 1, 1, 2, 3, 5, 8, 13)

>>> median(primes)

Executing the function!

7

>>> median(primes)

Executing the function!

7

>>> median = memoize(median, {}, 1)

>>> median(primes)

Executing the function!

7

>>> median(primes)

7

>>> median(fibonacci)

Executing the function!

2.5

>>> median(fibonacci)

2.5

NOTE ABOUT MEMOIZING ARGUMENTS

因为函数的参数将在字典中用于映射返回值,所以它们必须是可哈希的值。通常,这意味着任何东西都是不可变的,但是某些其他类型的对象也可能是可散列的。例如,如果传递的是一个列表而不是一个元组,本节描述的median()函数将会抛出一个错误。因为列表的内容可以改变,所以它们不能用作字典键。

分区(谓词,值)

这是一个简单的实用函数,根据将每个值传递给predicate函数的结果,将一个values序列分成两个列表。返回值是一个 2 元组,元组中的第一项是False响应,而第二项包含True响应。

>>> from django.utils.functional import partition

>>> partition(lambda x: x > 4, range(10))

([0, 1, 2, 3, 4], [5, 6, 7, 8, 9])

谓词应该返回TrueFalse,但是在内部partition()实际上利用了这样一个事实:当TrueFalse被用作序列的索引时,它们分别等价于10。这意味着如果你有一个已经返回10的谓词,你不需要转换它来使用TrueFalse来代替。

>>> even, odd = parittion(lambda x: x % 2, range(10))

>>> even

[0, 2, 4, 6, 8]

>>> odd

[1, 3, 5, 7, 9]

包装(功能)

第二章详细描述了 decorator,但是有一个方面在某些情况下会引起问题,因为 decorator 经常返回一个原始函数的包装器。事实上,这个包装器是一个与源文件中所写的完全不同的函数,所以它也有不同的属性。当自省函数时,如果几个函数通过同一个装饰器传递,这会导致混淆,因为它们共享相似的属性,包括它们的名字。

>>> def decorator(func):

...     def wrapper(*args, **kwargs):

...         return func(*args, **kwargs)

...     return wrapper

>>> def test():

...     print 'Testing!'

>>> decorated = decorator(test)

>>> decorated.__name__

'wrapper'

为了帮助缓解这种情况,Django 包含了 Python 自己的wraps()函数的副本,该函数是在 Python 2.5 中首次引入的。wraps()实际上是另一个装饰器,它将原始函数的细节复制到包装器函数上,所以当一切完成时,它看起来更像原始函数。只需将原始函数传递给wraps(),像使用包装器上的其他装饰器一样使用它,剩下的工作就交给它了。

>>> from django.utils.functional import wraps

>>> def decorator(func):

...     @wraps(func)

...     def wrapper(*args, **kwargs):

...         return func(*args, **kwargs)

...     return wrapper

>>> def test():

...     print 'Testing!'

>>> decorated = decorator(test)

>>> decorated.__name__

'test'

Caution

遗憾的是,wraps()无法让包装器与原函数完全一致。特别是,它的函数签名将总是反映包装函数的签名,所以试图自省修饰函数的参数可能会导致一些混乱。尽管如此,出于自动化文档和调试的目的,让wraps()更新名称和其他信息是非常有用的。

信号

大型应用的一个重要方面是知道应用的其他部分何时会发生某些事情。更好的是有能力在事件发生的瞬间做一些事情。为此,Django 包含了一个信号调度器,它允许代码广播事件的发生,同时提供一个方法让其他代码在事件发生时监听这些广播并做出相应的反应。它通过允许代码定义要调度的唯一信号来识别正在广播的事件的类型。

调度的概念和实现它的代码并不是 Django 独有的,但是它的实现是为 Web 应用的需求定制的。这个实现位于django.dispatch.dispatcher,尽管它被设计成通过简单的Signal对象使用,该对象在django.dispatch可用。Django 在很多地方使用信号,其中很多在本书的其他地方都有记录,在使用信号的地方。接下来的部分将更全面地讨论信号和调度是如何工作的,以及如何为特定事件注册侦听器。

它是如何工作的

基本过程相当简单。每一步都将在单独的章节中详细解释,但是下面的内容应该是一个很好的概述。

首先,一些 Python 代码定义了一个信号。如下一节所述,这是一个放置在可靠位置的Signal对象。该对象表示预期在某个时间点发生的事件,可能会发生多次。调度员不使用任何信号的集中登记;由您自己的代码决定在任何给定的时间使用哪个信号。

当您的代码触发了您希望其他代码知道的事件时,您的代码会向信号发送一些信息,包括表示事件来源的“sender”对象和描述事件其他细节的任何参数。信号本身只识别事件的类型;这些额外的参数描述了在特定时间发生的事情。

然后,该信号查看其注册侦听器列表,以查看是否有任何侦听器匹配所提供的信号和发送者,并依次调用它找到的每个函数,传递事件触发时该信号给出的任何参数。侦听器的注册可以在任何时候发生,当添加新的侦听器时,信号将更新其注册表,以便将来的事件将包括新的侦听器。

定义信号

信号不需要实现任何类型的协议,甚至不需要提供任何属性。它们实际上只是事件发生时用来做广告的工具;它们只是Signal的实例。定义一个成功信号的真正关键是确保它不会被取代。信号对象必须始终可以从同一导入位置获得,并且必须始终是同一对象。调度程序需要这样做,因为它使用对象作为标识符,将被调度的事件与已注册的适当侦听器相匹配。

>>> from django.dispatch import Signal

>>> signal = Signal()

>>> signal

<django.dispatch.dispatcher.Signal object at 0x...>

发送信号

每当您想要通知其他代码一个事件发生时,signals 提供了一个send()方法来将该信号发送给任何注册的侦听器。这个方法需要一个sender,它代表负责分派信号的对象,这允许监听器响应来自特定对象的事件。通常,Django 使用一个类——比如一个模型——作为sender,这样监听器可以在创建任何实例之前注册,同时也允许监听器响应该类的所有实例上的事件。

除了发送者之外,send()还接受任意数量的附加关键字参数,这些参数将直接传递给监听器。如下一节所示,侦听器必须总是接受所有的关键字参数,不管它们实际使用的是什么。这允许发送代码稍后向信号添加新信息,而不会给尚未更新以使用新信息的侦听器带来任何问题。发送信号的代码很可能会在以后添加一些特性,这种关键字参数支持使得将这些特性合并到现有信号中变得很容易。

一旦调用了所有的监听器,send()返回一个由注册的监听器返回的响应列表。该列表包含格式为(listener, response)的 2 元组序列。Django 自己的信号通常不使用任何返回值,但是它们对于支持将信息发送回应用本身的插件非常有用。

>>> from django.dispatch import Signal

>>> signal = Signal()

>>> sender = object()

>>> signal.send(sender=sender, spam='eggs')

[]

捕获返回值

函数通常会返回值,信号可以充分利用这一点。当用信号的参数调用每个监听器时,Django 捕获它的返回值并将它们收集在一个列表中。一旦所有的监听器都被调用,返回值的完整列表就会从Signal.send()返回,允许调用代码访问监听器提供的任何信息。这允许信号不仅仅用于额外的动作;它们也可以用于数据处理和相关任务。

定义监听器

发送时,信号将发送者和所有适当的参数传递给用该信号注册的每个侦听器函数。侦听器只是一个 Python 函数,和其他函数一样;唯一的区别是已经被注册为特定信号的收听者。由于信号只是简单地将监听器作为一个函数调用,它实际上可以是任何有效的 Python 可调用函数,其中许多在第二章中有所描述。实际上,标准函数是最常见的。

虽然允许侦听器有很大的灵活性,但信号确实对如何定义它们做了一个重要的假设:所有侦听器都必须接受传入的任何关键字参数。实际使用哪些参数完全取决于特定侦听器打算如何使用信号,但它必须正确无误地接受未使用的参数。如前所述,可以用任意数量的关键字参数发送信号,这些参数都将被传递给所有的侦听器。

这种方法的价值在于听众不需要知道信号负责的所有事情。可以为一个目的附加一个侦听器,期望一组特定的参数。然后,可以将附加参数添加到信号调度中,所有先前定义的侦听器将继续正常工作。与任何其他函数调用一样,如果侦听器期望一个信号没有提供的参数,Python 将引发一个TypeError

def listener(sender, a, **kwargs):

return a * 3

注册侦听器

一旦有了一个要处理的信号和一个要处理它的监听器,连接它们就是简单地调用信号的connect()方法。除了一个必需的参数之外,在注册信号时还可以指定几个选项,定制稍后调度信号时应该如何处理侦听器。

  • receiver—将接收信号及其相关参数的可调用函数。这显然是所有注册所必需的。
  • sender—观察信号的特定对象。由于每个信号都必须包含一个发送者,这就允许一个监听器只响应那个发送者。如果省略,将为发出给定信号的所有发送方调用侦听器。
  • weak—一个布尔值,指示是否应该使用弱引用,这个主题将在下一节中更详细地描述。默认为True,默认使用弱引用。
  • dispatch_uid—用于识别给定信号上的收听者的唯一字符串。由于模块有时可以不止一次地被导入,侦听器有可能被注册两次,这通常会导致问题。在这里提供一个唯一的字符串将确保侦听器只注册一次,不管模块被导入多少次。如果省略,将基于侦听器本身生成一个 ID。

强制强引用

虽然弱引用是一个相当复杂的话题,远远超出了本书的范围, 3 信号的使用在某些情况下会导致混乱,所以有必要给出这个问题及其解决方案的基本概述。当使用弱引用引用一个对象时,就像 Django 的 dispatcher 所做的那样,这个引用本身不会阻止对象被垃圾收集。它必须在别的地方还有一个强引用,否则 Python 会自动销毁它,释放它所占用的内存。

虽然 Python 中的标准引用是强引用,但默认情况下,dispatcher 使用弱引用来维护其注册侦听器列表。对于信号来说,这通常更好,因为这意味着属于不再使用的代码的侦听器函数不会因为被调用而耗尽宝贵的时间和精力。

然而,Python 中的一些情况通常会导致对象被破坏,这些情况在使用信号时需要特别注意。特别是,如果在另一个函数中定义了一个侦听器函数,可能是为了为特定对象定制一个函数,那么当侦听器的容器函数执行完毕并且其作用域被移除时,该侦听器将被销毁。

>>> from django.dispatch import Signal

>>> signal = Signal()

>>> def weak_customizer():

...     def weak_handler(sender, **kwargs):

...        pass

...     signal.connect(weak_handler)

...

>>> def strong_customizer():

...     def strong_handler(sender, **kwargs):

...        pass

...     signal.connect(strong_handler, weak=False)

...

>>> weak_customizer()

>>> strong_customizer()

>>> signal.send(sender="sender")

[(<function <strong_handler> at 0x...>, None)]

如您所见,注册侦听器的默认形式允许在定制函数执行完毕后销毁该函数。通过显式地指定weak=False,当信号在稍后的时间点被发送时,它仍然被调用。

现在怎么办?

本章中介绍的工具不会为您的应用提供主要的新功能,但是它们可以帮助您完成许多应用需要的更简单的任务。这些小事情真的可以帮助你把所有事情联系在一起。应用实际上如何使用是另一个问题,一些更有趣的选项将在下一章描述。

Footnotes 1

http://prodjango.com/mutagen/

2

http://prodjango.com/json/

3

http://prodjango.com/weak-references/

十、协调应用

Abstract

为企业编写软件是一项艰苦的工作。没有单一的规则手册来概述应该编写哪些应用,应该如何编写,它们应该如何相互交互,或者它们应该如何定制。所有这些问题的答案最好留给每个项目的开发人员,但是本章和第十一章给出的例子可以帮助你决定项目的最佳方法。

为企业编写软件是一项艰苦的工作。没有单一的规则手册来概述应该编写哪些应用,应该如何编写,它们应该如何相互交互,或者它们应该如何定制。所有这些问题的答案最好留给每个项目的开发人员,但是本章和第十一章的例子可以帮助你决定项目的最佳方法。

网站的大部分功能都是面向外部的,向组织外部的用户提供功能。很多时候,更多的功能集中在内部,旨在帮助员工更有效地执行日常任务。考虑一个需要跟踪其客户和可用房产的基本房地产网站。除了向外界显示属性之外,代理还需要管理这些属性以及帮助流程向前发展的人员。

与其构建一个面向特定需求的大型应用,不如尝试将这些需求分开,让多个应用协同工作以实现最终目标。这样做在开始时需要多做一些工作,但是随着新功能的不断增加,应用的清晰分离将有助于确定什么应该放在哪里以及一切应该如何协同工作。

联系人

虽然看起来房地产世界的一切都围绕着房地产,但人仍然是最基本的一块拼图。例如,一个给定的房产可能有一个所有者、一个房地产经纪人和几个潜在的买家。这些人在房地产过程中分别扮演不同的角色,但是无论他们扮演什么角色,表示他们所需的数据都是相同的。它们都可以概括为一个“联系人”,该联系人只包含识别它们并与之通信所必需的数据。

这种抽象为我们提供了一个简单的模型,可以用于与特定房产相关的人、尚未对房产表示兴趣的其他人、我们虚构的房地产办公室本身的员工,甚至像质量检查员和价值评估员这样的第三方联系人。每个人所扮演的角色可以通过将他们与另一个模型(比如一个属性)相关联来定义。

联系人.模型.联系人

联系信息通常包括姓名、地址、电话号码和电子邮件地址,其中一些已经可以被 Django 捕获。来自django.contrib.authUser模型包含一个人的姓和名以及电子邮件地址的字段,所以剩下的就是包含一些更真实的联系信息。将它与User相关联允许一个联系人包含两种类型的数据,同时也为以后可以登录的联系人提供了可能性。

因为我们的房地产公司将在美国运营,所以有一些特定的联系信息字段需要根据当地习俗验证数据。为了提供对区域数据类型的支持,有多种本地风格的包可供使用。每个包,包括我们将要使用的django-localflavor-us,都包含了一个模型和表单的字段选择,这些字段特定于该区域的公共数据类型。我们的Contact型号可以特别利用PhoneNumberFieldUSStateField

from django.db import models

from django.contrib.auth.models import User

from django_localflavor_us import models as us_models

class Contact(models.Model):

user = models.OneToOneField(User)

phone_number = us_models.PhoneNumberField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip_code = models.CharField(max_length=255)

class Meta:

ordering = ('user__last_name', 'user__first_name')

def __unicode__(self):

return self.user.get_full_name()

WHY NOT MODEL INHERITANCE?

第三章解释了一个 Django 模型如何直接从另一个继承,自动创建一个类似于这里使用的引用。因为这也增加了一些额外的易用选项,你可能会奇怪为什么Contact不直接从User继承。

模型继承最适合不直接使用基本模型的情况,因为 Django 不提供向现有模型添加继承实例的方法。在我们的例子中,这意味着如果数据库中已经存在一个User,我们将不能基于它创建一个新的Contact。因为有许多其他应用,包括 Django 的管理应用,可能会直接创建用户,所以我们需要能够毫无困难地为新用户或现有用户创建联系人。

通过显式使用OneToOneField,我们定义了模型继承将使用的完全相同的关系,但是没有在这种情况下限制我们的不同语法。我们失去了真正继承提供的一些语法优势,但是这些可以通过另一种方式来适应。

因为联系人本质上只是添加了一些属性的用户,所以在一个对象上拥有所有可用的属性是很有用的。否则,模板作者不仅要知道给定属性来自哪个模型,还要知道如何引用另一个模型来检索这些属性。例如,给定一个名为contactContact对象,下面的列表显示了它的许多属性和方法:

  • contact.user.username
  • contact.user.get_full_name()
  • contact.user.email
  • contact.phone_number
  • contact.address
  • contact.zip_code

这给模板作者带来了不必要的负担,他们不需要知道联系人和用户之间存在什么类型的关系。模型继承通过将所有属性直接放在接触上直接缓解了这种情况。这里,只需使用一组属性就可以实现相同的行为,这些属性将各种属性映射到后台的相关用户对象。

@property

def first_name(self):

return self.user.first_name

@property

def last_name(self):

return self.user.last_name

def get_full_name(self):

return self.user.get_full_name()

不是所有的User方法在Contact上都有意义。例如,is_anonymous()is_authenticated()方法最好留给User。视图和模板不会使用联系人来确定身份验证或权限,因此联系人将作为个人身份信息各个方面的中心位置。

contacts.forms.UserEditorForm

与其要求用户通过管理界面管理他们的联系人,不如有一个单独的表单专门用于联系人管理。这对于联系人来说甚至比大多数其他模型更重要,因为联系人实际上包括两个独立的模型。Django 提供的ModelForm助手 1 将一个表单映射到一个模型,要求contacts应用使用两个单独的表单来管理一个人。

一个表单可以包含两个模型所需的所有字段,但这不适用于ModelForm,因为表单必须包含手动填充和保存模型所需的所有逻辑。相反,可以使用两种独立的形式,以便将它们联系在一起。详见contacts.views.EditContact的描述。

因为 Django 自己提供了User模型,所以重用 Django 用于管理用户的任何东西似乎都是合乎逻辑的。不幸的是,为用户管理提供的表单是为与联系人管理非常不同的用例设计的。有两种形式可用,都住在django.contrib.auth.forms,各有不同的目的:

  • UserCreationForm—该表格旨在用于最基本的用户创建,仅接受一个用户名和两份密码(用于验证)。联系人所需的字段(姓名和电子邮件)不可用。
  • UserChangeForm—用于管理界面,该表单包含User模型上可用的每个字段。虽然这包括姓名和电子邮件,但它也包括一系列用于身份验证和授权的字段。

因为这些表单都不适合联系人管理的用例,所以为这个应用创建一个新表单更有意义。使这变得容易,允许一个表单只指定那些不同于缺省值的东西。对于联系人管理,这意味着只包括用户名、名、姓和电子邮件地址等字段。

from django import forms

from django.contrib.auth.models import User

class UserEditorForm(forms.ModelForm):

class Meta:

model = User

fields = ('username', 'first_name', 'last_name', 'email')

有了这些信息,ModelForm就可以根据底层模型提供的细节来管理表单的其余行为。剩下的工作就是提供一个补充表单来管理新的联系人级别的详细信息。

contacts . forms . contacteditorform

管理联系人的表单与用户表单非常相似,使用ModelForm来处理大部分细节。唯一的区别是,用于联系人的字段比已经定义的Contact模型中列出的字段有更具体的验证要求。例如,电话号码以纯文本的形式存储在模型中,但是它们遵循特定的格式,可以被表单验证。这些验证已经由模型中使用的同一个django-localflavor-us包提供了。这个ContactEditorForm可以使用四个类:

  • USStateField根据当前状态验证两个字母的代码
  • USStateSelect显示包含所有有效状态的列表框
  • USPhoneNumberField验证十位数电话号码,包括破折号
  • 验证五位数或九位数的邮政编码

Note

USStateField还包括美国领土:美属萨摩亚群岛、哥伦比亚特区、关岛、北马里亚纳群岛、波多黎各和美属维尔京群岛。

还有其他的类,但是这四个类足以定制联系人的验证。唯一剩下的可编辑字段“地址”和“城市”没有可以通过编程验证的既定格式。应用这些覆盖,ContactEditorForm如下所示:

from django import forms

from django_localflavor_us import forms as us_forms

from contacts.models import Contact

class ContactEditorForm(forms.ModelForm):

phone_number = us_forms.USPhoneNumberField(required=False)

state = us_forms.USStateField(widget=us_forms.USStateSelect, required=False)

zip_code = us_forms.USZipCodeField(label="ZIP Code", required=False)

class Meta:

model = Contact

exclude = ('user',)

注意这里使用了exclude而不是字段,就像在UserEditorForm中使用的一样。这告诉ModelForm使用模型中除了那些明确列出的字段之外的所有字段。因为用户将由UserEditorForm提供,所以没有必要在这里将其作为一个单独的选项。地址和城市不需要作为显式字段声明提供,因为ModelForm会自动使用标准文本字段。

contacts . views . editcontact 联络人。检视.编辑联络人

联系人由两种模型组成——因此也有两种表单——但是管理这些联系人的用户应该只需要处理一个包含所有适当字段的表单。Django 的特定于表单的通用视图在这里并没有真正帮助我们,因为它们只是为每个视图一个表单而设计的。我们将两种形式结合起来,这要么需要对UpdateView进行相当大的修改,要么需要一个结合两种形式的新类。这两个选项都不令人愉快,所以我们将采用一种更实用的方法,用稍微通用一些的视图,手工组装表单处理行为。

首先要做的选择是接受什么样的论点。因为这个视图将在模板中呈现表单,所以最好在 URL 配置中接受一个模板名称,所以我们将依赖 Django 的TemplateView,它会自己处理这个问题。通过简单地子类化django.views.generic.TemplateView,我们的新视图将在其配置中自动接受一个template_name参数,并提供一个将模板呈现给响应的方法。

为了调出单个联系人进行编辑,视图还必须能够识别应该使用哪个联系人。这个标识符必须是唯一的,并且应该是用户可以合理理解的。因为每个联系人都与一个用户相关,并且每个用户都有一个唯一的用户名,所以这个用户名非常适合这个目的。

from django.views.generic import TemplateView

class EditContact(TemplateView):

def get(self, request, username=None):

pass

注意用户名是可选的。拥有一个可选的标识符允许这个视图用于添加新的联系人以及编辑现有的联系人。这两种情况需要本质上相同的行为:接受用户的联系信息,检查它们是否是有效数据,并将它们保存在数据库中。添加和编辑的唯一区别是Contact对象是否已经存在。

考虑到这个目标,视图必须准备好创建一个新的Contact对象,甚至可能创建一个新的User,如果它们都不存在的话。必须处理四种不同的情况:

  • 提供了一个用户名,并且对于该用户名存在一个User和一个Contact。视图应该继续编辑两个现有记录。
  • 提供了一个用户名并且存在一个User,但是没有Contact与之相关联。应该创建一个新的Contact并与User相关联,这样两者都可以被编辑。
  • 提供了一个用户名,但不存在它的User,这也意味着不存在Contact。请求用户名意味着一个已存在的用户,因此请求一个不存在的用户应该被认为是错误的。在这种情况下,这是 HTTP 404(未找到)错误代码的适当用法。
  • 没有提供用户名,这意味着现有用户和联系人是不相关的。应该创建新的UserContact对象,忽略任何可能已经存在的对象。该表格将要求提供一个新的用户名。

Tip

使用 404 错误代码并不总是意味着您必须提供通用的“未找到页面”页面。您可以向HttpResponseNotFound类提供您喜欢的任何内容,而不是默认的HttpResponse类。为了简单起见,这些例子仅仅依赖于标准的 404 错误页面,但是对于您的站点来说,显示类似“您请求的联系人尚不存在”这样的 404 页面可能更有意义这允许您利用已知的 HTTP 状态代码,同时仍然向用户显示更有用的消息。

这些情况可以用get_objects()方法轻松处理。它被分解到自己的方法中,因为我们最终会从get()post()方法中都需要它。

from django.shortcuts import get_object_or_404

from django.views.generic import TemplateView

from django.contrib.auth.models import User

from contacts.models import Contact

class EditContact(TemplateView):

def get_objects(self, username):

# Set up some default objects if none were defined.

if username:

user = get_object_or_404(User, username=username)

try:

# Return the contact directly if it already exists

contact = user.contact

except Contact.DoesNotExist:

# Create a contact for the user

contact = Contact(user=user)

else:

# Create both the user and an associated contact

user = User()

contact = Contact(user=user)

return user, contact

def get(self, request, username=None):

pass

一旦知道这两个对象都存在,视图就可以显示这两个对象的表单,这样用户就可以填写信息。这是通过get()方法处理的。

from django.shortcuts import get_object_or_404

from django.views.generic import TemplateView

from django.contrib.auth.models import User

from contacts.models import Contact

from contacts import forms

class EditContact(TemplateView):

def get_objects(self, username):

# Set up some default objects if none were defined.

if username:

user = get_object_or_404(User, username=username)

try:

# Return the contact directly if it already exists

contact = user.contact

except Contact.DoesNotExist:

# Create a contact for the user

contact = Contact(user=user)

else:

# Create both the user and an associated contact

user = User()

contact = Contact(user=user)

return user, contact

def get(self, request, username=None):

user, contact = self.get_objects()

return self.render_to_response({

'username': username

'user_form': forms.UserEditorForm(instance=user)

'contact_form': forms.ContactEditorForm(instance=contact)

})

然后视图可以继续处理表单,并用适当的信息填充这些对象。它必须独立于其他表单实例化、验证和保存每个表单。这样,每个表单只需要知道它要管理的数据,而视图可以将两者联系在一起。

如果两个表单都保存正确,视图应该重定向到一个新的 URL,在那里可以查看编辑过的联系信息。这对于新的联系人特别有用,因为在处理表单之前不会给他们分配 URL。在任何其他情况下,包括第一次查看表单时(即尚未提交任何数据),以及提交的数据未能通过验证时,视图应该返回可以显示适当表单的呈现模板。

from django.core.urlresolvers import reverse

from django.http import HttpResponseRedirect

from django.shortcuts import get_object_or_404

from django.views.generic import TemplateView

from django.contrib.auth.models import User

from contacts.models import User

from contacts import forms

class EditContact(TemplateView):

def get_objects(self, username):

# Set up some default objects if none were defined.

if username:

user = get_object_or_404(User, username=username)

try:

# Return the contact directly if it already exists

contact = user.contact

except Contact.DoesNotExist:

# Create a contact for the user

contact = Contact(user=user)

else:

# Create both the user and an associated contact

user = User()

contact = Contact(user=user)

return user, contact

def get(self, request):

user, contact = self.get_objects()

return self.render_to_response({

'username': user.username

'user_form': forms.UserEditorForm(instance=user)

'contact_form': forms.ContactEditorForm(instance=contact)

})

def post(self, request):

user, contact = self.get_objects()

user_form = forms.UserEditorForm(request.POST, instance=user)

contact_form = forms.ContactEditorForm(request.POST, instance=contact)

if user_form.is_valid() and contact_form.is_valid():

user = user_form.save()

# Attach the user to the form before saving

contact = contact_form.save(commit=False)

contact.user = user

contact.save()

return HttpResponseRedirect(reverse('contact_detail'

kwargs={'slug': user.username}))

return self.render_to_response(self.template_name, {

'username': user.username

'user_form': user_form

'contact_form': contact_form

})

管理配置

因为这个应用有自己的添加和编辑联系人的视图,所以不太需要使用管理界面。但是由于后面描述的Property模型既与Contact相关,又大量使用 admin,所以配置一个管理联系人的基本界面是个好主意。

from django.contrib import admin

from contacts import models

class ContactAdmin(admin.ModelAdmin):

pass

admin.site.register(models.Contact, ContactAdmin)

它没有提供同时编辑UserContact模型的便利,但是为通过管理员管理的相关模型提供了价值。

URL 配置

除了添加和编辑联系人,该应用还必须提供一种方式来查看所有现有的联系人和任何特定联系人的详细信息。这些特性反过来要求在contact应用的 URL 配置中考虑四种不同的 URL 模式。其中两个将映射到上一节描述的edit_contact视图,而另外两个将映射到 Django 自己的通用视图。

  • /contacts/—所有现有联系人的列表,带有指向各个联系人详细信息的链接
  • /contacts/add/—可以添加新联系人的空表单
  • /contacts/{username}/—给定用户的所有联系信息的简单视图
  • /contacts/{username}/edit/—填充有任何现有数据的表单,可以在其中更改数据和添加新数据

这些 URL 开头的/contacts/部分不是任何联系人视图本身的组成部分;这是站点级的区别,指向整个contacts应用。因此,它不会包含在应用的 URL 配置中,而是包含在站点的配置中。剩下的是一组 URL 模式,它们可以移植到站点需要的任何 URL 结构中。

第一个模式是所有现有联系人的列表,表面上很简单。一旦从 URL 中删除了/contacts/,就什么都没有了——更确切地说,剩下的只是一个空字符串。空字符串确实很容易与正则表达式匹配,但是我们将为它使用的视图django.views.generic.ListView需要一些额外的定制才能正常工作。

首先,它需要一个queryset和一个template_name,控制在哪里可以找到对象以及它们应该如何显示。对于此应用,所有联系人都是可用的,没有任何过滤。根据您的风格,模板名称可以是最合适的名称;我就叫它"contacts/contact_list.html"

通过显示所有联系人,列表可能会变得很长,因此如果需要的话,将结果分成多个页面会更有用。ListView也通过它的paginate_by参数提供了这一点。如果提供的话,它会在溢出到下一页之前提供单个页面上应该显示的最大数量的结果。然后,该模板可以控制页面信息和相关页面链接的显示方式。

from django.conf.urls.defaults import *

from django.views.generic import ListView

from contacts import models

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

)

接下来是添加新联系人的 URL,使用自定义的EditContact视图。像联系人列表一样,这个 URL 模式的正则表达式非常简单,因为它不包含任何要捕获的变量。除了匹配 URL 的add/部分,这个模式只需要指向正确的视图并传递一个模板名。

from django.conf.urls.defaults import *

from django.views.generic import ListView

from contacts import models , views

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

url(r'^add/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_add_form')

)

其余的 URL 模式都需要从 URL 本身捕获用户名,然后将其传递给相关的视图。用户名遵循相当简单的格式,允许字母、数字、破折号和下划线。这可以用正则表达式[\w-]+来表示,这是一种通常用于识别文本标识符的模式,通常被称为“slugs”

Note

就像 Django 本身一样,Slugs 也植根于新闻行业。slug 是一篇文章在付印前给新闻机构内部交流使用的名称。就在印刷之前,文章会有一个合适的标题,但 slug 仍然是唯一引用特定文章的方式,不管它是否可供公众查看。

要编写的第一个视图,即基本的 contact detail 页面,将使用 Django 提供的另一个通用视图django.views.generic.DetailView,因此必须注意用户名所分配到的变量的名称。自定义的EditContact视图称其为username,但是DetailView不知道要寻找具有该名称的东西。相反,它允许一个 URL 模式捕获一个slug变量,其功能相同。另一个要求是提供一个slug_field参数,它包含与 slug 匹配的字段的名称。

通常,这个slug_field参数是模型上可以找到slug值的字段的名称。不过,像大多数通用视图一样,DetailView需要给一个queryset参数一个有效的 QuerySet,从中可以检索到一个对象。视图然后向 QuerySet 添加一个get()调用,使用slug_field / slug组合来定位一个特定的对象。

这个实现细节很重要,因为它为 URL 模式提供了额外的灵活性,如果视图将slug_field与模型上的实际字段相匹配,这种灵活性是不可用的。更具体地说,slug_field可以包含一个跨越相关模型的查找,这一点很重要,因为联系人是由两个不同的模型组成的。URL 模式应该通过查询相关的User对象的用户名来检索一个Contact对象。为此,我们可以将slug_field设置为"user__username"

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from contacts import models, views

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

url(r'^add/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_add_form')

url(r'^(?P<slug>[\w-]+)/$'

DetailView.as_view(queryset=models.Contact.objects.all()

slug_field='user__username'

template_name='contacts/list.html')

name='contact_detail')

)

最后一个 URL 模式是编辑单个联系人,它严格遵循用于添加新联系人的模式。两者之间唯一的区别是用于匹配 URL 的正则表达式。前面的模式没有从 URL 中捕获任何变量,但是这个模式需要捕获用户名来填充表单的字段。用于捕获用户名的表达式将使用与细节视图相同的格式,但是将使用名称username而不是slug

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from contacts import modelsviews

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Contact.objects.all()

template_name='contacts/list.html'

paginate_by=25)

name='contact_list')

url(r'^add/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_add_form')

url(r'^(?P<slug>[\w-]+)/$'

DetailView.as_view(queryset=models.Contact.objects.all()

slug_field='user__username'

template_name='contacts/list.html')

name='contact_detail')

url(r'^(?P<username>[\w-]+)/edit/$', views.EditContact.as_view(

template_name='contacts/editor_form.html'

), name='contact_edit_form')

)

这个应用现在唯一缺少的是 URL 模式中提到的四个模板。由于这本书的目标是开发,而不是设计,这些留给读者作为练习。

房地产属性

房地产公司的主业当然是房地产。单个建筑或一块土地通常被称为属性,但是这个术语不应该与 Python 的属性概念混淆,在第二章中有描述。这种名称冲突是不幸的,但并不意外;完全不同的人群使用相同的术语表达不同的意思是很常见的。

当这种情况普遍出现时,最好使用你的听众最容易理解的术语。在与房地产经纪人会面时,你应该能够使用“财产”来指代一块房地产,而不会有任何混淆或解释。当与程序员交谈时,“属性”可能指的是模型、对象或内置函数。

Python 的property装饰器在很多情况下都很有用,但是本章的大部分内容将集中在其他 Python 技术上。鉴于此,除非另有说明,否则术语“财产”将指不动产。

属性.模型.属性

物业管理应用中最基本的项目是一个Property。在房地产术语中,房产只是一块土地,通常附有一栋或多栋建筑物。这包括房屋、零售店、工业区和未开发的土地。虽然这涵盖了广泛的选项,但也有许多事情是跨领域共享的。这些共享功能中最基本的是所有属性都有一个地址,该地址由几个部分组成:

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

这个模型还包括一个 slug,它将用于识别 URL 中的属性。

Note

这种模式只使用一个地址字段,而许多地址表单使用两个字段。两个地址行总是适合于邮寄地址,因为它们允许在一个建筑物内进行划分,例如公寓或办公室套房。房地产往往专注于建筑本身和它所坐落的土地,而不是建筑如何分割,所以一个领域就足够了。共管公寓是单独出售的建筑物的子部分,因此在进行共管公寓交易的市场中,需要一个额外的地址字段来唯一标识建筑物内的属性。

除了能够定位物业之外,还可以添加更多的字段来描述物业的大小和占用它的建筑。包含这些信息的方法有很多种,而Property将利用多种方法,所有这些方法都是可选的。通常,所有这些都将在列表公开之前填写,但数据库应该支持管理具有不完整信息的属性,以便代理可以在信息可用时填充它。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

square_feet字段是指建筑物内的可用面积。当设计或改造一栋建筑时,有必要将其分解为单个房间的尺寸,但对于买卖房产的任务来说,总量本身就很好。acreage字段代表房产所占的总土地面积,以英亩为单位,相当于 43560 平方英尺。

Tip

如果代理确实获得了酒店内各个房间的尺寸,则可以使用本章后面的“properties.models.Feature”一节中描述的Feature模型将这些尺寸作为单独的酒店特征包括在内。

到目前为止,Property模型的大部分内容都集中在描述酒店本身,但也包括销售过程的一些方面。价格可能是属性列表中最重要的方面,尽管它不是一个物理属性,但每个属性一次只能有一个价格,因此在这里将其作为一个字段仍然是有意义的。下一章将解释我们如何跟踪过去的价格,但是这个模型将只存储当前价格。

另一个这样的属性是资产的status——它当前在销售过程中的位置。对于数据库中的新条目,可能根本没有任何状态。也许正在为正在考虑出售但尚未决定在市场上上市的房主记录一些房产信息。一旦业主决定出售,它可以上市供公众考虑,其余的过程开始。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

LISTED, PENDING, SOLD = range(3)

STATUS_CHOICES = (

(LISTED, 'Listed')

(PENDING, 'Pending Sale')

(SOLD, 'Sold')

)

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField(max_length=2)

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES

null=True, blank=True)

price = models.PositiveIntegerField(null=True, blank=True)

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

除了可以在模型上存储一次的属性之外,还有其他属性特征可以多次出现或以多种不同的组合出现。这些便利设施,如壁炉、地下室、车库、阁楼和电器,并不是每个房产都有的或没有的功能清单的一部分。这使得很难(如果不是不可能的话)为每个特征创建一个字段,而不必在每次出现不符合先前假设的新属性时修改模型的结构。

相反,特征应该存储在另一个模型中,指出哪些特征存在于属性中,并详细描述它们。另一个模型可以介入,将这些特性归纳为通用类型,以便可以浏览和搜索。例如,用户可能对查找所有带壁炉的房产感兴趣。拥有一个专门定义壁炉的模型,以及一个描述各个壁炉的相关模型,有助于实现这种类型的行为。请参阅后面的“properties.models.Feature”和“properties . models . property feature”部分,了解有关其工作原理的更多详细信息。

房产也有许多相关的人,如业主、房地产经纪人、建筑师、建筑商,可能还有几个潜在的买家。这些都有资格作为联系人,并使用已经定义的Contact模型进行存储。为了使其尽可能通用,他们将被称为“利益相关方”,因为每个人都在有关财产的交易中有一些利益。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

LISTED, PENDING, SOLD = range(3)

STATUS_CHOICES = (

(LISTED, 'Listed')

(PENDING, 'Pending Sale')

(SOLD, 'Sold')

)

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES

null=True, blank=True)

price = models.PositiveIntegerField(null=True, blank=True)

features = models.ManyToManyField('Feature', through='PropertyFeature')

interested_parties = models.ManyToManyField(Contact

through='InterestedParty')

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

不是所有的房产都应该公开上市。在财产上市之前,以及在出售之后,应当对公众保密,只让工作人员管理。与其每次需要公开显示一个属性时都为此键入一个查询,不如创建一个自定义管理器,用一种方法来缩小列表范围。

class PropertyManager(models.Manager):

def listed(self):

qs = super(PropertyManager, self).get_query_set()

return qs.filter(models.Q(status=Property.LISTED) | \

models.Q(status=Property.PENDING))

这可以通过简单的赋值附加到模型上;任何名字都可以,但是约定俗成是称呼标准管理器objects,所以这个会这么做。

from django.db import models

from django_localflavor_us import models as us_models

class Property(models.Model):

LISTED, PENDING, SOLD = range(3)

STATUS_CHOICES = (

(LISTED, 'Listed')

(PENDING, 'Pending Sale')

(SOLD, 'Sold')

)

slug = models.SlugField()

address = models.CharField(max_length=255)

city = models.CharField(max_length=255)

state = us_models.USStateField()

zip = models.CharField(max_length=255)

square_feet = models.PositiveIntegerField(null=True, blank=True)

acreage = models.FloatField(null=True, blank=True)

status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES

null=True, blank=True)

price = models.PositiveIntegerField(null=True, blank=True)

features = models.ManyToManyField('Feature', through='PropertyFeature')

interested_parties = models.ManyToManyField(Contact

through='InterestedParty')

objects = PropertyManager()

class Meta:

verbose_name_plural = 'properties'

def __unicode__(self):

return u'%s, %s' % (self.address, self.city)

属性.模型.功能

一个特征仅仅是酒店提供的一些值得注意的东西。它可以是一个普通的必需品,如地下室或洗衣房,但也可以是非常独特的,如壁炉或阳光房。这些功能经常被列出来,试图将一个房产与另一个区别开来,因为买家经常会列出他们想要的功能。

Feature模型仅包含定义特定类型特征所需的信息。Feature不是描述一个具体的壁炉,而是简单地定义了什么是壁炉,为各个壁炉提供了一个定位点。这样,可以使用这个模型作为起点,通过特性来搜索属性。

class Feature(models.Model):

slug = models.SlugField()

title = models.CharField(max_length=255)

definition = models.TextField()

def __unicode__(self):

return self.title

属性.模型.属性特征

在查看特定属性时,特定的细节远比在高层次上定义一个特性更有用。PropertyFeature模型在PropertyFeature之间架起了一座桥梁,提供了一种描述某一特定物业的个人特征的方式。

class PropertyFeature(models.Model):

property = models.ForeignKey(Property)

feature = models.ForeignKey(Feature)

description = models.TextField(blank=True)

def __unicode__(self):

return unicode(self.feature)

属性.模型.兴趣方

对某一特定房产感兴趣的联系人多种多样,从业主和买家到房地产经纪人和安全检查员。这些人中的每一个都可以通过关系的方式连接到特定的资产,该关系包括关于关系性质的一些细节。

from contacts.models import Contact

class InterestedParty(models.Model):

BUILDER, OWNER, BUYER, AGENT, INSPECTOR = range(5)

INTEREST_CHOICES = (

(BUILDER, 'Builder')

(OWNER, 'Owner')

(BUYER, 'Buyer')

(AGENT, 'Agent')

(INSPECTOR, 'Inspector')

)

property = models.ForeignKey(Property)

contact = models.ForeignKey(Contact)

interest = models.PositiveSmallIntegerField(choices=INTEREST_CHOICES)

class Meta:

verbose_name_plural = 'interested parties'

def __unicode__(self):

return u'%s, %s' % (self.contact, self.get_interest_display())

Note

这些角色可以重叠,例如所有者同时是建筑商和房地产经纪人。一些数据库允许将字段用作位掩码,您可以切换单个位来指示联系人履行的角色。由于 Django 不支持创建或搜索这些类型的字段,我们改为每行只存储一个角色;具有多个角色的联系人可以简单地使用多行来描述情况。

管理配置

房产列表是供普通公众浏览的,但只能由房地产代理公司的员工编辑,这些员工在该领域受过广泛的培训并有丰富的经验,可以被信任来完成这项任务。该描述与 Django 内置管理应用的目标受众相同。

使用 admin 提供的功能,用户可以很容易地将界面放在一起,以便能够编辑和维护 properties 应用中的所有各种模型。不需要单独的编辑器视图,只需要做一些小的修改就可以定制管理,以一种用户友好的方式使用这些模型。

“THE ADMIN IS NOT YOUR APP”

如果你花很多时间在 Django 社区,你可能会遇到这样一句话,“管理不是你的应用。”这里传达的普遍观点是,管理员的关注点相当有限,远比大多数网站有限。它预计将由可信的工作人员使用,他们可以使用更基本的数据输入界面。当你发现自己在努力寻找让管理员做你想做的事情的方法时,很可能你需要开始写自己的观点,不再依赖管理员。

这并不意味着管理员只在开发过程中有用。如果一个基本的编辑界面适合工作人员使用,它可以节省时间和精力。通过一些简单的定制,管理员可以执行这种编辑界面所需的大多数常见任务。本章前面描述的 contacts 应用不能依赖于 admin,因为它需要组合两个表单,这超出了 admin 的预期范围。

对于属性,管理员完全有能力生成一个合适的界面。因为只有员工需要编辑属性数据,所以没有必要创建与站点其他部分集成的自定义视图。您可以花更多的时间来构建面向公众的应用。

要设置的第一个模型是Property,但是由于相关模型的工作方式,需要先对PropertyFeatureInterestedParty进行一些配置。这些都是使用一个简单的类配置的,该类告诉管理员将它们作为页面末尾的表添加到属性编辑器中。除了任何现有的关系,管理员应该显示一个空记录,可用于添加新的关系。

from django.contrib import admin

from properties import models

class InterestedPartyInline(admin.TabularInline):

model = models.InterestedParty

extra = 1

class PropertyFeatureInline(admin.TabularInline):

model = models.PropertyFeature

extra = 1

为了在Property模型的管理页面上定制一些更专业的字段,需要一个定制的ModelForm子类。这允许表单指定应该为其statezip字段使用什么小部件,因为它们遵循一种比自由格式文本字段更具体的格式。所有其他字段可以保持原样,因此不需要在该表单中指定它们。

from django.contrib import admin

from django import forms

from django_localflavor_us import forms as us_forms

from properties import models

class InterestedPartyInline(admin.TabularInline):

model = models.InterestedParty

extra = 1

class PropertyFeatureInline(admin.TabularInline):

model = models.PropertyFeature

extra = 1

class PropertyForm(forms.ModelForm):

state = us_forms.USStateField(widget=us_forms.USStateSelect)

zip = us_forms.USZipCodeField(widget=forms.TextInput(attrs={'size': 10}))

class Meta:

model = models.Property

现在我们终于可以为Property本身配置管理界面了。第一个定制是使用PropertyForm而不是通常使用的普通ModelForm

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

admin.site.register(models.Property, PropertyAdmin)

在该表单中,并非所有字段都应该显示在一个从上到下的简单列表中。通过将citystatezip字段放在一个元组中,完整的地址可以以更熟悉的格式显示,这样它们都在同一行结束。slug 放在地址旁边,因为它将根据该信息进行填充。销售字段可以放在一个单独的分组中,与大小相关的字段也可以放在一个单独的分组中,并用一个标题将每个分组区分开。

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

fieldsets = (

(None, {'fields': (('address', 'slug')

('city', 'state', 'zip'))})

('Sales Information', {'fields': ('status'

'price')})

('Size', {'fields': ('square_feet'

'acreage')})

)

相关的模型被添加到一个名为inlines的元组中,该元组控制其他模型如何附加到现有的管理界面。因为它们已经在自己的类中配置好了,所以我们需要做的就是将它们添加到PropertyAdmin中。

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

fieldsets = (

(None, {'fields': (('address', 'slug')

('city', 'state', 'zip'))})

('Sales Information', {'fields': ('status'

'price')})

('Size', {'fields': ('square_feet'

'acreage')})

)

inlines = (

PropertyFeatureInline

InterestedPartyInline

)

最后,生成 slug 的声明需要一个分配给prepopulated_fields属性的字典。本词典中的关键字是自动生成的SlugField的名称。关联的值是一个字段名元组,slug 的值应该从这个元组中提取。根据地址和邮政编码,所有属性都应该是唯一的,所以这两个字段可以组合起来形成属性的一个 slug。

class PropertyAdmin(admin.ModelAdmin):

form = PropertyForm

fieldsets = (

(None, {'fields': (('address', 'slug')

('city', 'state', 'zip'))})

('Sales Information', {'fields': ('status'

'price')})

('Size', {'fields': ('square_feet'

'acreage')})

)

inlines = (

PropertyFeatureInline

InterestedPartyInline

)

prepopulated_fields = {'slug': ('address', 'zip')}

Note

在管理应用中编辑模型实例时,使用 JavaScript 预先填充了 Slug 字段。这是一个有用的便利,只要缺省的 slug 是合适的,就节省了必须访问单独字段的时间和麻烦。在 Python 中创建对象时,不使用显式值填充字段的唯一方式是通过作为其default参数传入的函数或通过字段的pre_save()方法。

有了这些,剩下的唯一需要建立的模型就是Feature。因为它是比Property更简单的模型,所以管理声明也相当简单。有三个字段需要安排,还有一个SlugField需要配置。

class FeatureAdmin(admin.ModelAdmin):

fieldsets = (

(None, {

'fields': (('title', 'slug'), 'definition')

})

)

prepopulated_fields = {'slug': ('title',)}

admin.site.register(models.Feature, FeatureAdmin)

URL 配置

因为属性的实际管理是由管理界面处理的,所以只需为用户配置 URL 来查看属性列表。这些类型的只读视图最好由 Django 自己的通用视图来处理,这些视图被配置为与所讨论的模型一起工作。具体来说,这些 URL 将使用我们在本章前面用于联系人的相同的通用列表和详细视图。

可使用ListView设置房产列表的视图。这个视图需要一个 QuerySet 来定位条目,这就是PropertyManager有用的地方。它的listed()方法将查询范围缩小到应该向公众显示的项目。

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from properties import models

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Property.objects.listed()

template_name='properties/list.html'

paginate_by=25)

name='property_list')

)

尽管详细视图需要较少的配置选项——因为它不需要paginate_by参数——正则表达式变得有点复杂。在 URL 中查找属性最好由 slug 处理,但是 slug 通常可以由字母、数字和基本标点符号的任意组合组成。属性的 slug 是一种更具体的格式,以地址中的街道号开始,以邮政编码结束。中间的街道名称仍然可以是任何东西,但它总是被数字包围。

这个简单的事实有助于形成用于从 URL 中捕获 slug 的正则表达式。这里的想法是尽可能的具体,这样一个 URL 模式不会干扰其他可能寻找相似模式的模式。单个Property对象的详细视图的 URL 配置如下所示:

from django.conf.urls.defaults import *

from django.views.generic import ListView, DetailView

from properties import models

urlpatterns = patterns(''

url(r'^$'

ListView.as_view(queryset=models.Property.objects.listed()

template_name='properties/list.html'

paginate_by=25)

name='property_list')

url(r'^(?P<slug>\d+-[\w-]+-\d+)/$'

DetailView.as_view(queryset=models.Property.objects.listed()

slug_field='slug'

template_name='properties/detail.html')

name='property_detail')

)

此正则表达式为段的开头和结尾的数字添加了显式规则,用破折号与中间部分隔开。这将像通常的[\w-]+一样匹配属性 slugs,但有一个重要的额外好处:这些 URL 现在可以放在站点的根目录下。拥有更具体的正则表达式允许更小的 URL,如 http://example.com/123-main-st-12345/ 。这是一个保持 URL 小而整洁的好方法,同时不会妨碍其他可能使用 slugs 的 URL 配置。

现在怎么办?

一些应用就位并准备好协同工作后,一个基本的站点就成形了。下一章将展示如何把你到目前为止学到的所有工具结合起来,为这样的应用添加重要的新特性。

Footnotes 1

http://prodjango.com/modelform/

十一、改进应用

Abstract

一旦一个站点有了一组正常工作的基本应用,下一步就是添加更多的高级功能来补充现有的行为。这有时可能只是简单地添加更多的应用,每个应用都为用户和员工提供新的功能。其他时候,有一些方法可以增强您现有的应用,让它们直接增加新的功能,而不需要一个独立的应用。

一旦一个站点有了一组正常工作的基本应用,下一步就是添加更多的高级功能来补充现有的行为。这有时可能只是简单地添加更多的应用,每个应用都为用户和员工提供新的功能。其他时候,有一些方法可以增强您现有的应用,让它们直接增加新的功能,而不需要一个独立的应用。

这些“元应用”或“子框架”的构建目标是使用已经提供的钩子轻松集成到现有的应用中。这本书举例说明了许多这样的挂钩,它们可以结合使用,以达到很好的效果。通常可以编写一个工具来执行许多任务,但只需要在现有的应用中添加一行代码。

添加 API

如今,大多数网站都有一个 API,允许程序员与网站的内容和功能进行交互,而不需要用户甚至网络浏览器。目标是使用结构化、可靠的技术,以简单的方式提供数据,以便代码处理您的数据。Django 基于类的视图提供了多种定制视图行为的方法,这对于生成 API 非常有用,无需自己编写大量新代码。

构建一个 API 需要做一些决定,但并不是所有的决定都需要马上做出。这里有一个小例子,展示了设计 API 时需要回答的一些常见问题。

  • 应该使用什么格式来传输数据?
  • 应该公开哪些类型的数据?
  • 应该如何组织这些数据?
  • 用户访问数据需要认证吗?
  • 用户可以自定义检索哪些数据吗?
  • 用户可以通过 API 修改数据吗?
  • 不同的 API 端点有单独的权限吗?

本章将在第十章中概述的房地产网站的背景下回答其中的一些问题。更好的是,您将看到一个简单框架的示例,它可以添加必要的 API 特性,而不需要直接向应用添加太多东西。可重用性是这些特性长期成功的关键,所以为这样的任务开发一个可配置的工具是理想的。

Caution

本章不会回答所有这些问题。特别是,本章中的例子没有使用任何认证或授权。Django 标准的基于会话的认证不太适合与 API 一起使用,但是您有几个选择。你可以简单地让你的 web 服务器处理认证 1 ,实现一个完整的 OAuth 提供者 2 或者使用其他对你的站点有意义的方法。那些决定和相关的说明超出了本书的范围。

序列化数据

一个很好的起点是为您的数据建立一种使用格式。如今,事实上的标准是 JSON,即 JavaScript 对象符号。它起源于一种在浏览器内部使用数据的简单方法,因为浏览器本身就理解 JavaScript 对象,但它后来成为一种简单、可读和可靠的跨平台数据格式。Python 有自己的工具可以直接读写它,许多其他编程语言也是如此。

事实上,Django 甚至有自己的工具,可以使用 JSON 编写模型实例并再次读取它们。因为它接受内存中的对象并将其转换为可以通过网络发送的字符序列,所以这个过程称为序列化。Django 的序列化工具位于django.core.serializers。使用get_serializer()函数获得 JSON 序列化器相当简单。

要获得序列化器,只需传入您想要使用的序列化方法的名称,您将获得一个可用于序列化对象的类。Django 支持三种序列化格式。

  • json —JavaScript 对象符号
  • xml—可扩展标记语言
  • YAML 不是一种标记语言,如果你安装了 PyYAML 3 就可以使用

>>> from django.core import serializers

>>> JSONSerializer = serializers.get_serializer('json')

无论您选择哪种格式,从这里返回的序列化程序类都以相同的方式工作。本章的其余部分将使用 JSON,但是您应该能够使用 XML 或 YAML,只需做相对较小的修改。

序列化器的用法与 Python 直接提供的更简单的json模块略有不同。Django 的序列化器没有使用dumps()loads()方法,而是分别提供了serialize()deserialize()方法来在 JSON 之间来回转换数据。此外,这些方法适用于适当的 Django 模型实例,而不仅仅是列表和字典这样的本地数据结构。现在,我们只看一下serialize()方法,从您的应用中获取数据,以便其他人可以使用它。

>>> serializer = JSONSerializer()

>>> from contacts.models import Contact

>>> serializer.serialize(Contact.objects.all())

'[{...}, {...}, {...}]'

如果您查看每个序列化联系人的实际输出,您会注意到一些您可能没有预料到的附加信息。Django 的序列化工具旨在产生可以反序列化的输出,而无需事先知道最初序列化了哪些模型和实例。为此,输出包括关于模型本身的一些信息,以及每个实例的主键。每个物体看起来都像这样。

{

"pk": 1

"model": "contacts.contact"

"fields": {

"user": 1

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

}

对于一个 API,您已经知道您正在使用什么模型和 ID,因为它们将被映射为 URL 的一部分。理想情况下,我们可以从中获取fields字典,然后来回发送。虽然没有记录,但是 Django 的序列化器确实提供了一种方法来覆盖它们的行为。首先要意识到get_serializer()的结果实际上是一个类,而不是一个实例对象。这允许您在实例化它之前创建一个子类,产生在子类上重写单个方法的所有好处。我们将把这段代码放在一个名为serializers.py的文件中,因为我们在本章后面还会添加一些文件,所以我们将在一个名为api的包中创建这段代码。最后,我们将能够把这个代码作为api.serializers导入。

from django.core import serializers

class QuerySetSerializer(serializers.get_serializer('json')):

pass

理解如何重写序列化程序需要了解一些序列化程序的工作原理。serialize()方法接受 QuerySet 或任何产生模型实例的 iterable。它遍历该对象,对于找到的每个对象,它遍历其字段,在每一步输出值。通过查看沿途调用的方法,可以很容易地看到整个过程。

  • start_serialization()—设置列表以保存将在流中输出的对象。
  • start_object(obj)—设置一个字典来收集单个对象的信息。
  • handle_field(obj, field)—每个字段被单独添加到对象的字典中。
  • handle_fk_field(obj, field)—使用单独的方法处理外键关系。
  • 与外键一样,多对多关系使用它们自己的方法来处理。
  • end_object(obj)—一旦所有字段都被处理,对象就有机会为其数据完成字典。这是将模型信息和主键值添加到字段中的地方,产生前面显示的输出。
  • get_dump_object(obj)—在 end_object()内部调用,负责定义每个被序列化的对象的实际结构。
  • end_serialization()—一旦所有的对象都被处理,这个方法就完成了流。

我们将应用的第一个定制是简化输出的结构。因为我们将使用 URL 来指示我们正在处理的对象的类型以及它的 ID,所以我们在序列化输出中需要的只是字段的集合。正如我们刚刚看到的过程中所暗示的,这是由get_dump_object()方法处理的。除了它所提供的对象之外,get_dump_object()还可以访问已经由字段处理方法组装的当前数据。该数据存储在 _current 属性中。

get_dump_object()的默认实现将字段数据包装在一个字典中,同时包装的还有对象的主键及其模型的路径和名称。我们在重写的方法中需要做的只是返回当前的字段数据。

class QuerySetSerializer(serializers.get_serializer('json')):

def get_dump_object(self, obj):

return self._current

有了这个简单的方法,您已经可以看到输出的改进。

{

"user"

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

输出单个对象

上一节末尾显示的例子只是列表中的一个条目,因为serialize()只对 iterables 进行操作。对于一个 API 来说,您可能更倾向于从一个细节视图中输出一个单独的对象,它不应该被包装在一个列表中。我们需要一个SingleObjectSerializer,而不是一个QuerySetSerializer。它仍然基于QuerySetSerializer,所以我们可以重用我们在那里添加的所有功能,但只需做足够的修改来处理单个对象。

首先要覆盖的是serialize()方法,因此它可以接受单个对象,但是为了重用所有的序列化行为,它需要用一个列表而不是单个对象来调用它的父方法。这是一个相当简单的覆盖。

class SingleObjectSerializer(QuerySetSerializer):

def serialize(self, obj, **options):

# Wrap the object in a list in order to use the standard serializer

return super(SingleObjectSerializer, self).serialize([obj], **options)

不幸的是,因为这将对象包装在一个列表中,并返回没有任何其他更改的输出,这实际上将仍然输出一个 JSON 字符串中的列表。为了只输出列表中一个对象的值,有必要去掉它周围的列表字符。结果是一个字符串,所以可以使用字符串的strip()方法删除这些字符。

我们可以将这段代码直接放在serialize()方法中,在调用父方法之后,但在最后返回字符串之前,但是 Django 的序列化器还有一个定制点,我们还没有研究过。一旦所有对象都被组装成序列化程序可以处理的格式,就会要求getvalue()方法返回完全序列化的输出。这是放置我们的定制的更好的地方,因为它符合原始方法的意图。将您的重写与原始实现的意图保持一致是一个很好的方法,可以确保将来的更改不会以意想不到的方式破坏您的代码。

class SingleObjectSerializer(QuerySetSerializer):

def serialize(self, obj, **options):

# Wrap the object in a list in order to use the standard serializer

return super(SingleObjectSerializer, self).serialize([obj], **options)

def getvalue(self):

# Strip off the outer list for just a single item

value = super(SingleObjectSerializer, self).getvalue()

return value.strip('[]\n')

这就是我们得到一个新的完全能够处理单个对象的序列化器所需要的。现在,您可以序列化一个对象本身,并获得该对象的输出作为回报。

>>> serializer = SingleObjectSerializer()

>>> from contacts.models import Contact

>>> serializer.serialize(Contact.objects.get(pk=1))

'{...}'

处理关系

查看当前的输出,您会注意到与联系人关联的用户仅由其主键表示。因为 Django 的序列化程序旨在一次重构一个模型,所以它们只包含每个对象所必需的数据。对于 API 来说,包含相关对象的一些细节更有用,最好是在嵌套字典中。

这部分输出由handle_fk_field()方法管理,默认实现只是输出数字 ID 值。我们可以覆盖它来提供对象的细节,但是这需要一个有趣的方法,因为有一个你可能没有想到的问题。Django 的序列化器包装了更多的通用序列化器,并添加了处理 Django 模型所必需的行为,但是这些添加的行为只适用于第一层数据。任何试图在一级 iterable 之外序列化的 Django 模型都会引发一个TypeError,表明它不是一个可序列化的对象。

乍一看,答案似乎是分别序列化相关对象,然后将它们附加到结构的其余部分。这方面的问题是序列化程序的输出是一个字符串。如果您将该输出附加到self._current字典,它将被序列化为一个单独的字符串,其中恰好包含另一个序列化的对象。

所以我们不能让对象不序列化,我们也不能完全序列化它。幸运的是,Django 通过另一个通常没有记录的序列化程序提供了两者之间的路径。'python'序列化器可以接受 Django 对象并生成原生 Python 列表和字典,而不是字符串。这些列表和字典可以在可序列化结构中的任何地方使用,并将产生您所期望的结果。

我们现在需要两个序列化器:一个用于输出整体结构,包括相关对象,另一个用于将字符串输出为 JSON 或您喜欢的任何其他格式。Python 序列化程序将完成大部分工作,我们可以通过将其与基本的 JSON 序列化程序相结合来构建一个更有用的序列化程序。下面是我们现有实现的样子。

class DataSerializer(serializers.get_serializer('python')):

def get_dump_object(self, obj):

return self._current

class QuerySetSerializer(DataSerializer, serializers.get_serializer('json')):

pass # Behavior is now inherited from DataSerializer

注意,get_dump_object()移到了新的DataSerializer中,因为它实际上与 JSON 输出没有任何关系。它的唯一目的是定义输出的结构,这适用于任何输出格式。那也是被覆盖的handle_fk_field()的归属。它有三项任务要完成。

  • 检索相关对象
  • 将其转换成原生 Python 结构
  • 将其添加到主对象的数据字典中

第一点和第三点很简单,但中间的一点看起来有点棘手。我们不能只调用self.serialize(),因为每个序列化器通过_current属性在整个过程中维护状态。我们需要实例化一个新的序列化器,但是我们也需要确保总是使用DataSerializer,而不是意外地获得 JSON 序列化器的一个实例。这是确保它输出原生 Python 对象而不是字符串的唯一方法。

class DataSerializer(serializers.get_serializer('python')):

def get_dump_object(self, obj):

return self._current

def handle_fk_field(self, obj, field):

# Include content from the related object

related_obj = getattr(obj, field.name)

value = DataSerializer().serialize([related_obj])

self._current[field.name] = value[0]

关于这个新方法,另一个值得注意的有趣的事情是,它在序列化相关对象之前将它包装在一个列表中。Django 的序列化器只对 iterable 进行操作,所以当处理单个对象时,您总是需要将它包装在 iterable 中,比如 list。在 Python 序列化器的情况下,输出也是一个列表,所以当把它赋回给self._current时,我们只需要从那个列表中获取第一项。

这样,典型联系人的序列化输出如下所示。

{

"user": {

"username": "admin"

"first_name": "Admin"

"last_name": "User"

"is_active": true

"is_superuser": true

"is_staff": true

"last_login": "2013-07-17T12:00:00.000Z"

"groups": []

"user_permissions": []

"password": "pbkdf2_sha256$10000$..."

"email": "admin@example.com"

"date_joined": "2012-12-04T17:46:00.000Z"

}

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

只需在一个方法中添加几行额外的代码,我们现在就有能力在其他方法中嵌套对象,而且因为它使用了DataSerializer,所以它们可以嵌套任意多的深度。但是在一个User对象中有很多信息,其中大部分并不需要包含在 API 中,并且其中的一些信息——比如密码散列——不应该被泄露。

控制输出字段

Django 再次适应了这种情况,这一次是通过向serialize()方法提供一个fields参数。只需传入一个字段名列表,只有那些字段会被handle_*_field()方法处理。例如,我们可以通过完全排除用户来简化我们的Contact模型的输出。

SingleObjectSerializer().serialize(Contact.objects.get(pk=1), fields=[

'phone_number'

'address'

'city'

'state'

'zip_code'

])

有了这些,输出肯定会变得更简单。

{

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

当然,从输出中删除用户并没有真正的帮助。我们真正需要做的是限制用户对象上的字段,而不是联系人。不幸的是,这是 Django 的序列化程序的意图稍微妨碍我们的另一种情况。就像我们必须拦截handle_fk_field()中用户对象的序列化一样,这也是我们必须为其对serialize()方法的调用提供fields参数的地方。但是每次我们想要指定字段时,都需要重写方法,并对我们想要处理的每个模型进行特殊处理。

一个更通用的解决方案是创建一个模型及其相关字段的注册表。然后,handle_fk_field()方法可以使用它找到的字段列表检查它接收到的每个对象的注册表,如果模型没有注册,则返回到标准序列化。设置注册中心非常简单,注册模型和字段列表组合的功能也是如此。

field_registry = {}

def serialize_fields(model, fields):

field_registry[model] = set(fields)

Note

字段可以作为任何 iterable 传入,但是在内部被显式地放入一个集合中。字段的顺序对于序列化过程来说无关紧要,集合可以更小更快,因为它不担心排序问题。此外,在这一章的后面,我们将能够利用集合的特定行为来使实现的某些部分更容易使用。

有了这个,我们就可以在任何需要为模型指定字段列表的地方导入它,并简单地用适当的映射调用它一次,稍后将需要使用它。使用这个注册表的代码实际上不会放入handle_fk_field()中,因为它只会应用于相关的对象,而不是最外层的对象本身。为了使使用模式更加一致,如果您可以在注册表中指定字段,并为您序列化的每个对象使用这些注册的字段,无论它是否是关系,这将是理想的。

为了支持这个更一般的用例,读取字段注册表的代码可以放在serialize()方法中。它是主对象和相关对象的主要入口点,因此是提供这种额外行为的好地方。

它需要做的第一个任务是确定正在使用的模型。因为可以传入 QuerySet 或标准 iterable,所以有两种方法可以获得传入的对象的模型。最直接的方法利用了 QuerySets 也是可迭代的这一事实,因此您总是可以只获得第一项。

class DataSerializer(serializers.get_serializer('python')):

def serialize(self, queryset, **options):

model = queryset[0].__class__

return super(DataSerializer, self).serialize(queryset, **options)

# Other methods previously described

这对于两种情况都适用,但是它将为每个传入的 QuerySet 进行额外的查询,因为获取第一条记录实际上是一种不同于迭代所有结果的操作。当然,对于非 QuerySet 输入,我们无论如何都需要这样做,但是 query set 有一些我们可以使用的额外信息。每个 QuerySet 上还有一个model属性,它已经包含了用于查询记录的模型,所以如果该属性存在,我们可以使用它来代替。

class DataSerializer(serializers.get_serializer('python')):

def serialize(self, queryset, **options):

if hasattr(queryset, 'model'):

model = queryset.model

else:

model = queryset[0].__class__

return super(DataSerializer, self).serialize(queryset, **options)

# Other methods previously described

因为这不是专门检查一个QuerySet对象,而只是检查是否存在model属性,如果你碰巧有其他产生模型的 iterable,只要 iterable 也有一个model属性,它也能正确工作。

有了模型,很容易在字段列表注册表中执行查找,但重要的是,我们只有在没有提供fields参数时才这样做。像这样的全局注册应该总是很容易在特定情况下被覆盖。如果提供了 fields 参数,它将出现在选项字典中,这也是我们放置从注册表中找到的字段列表的地方。因此,添加这部分流程也变得非常简单。

class DataSerializer(serializers.get_serializer('python')):

def serialize(self, queryset, **options):

if hasattr(queryset, 'model'):

model = queryset.model

else:

model = queryset[0].__class__

if options.get('fields') is None and model in field_registry:

options['fields'] = field_registry[model]

return super(DataSerializer, self).serialize(queryset, **options)

# Other methods previously described

Note

第二个if块可以一眼看上去很奇怪,但是不能简单的查看'fields'是否存在于options字典中。在某些情况下,可以显式传入一个None,其行为应该与参数被完全忽略的情况相同。考虑到这一点,我们使用get(),如果没有找到,则返回到None,然后我们手动检查None,以确保我们捕捉到所有正确的案例。特别是,提供一个空列表仍然应该覆盖任何已注册的字段,所以我们不能只使用布尔值not

现在serialize()将自动为它已经知道的任何模型注入一个字段列表,除非被定制的fields参数覆盖。这意味着在尝试序列化任何东西之前,你必须确保注册你的字段列表,但是正如你将在本章后面看到的,这在你的 URL 配置中很容易做到。还要注意的是,如果模型没有被分配一个字段列表,你自己也没有指定一个,那么这个更新将简单地不指定fields参数,回到我们之前看到的默认行为。

有了这些,我们可以轻松地为我们的ContactUser模型定制字段列表。我们不需要特别定制Contact,因为我们想要包含它的所有字段,但是出于演示的目的,这里也包含了它。此外,显式比隐式好,在这里指定一切有助于记录 API 的输出。

from api import serialize_fields

from contacts.models import Contact

from django.contrib.auth.models import User

serialize_fields(Contact, [

'phone_number'

'address'

'city'

'state'

'zip_code'

'user'

])

serialize_fields(User, [

'username'

'first_name'

'last_name'

'email'

])

有趣的是,这些字段列表大多与我们在第十章中创建的表单中已经提供的字段相匹配。表单还保留了一个自己的字段列表,所以我们实际上可以使用表单字段名称来重写这些注册,这有助于我们避免重复。这让表单主要负责哪些字段对最终用户有用,API 只是简单地跟着做。我们需要做的唯一改变是添加回Contact模型的user属性,因为在表单场景中处理方式不同。

from api import serialize_fields

from contacts.models import Contact

from django.contrib.auth.models import User

from contacts.forms import ContactEditorForm, UserEditorForm

serialize_fields(Contact, ContactEditorForm.base_fields.keys() + ['user'])

serialize_fields(User, UserEditorForm.base_fields.keys())

现在,当我们使用SingleObjectSerializer序列化一个Contact对象时,有了这些新的变化,它最终看起来像你所期望的那样。

{

"user": {

"username": "admin"

"first_name": "Admin"

"last_name": "User"

"email": "admin@example.com"

}

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

多对多关系

到目前为止,该 API 将输出您可能需要的几乎所有内容,缺少的主要特性是多对多关系。handle_fk_field()方法将只处理指向每个记录一个对象的简单外键,而多对多关系将产生一个相关对象的列表,所有这些对象都需要序列化并插入到 JSON 字符串中。

正如本章前面所概述的,序列化器也有一个handle_m2m_field()方法,我们可以用它来定制它们如何处理这些更复杂的关系。从技术上来说,这些关系已经得到了轻微的处理,但只是以与外键最初相同的方式。每个相关的对象将仅仅产生它的主键值,其他什么都没有。为了从这些关系中获得更多信息,我们需要应用一些与外键相同的步骤。

外键处理的第一个变化是引用相关对象的属性不是 object 或 QuerySet 本身;这是一个 QuerySet 管理器。这意味着它本身是不可迭代的,因此不能被直接序列化,所以我们必须调用它的all()方法来获得一个 QuerySet。然后,我们可以直接通过标准的serialize()方法传递它,而不是将它包装在一个列表中。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

# Include content from all related objects

related_objs = getattr(obj, field.name).all()

values = DataSerializer().serialize(related_objs)

self._current[field.name] = values

准备好之后,如果我们将'groups'字段添加到User对象的注册表中,联系人看起来会是什么样子。

{

"user": {

"username": "admin"

"first_name": "Admin"

"last_name": "User"

"email": "admin@example.com"

"groups": [

{

"name": "Agents"

"permission_set": [...]

}

{

"name": "Buyers"

"permission_set": [...]

}

{

"name": "Sellers"

"permission_set": [...]

}

]

}

"address": "123 Main Street"

"city": "Los Angeles"

"state": "CA"

"zip_code": "90210"

"phone_number": "123-456-7890"

}

当然,在这种情况下,权限没有多大意义,所以您可能希望从User对象的字段列表中删除这些权限,但是除此之外,这看起来是另一个非常简单的解决方案,可以帮助我们继续前进。不幸的是,Django 中的多对多关系还有一个特点,它让事情变得更加复杂。

当指定多对多关系时,可以选择指定一个“直通”模型,该模型可以包含有关该关系的一些附加信息。这些信息并不直接附属于任何一个模型,而是两者之间关系的一部分。我们刚刚为handle_m2m_field()应用的简单方法完全忽略了这个特性,所以我们的输出中不会包含任何额外的信息。

记得从第十章的中,我们的Property模型通过多对多的关系与FeatureContact相关联,并且它们中的每一个都使用了through参数来包含一些额外的信息字段。如果你试图用我们现在的代码序列化一个Property对象,你会看到下面的内容。

{

"status": 2

"address": "123 Main St."

"city": "Anywhere"

"state": "CA"

"zip": "90909"

"features": [

{

"slug": "shed"

"title": "Shed"

"definition": "Small outdoor storage building"

}

{

"slug": "porch"

"title": "Porch"

"definition": "Outdoor entryway"

}

]

"price": 130000

"acreage": 0.25

"square_feet": 1248

}

正如您所看到的,所列出的特性只包括关于所提到的特性的生成类型的信息。这个定义简单地解释了棚子和门廊的一般含义,但是对于这个特殊的属性没有任何具体的特征。这些细节只存在于PropertyFeature关系表中,该表目前被忽略。让我们来看看我们希望用它来做什么,以便更好地理解如何到达那里。我们正在寻找的字段存储在一个中间的PropertyFeature模型中,但是我们希望将它们包含进来,就好像它们直接在Feature模型中一样。这需要将Feature实例和PropertyFeature实例的属性合并到一个字典中。

获取适当的字段

我们遇到的第一个问题是,PropertyFeature模型上的字段比我们真正想要包含的要多。它包括与PropertyFeature相关的两个ForeignKey字段,这实际上只是为了支持关系,并没有添加任何有用的信息,这些信息是我们用前面显示的简单方法无法获得的。我们不想包括那些,或者它的自动主键。其他的都是有用的信息,但是我们需要一种方法来识别哪些字段是有用的,哪些是无用的。

为了获得这些信息,我们将从一个助手方法开始,该方法可以查看PropertyFeature模型中的字段,并根据它们的用途组织它们。有四种类型的字段,每一种都可以通过我们可以在代码中自省的不同东西来标识。

  • 自动递增主键将是AutoField的一个实例。这个字段对我们没有任何用处,所以一旦找到它,可以放心地忽略它。
  • 一个外键指向我们正在使用的主模型。在这种情况下,它指向Property模型。这可以被识别为ForeignKey的一个实例,其rel.to属性的值与传入的对象的类相匹配。我们称之为source场。还有一个指向相关模型的外键,在本例中是Feature。这可以被识别为ForeignKey的一个实例,其rel.to属性的值与传递给handle_m2m_field()ManyToMany字段上的rel.to属性相匹配。让我们称之为target字段。
  • 最后,不属于其他三个类别的任何其他字段包含关于关系本身的信息,这些是我们正在努力收集的信息。我们称之为extra字段。

有了这些规则,新的get_through_fields()方法就相当简单了。它只需要查看关系模型上的所有字段,并根据这些规则识别每个字段,返回我们需要在handle_m2m_field()中处理的字段。

from django.db.models import AutoField, ForeignKey

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def get_through_fields(self, obj, field):

extra = set()

for f in field.rel.through._meta.fields:

if isinstance(f, AutoField):

# Nothing to do with AutoFields, so just ignore it

continue

if isinstance(f, ForeignKey):

# The source will refer to the model of our primary object

if f.rel.to == obj.__class__:

source = f.name

continue

# The target will be the same as on the ManyToManyField

if f.rel.to == field.rel.to:

target = f.name

continue

# Otherwise this is a standard field

extra.add(f.name)

return source, target, extra

获取关系信息

现在我们已经有了我们需要的字段,这个过程的核心是分别找到每个关系,并从中提取适当的信息。我们将一次构建这个新版本的handle_m2m_field()几行代码,这样就更容易看到所有的部分在这个过程中组合在一起。

首先,我们需要检索适用于该任务的所有字段信息。上一节为从关系模型中获取信息做好了准备,但是我们还需要包含在序列化输出中的字段列表。我们没有使用标准流程序列化PropertyFeature,所以它不能像其他模型一样使用字段注册表。此外,我们将在由Feature引用的结构中一起返回来自FeaturePropertyFeature的所有数据,所以如果我们允许配置在指定Feature时指定两个模型的所有字段,那会更好。例如,要获取特性的标题及其对当前属性的描述,我们可以在一行中注册它们。

api.serialize_fields(Feature, ['title', 'description'])

title字段将来自Feature模型,而description来自PropertyFeature,但是这允许实现细节被更好地隐藏起来。调用get_through_fields()非常容易,检索注册字段列表与在handle_fk_field()中一样,只有一个小的例外。如果没有已经注册的字段列表,我们可以使用从调用get_through_fields()返回的extra字段。我们必须确保在默认情况下指定一些东西,因为否则自动主键和那两个额外的外键也会被序列化,即使它们在这里没有用。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

接下来,我们准备迭代当前对象的所有关系。这种方式的工作原理与看起来有点不同,因为访问多对多关系的简单方法不会返回任何额外的关系信息。为此,我们需要直接查询关系模型,只过滤那些source引用了传递给handle_m2m_field()的对象的结果。同时,我们还可以建立一个列表来存储从这些关系中检索到的数据。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

现在我们已经准备好开始遍历这些关系,并从每一个关系中提取必要的信息。第一步是根据我们之前找到的字段列表实际序列化相关的模型。例如,这将添加来自Feature模型的title数据。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

for relation in relationships.select_related():

# Serialize the related object first

related_obj = getattr(relation, target)

data = DataSerializer().serialize([related_obj])[0]

请注意,我们需要将对象包装在一个列表中进行序列化,然后从结果列表中获取第一项。我们之前创建了一个SingleObjectSerializer,但这只是为了作为一个更加公共的接口与 JSON 输出一起工作。我们只在一种方法中这样做,所以不值得创建另一种单对象变体来处理原生 Python 数据结构。

sourcetarget字段已经被证明是有用的,我们现在有了一个data字典,包含了我们需要的一些内容。为了从关系模型中获得其余的信息,我们查看extra字段。然而,我们不一定需要所有的人。我们只需要获得那些也包含在字段列表注册表中的。这就是我们将它们都存储为集合变得非常有用的地方。我们可以使用&操作符来执行一个简单的交集操作,只得到两个地方都有的字段。对于我们找到的每一个值,我们只需将它与其他值一起添加到data字典中。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

for relation in relationships.select_related():

# Serialize the related object first

related_obj = getattr(relation, target)

data = DataSerializer().serialize([related_obj])[0]

# Then add in the relationship data, but only

# those that were specified in the field list

for f in fields & extra_fields:

data[f] = getattr(relation, f)

现在剩下的就是将所有这些数据添加到对象列表中,并将整个集合添加到序列化程序用来跟踪当前对象的字典中。

class DataSerializer(serializers.get_serializer('python')):

# Other methods previously described

def handle_m2m_field(self, obj, field):

source, target, extra_fields = self.get_through_fields(obj, field)

fields = field_registry.get(field.rel.to, extra_fields)

# Find all the relationships for the object passed into this method

relationships = field.rel.through._default_manager.filter(**{source: obj})

objects = []

for relation in relationships.select_related():

# Serialize the related object first

related_obj = getattr(relation, target)

data = DataSerializer().serialize([related_obj])[0]

# Then add in the relationship data, but only

# those that were specified in the field list

for f in fields & extra_fields:

data[f] = getattr(relation, f)

objects.append(data)

self._current[field.name] = objects

现在,当我们序列化我们之前看到的同一个Property对象时,您可以看到它将来自FeaturePropertyFeature的信息包含在一个字典中,这使它成为我们系统中更有用的数据表示。

{

"status": 2

"address": "123 Main St."

"city": "Anywhere"

"state": "CA"

"zip": "90909"

"features": [

{

"title": "Shed"

"description": "Small woodshed near the back fence"

}

{

"title": "Porch"

"description": "Beautiful wrap-around porch facing north and east"

}

]

"price": 130000

"acreage": 0.25

"square_feet": 1248

}

检索数据

有了我们的数据结构,下一个合乎逻辑的步骤是从数据库中检索数据并适当地呈现它。这是视图的工作,特别是我们可以从基于类的视图中获得很多好处。在内部,基于类的视图由几个插件组成,这些插件根据需要组合在一起以构建更有用的类。我们将从创建自己的混音开始,名为ResourceView。这段代码将放在api包中一个名为views.py的新文件中,与前面章节中的序列化代码放在一起。

资源视图

与大多数基于类的视图一样,这里的大部分工作是允许为每个用例定制它的行为。因为ResourceView的目的是序列化一个或多个对象,所以我们可以赋予它接受用于执行该步骤的serializer的能力。另外,我们还将通过添加它自己的serialize()方法使它更容易使用,这样你就不用担心直接访问序列化器了。

from django.views.generic import View

class ResourceView(View):

serializer = None

def serialize(self, value):

return self.serializer.serialize(value)

注意,默认情况下,serializer被设置为标准的 JSON 序列化程序。对于 QuerySets 和单个对象,我们有不同的序列化器,此时,没有办法知道使用哪一个。与其抛硬币来决定哪种用法更常见,不如暂时不要定义它,而需要在子类或单独的 URL 配置中指定它。

现在,serialize()调用缺少的一点是指定输出字段的能力。我们的序列化代码中有相当一部分是为了支持该特性而设计的,所以ResourceView应该将该行为暴露给各个 URL 进行定制。在这里使用None作为缺省值将会自动序列化所提供的任何模型上的所有可用字段。

from django.views.generic import View

class ResourceView(View):

serializer = None

fields = None

def get_fields(self):

return self.fields

def serialize(self, value):

return self.serializer.serialize(value, fields=self.get_fields())

我们在这里使用了一个get_fields()方法,而不仅仅是原始属性访问,因为 mix-in 旨在以我们可能不期望的方式被子类化。在本章的后面部分,您将看到一个子类,它需要通过添加一个在没有指定fields时使用的回退来改变字段的检索方式。我们可以考虑在子类中使用一个property来代替一个方法,但是如果它的未来子类需要再次覆盖那个行为,特别是如果它想要建立在它的父类的行为之上,那么这会导致它自己的一系列问题。一般来说,方法是处理子类化行为的一种更直接的方式,所以它们非常适合像这样的混合类。

资源列表视图

现在我们可以开始处理一个实际上有用的真实视图了。因为ResourceView只是一个提供一些新选项和方法的组合,所以我们可以将它与我们想要使用的几乎任何其他 Django 视图结合起来。对于最基本的情况,我们可以使用 Django 自己的ListView来提供一个对象集合,并简单地将它们序列化,而不是呈现一个模板。

因为ListView是基于TemplateView的,所以它已经包含了一个方法,通过渲染模板的方式将给定的context字典渲染到HttpResponse中。我们没有呈现模板,但是我们确实需要返回一个HttpResponse,上下文已经给了我们这样做所需要的一切。这允许我们使用定制的render_to_response()来使用 JSON 序列化程序代替模板渲染器以获得正确的结果。

首先,我们需要指定我们想要使用的序列化程序,因为默认的ResourceView没有指定序列化程序。

from django.views.generic import View, ListView

from api import serializers

# ResourceView is defined here

class ResourceListView(ResourceView, ListView):

serializer = serializers.QuerySetSerializer()

接下来,我们可以重写render_to_response()方法。这将需要执行三个步骤:

  • 从提供的上下文中获取对象列表
  • 将这些对象序列化为一个 JSON 字符串
  • 返回一个合适的HttpResponse

鉴于我们已经具备的特性,前两步很容易做到。不过最后一步不能只是一个标准的HttpResponse。我们需要定制它的Content-Type,向 HTTP 客户端表明内容由一个 JSON 值组成。我们需要的值是application/json,可以使用HttpResponsecontent_type参数设置。所有这些步骤组合成一个非常短的函数。

from django.http import HttpResponse

from django.views.generic import View, ListView

from api import serializers

# ResourceView is defined here

class ResourceListView(ResourceView, ListView):

serializer = serializers.QuerySetSerializer()

def render_to_response(self, context):

return HttpResponse(self.serialize(context['object_list'])

content_type='application/json')

信不信由你,这就是通过你的新 API 提供一个 JSON 对象列表的全部内容。所有对ListView可用的选项在这里也是可用的,唯一的区别是输出将是 JSON 而不是 HTML。下面是一个相关联的 URL 配置可能的样子,因此您可以看到各种序列化特性是如何组合起来实现这一点的。

from django.conf.urls import *

from django.contrib.auth.models import User, Group

from api.serializers import serialize_fields

from api.views import ResourceListView

from contacts.models import Contact

from contacts import forms

serialize_fields(Contact, forms.ContactEditorForm.base_fields.keys() + ['user'])

serialize_fields(User, forms.UserEditorForm.base_fields.keys())

serialize_fields(Group, ['name'])

urlpatterns = patterns(''

url(r'^$'

ResourceListView.as_view(

queryset=Contact.objects.all()

), name='contact_list_api')

)

资源详细视图

接下来,我们需要为我们的模型提供一个详细的视图,它的工作方式与上一节描述的一样。事实上,只有三点不同:

  • 我们需要子类化DetailView而不是ListView
  • 我们用SingleObjectSerializer代替QuerySetSerializer
  • 我们需要的上下文变量被命名为'object'而不是'object_list'

有了这三个变化,这里的ResourceListViewResourceDetailView一起在api包的views.py文件中。

from django.http import HttpResponse

from django.views.generic import View, ListView, DetailView

from api import serializers

# ResourceView is defined here

class ResourceListView(ResourceView, ListView):

serializer = serializers.QuerySetSerializer()

def render_to_response(self, context):

return HttpResponse(self.serialize(context['object_list'])

content_type='application/json')

class ResourceDetailView(ResourceView, DetailView):

serializer = serializers.SingleObjectSerializer()

def render_to_response(self, context):

return HttpResponse(self.serialize(context['object'])

content_type='application/json')

此外,这里是上一节 URL 配置的延续,扩展后还包含了对ResourceDetailView的引用。

from django.conf.urls import *

from django.contrib.auth.models import User, Group

from api.serializers import serialize_fields

from api.views import ResourceListView, ResourceDetailView

from contacts.models import Contact

from contacts import forms

serialize_fields(Contact, forms.ContactEditorForm.base_fields.keys() + ['user'])

serialize_fields(User, forms.UserEditorForm.base_fields.keys())

serialize_fields(Group, ['name'])

urlpatterns = patterns(''

url(r'^$'

ResourceListView.as_view(

queryset=Contact.objects.all()

), name='contact_list_api')

url(r'^(?P<slug>[\w-]+)/$'

ResourceDetailView.as_view(

queryset=Contact.objects.all()

slug_field='user__username'

), name='contact_detail_api')

)

现在怎么办?

本章展示的 API 只是一个开始,它提供了对一些模型的匿名只读访问。您可以在许多不同的方向上扩展它,添加诸如认证、外部应用授权之类的东西,甚至使用 Django 的表单通过 API 更新数据。

本书中讨论的工具和技术远远超出了 Django 官方文档的范围,但是仍然有很多内容没有探索。使用 Django 和 Python 还有很多其他的创新方法。

当您开发自己的应用时,一定要考虑回馈 Django 社区。该框架可用是因为其他人决定免费分发它;通过这样做,你可以帮助更多的人发现更多的可能性。

Footnotes 1

http://prodjango.com/remote-user

2

http://prodjango.com/oauth

3

http://prodjango.com/pyyaml

posted @ 2024-08-13 14:27  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报