Django 高级教程(一)
原文:Pro Django
一、了解 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
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
11
11 http://prodjango.com/irc-faq/
12
12 http://prodjango.com/django-users/
13
14
二、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()函数采用单个参数,返回True或False,表示该对象是否可以作为函数调用。
类基本(对象):
...及格
...
类可调用(对象):
...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 提供了两种特殊的方法来定义函数参数,这两种方法允许函数接受未由显式声明的参数处理的多余参数。这些“额外的”参数将在下面解释。
注意名字args和kwargs仅仅是 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.functional的curry函数中提供了自己的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—包含传递给函数的关键字参数的字典
然而,要认识到的一件重要的事情是,装饰器接收的args和kwargs是作为位置参数传递的,没有通常的星号符号。然后,当将它们传递给包装的函数时,必须使用星号符号来确保函数接收到它们,而不必知道装饰器是如何工作的。
描述符
通常,引用对象上的属性会直接访问属性值,没有任何复杂性。获取和设置属性会直接影响对象的实例命名空间中的值。有时,在访问这些值时必须做额外的工作。
- 从复杂的源中检索数据,如数据库或配置文件
- 将简单的值转换为复杂的对象或数据结构
- 自定义它所附着的对象的值
- 在保存到数据库之前,将值转换为可存储的格式
在一些编程语言中,通过创建额外的实例方法来访问那些需要它的属性,这种类型的行为成为可能。虽然这种方法很实用,但会导致一些问题。首先,这些行为通常更多地与属性中存储的数据类型相关联,而不是与它所附加到的实例的某些方面相关联。通过要求对象提供访问此数据的附加方法,包含此行为的每个对象都必须在其实例方法中提供必要的代码。
另一个重要问题是,当一个曾经简单的属性突然需要这种更高级的行为时会发生什么。当从简单属性更改为方法时,对该属性的所有引用也需要更改。为了避免这种情况,这些语言的程序员采用了一种标准的做法,即总是为属性访问创建方法,这样对底层实现的任何更改都不会影响任何现有的代码。
为了改变一个属性的访问方式而修改那么多代码并不有趣,所以 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参数可以用来确定描述符是从一个对象还是它的类中访问的。如果instance是None,那么属性是从类而不是从实例中访问的。如果描述符被以不应该的方式访问,这可以用来引发一个异常。
此外,通过定义此方法,您可以让描述符负责检索一个值并将其返回给请求它的代码。否则将迫使 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 代码检索关于程序员所写内容的信息,而不需要程序员重新编写。
本节描述的一些特性依赖于一个强大的标准库模块inspect。inspect模块提供了方便的功能来执行高级自检。
这里只详细介绍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)—如果cls和base相同,或者如果cls继承了base的某个祖先,则返回Trueisinstance(obj, base)—测试对象是否是base的实例或其任何祖先
class CustomDict(dict):
...pass #假装这里有更有用的东西
...
issubclass(CustomDict,Dict)
真实的
issubclass(CustomDict,CustomDict)
真实的
my_dict = CustomDict()
实例(my_dict,dict)
真实的
事件(my_dict,CustomDict)
真实的
issubclass和isinstance有明确的关系: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_field是True,这将是一个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和一个accounts的app_label将会是"accounts_account"。db_tablespace—对于 Oracle 以及将来可能出现的其他数据库后端,表可以放在磁盘的不同部分,或者完全放在不同的磁盘上。默认情况下,这只是一个空字符串,它告诉数据库将表存储在默认位置。对于不支持该选项的后端,该选项将被忽略。get_latest_by—基于日期的字段的名称,例如DateField或DateTimeField,它应该用于确定模型的最新实例。如果未提供,这将是一个空字符串。order_with_respect_to—与另一个模型相关的字段的实例,在对该模型的实例进行排序时使用。这默认为None,这意味着模型的排序完全由模型本身的字段决定,而不是由任何相关的模型决定。ordering—包含在对模型的实例进行排序时要使用的字段名称的元组。默认情况下,这是一个空元组,它依赖于数据库来确定模型实例的排序。permissions—要添加到模型中的附加权限的元组序列。序列中的每个元组包含两个值,第一个值是要在代码和数据库中使用的权限的名称,第二个值是为用户或组选择权限时要在管理界面中显示的文本。unique_together—指示任何字段组的元组序列,当组合时,必须仅在数据库中的一个记录中使用。序列中的每个元组都包含对于特定索引必须是唯一的字段名称。多个元组彼此之间没有任何关系;它们各自代表数据库级别的一个单独的索引。verbose_name-模型的单个实例的显示名称。默认情况下,这是由类本身的名称决定的,通过将每个大写部分拆分成一个单独的非大写单词;Article会变成"article",而AddressBook会变成"address book"。verbose_name_plural-模型的多个实例的显示名称。默认情况下,这只是结尾带有“s”的verbose_name。Article会是"articles",AddressBook会是"address books"。verbose_name_raw—verbose_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()将返回True或False,这取决于是否所有已安装的应用都已被正确处理。利用这一点,可以从拥有自己的已知模型缓存中受益的应用可以检查该缓存是否可用于该目的。如果是,它可以直接使用这个缓存,如果不是,它可以手动确定它需要知道什么。
检索所有应用
当想要反思一个站点的内容时,看看应用本身的结构也是非常有用的。毕竟,只有在有模型可看的情况下,查看模型才是有用的,有时需要收集当前使用的所有模型。按照声明它们的应用来排列它们也很有用。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异常,但是如果改为给出True,cache.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属性相同。在其他情况下,当实际的字段名被访问时,向其他代码公开一个更复杂的对象,比如另一个模型实例,会更合适。对于这些情况,attname和name会有所不同,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—一个布尔值,指示字段是否允许空字符串作为可能的值。这不是作为特定字段实例的配置指定的选项,而是在字段的类本身中定义的。许多字段,如CharField和EmailField,将空字符串与None分开处理,因此该属性允许后端决定如何为数据库(如 Oracle)处理空字符串,否则这些数据库可能会失去这种区别。help_text—在字段定义中提供的信息性文本,当字段呈现以供编辑时向用户显示。这将被传递给基于模型生成的表单,比如提供的管理界面。max_length—字段值可以包含的最大长度。大多数基于字符串的字段,比如CharField和EmailField,都使用这个来限制字符串内容的长度,无论是在表单字段还是底层数据库列中。其他字段类型,比如IntegerField和DateField,可以忽略它,因为它在这些情况下没有任何意义。name—字段的名称,在将字段分配给模型时定义。这被设置为contribute_to_class()过程的一部分,通过避免两次输入名字来保持干燥。这将是属性的名称,将在该属性中分配和检索字段的本地 Python 值。与此形成对比的是attname,它存储了填充name所需的原始数据。通常,这两个值是相同的,但是对于它们不同的情况,理解它们的区别是很重要的。null—一个布尔值,指示字段是否可以在没有赋值的情况下提交到数据库。这主要控制如何创建底层数据库列,但是只要语义保持不变,一些应用可能会找到其他用途。primary_key—一个布尔值,指示该字段是否应该用作数据库表的主键。除了指示数据库生成主键索引之外,Django 还使用这个指示器来确定在查找特定实例时使用哪个字段的值,比如通过外键关系查找相关对象。参见本章前面的“主键”一节,详细了解用于确定哪个字段的值设置为True的_meta.pk快捷键。rel—对于将一个模型与另一个模型相关联的字段,这将是一个描述该关系的各个方面的特殊对象。对于所有非关系字段类型,这将被设置为None。serialize—一个布尔值,指示当使用序列化框架序列化模型实例时是否应包含该字段。3unique-一个布尔值,表示该字段在模型的所有实例中必须是唯一的。这主要用于在数据库中创建适当的约束来强制执行该条件,但它也可以由应用使用。例如,提供关于用户输入的值对于模型是否有效的详细反馈的内容编辑应用在做出该决定时也可以考虑这一点。unique_for_date—与日期相关的字段的名称,例如DateField或DateTimeField,其值应该是唯一的。这在本质上类似于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参数根据需要设置字段的name、attname、column和verbose_name属性。对于attname和column值,该方法遵从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 自己的行为将确保没有任何东西被分配给类本身。
contribute _ to _ related _ class(self,cls,related)
对于与其他模型相关联的字段,一旦相关模型可用,就会调用这个函数,这样属性也可以添加到该模型中。例如,当应用了ForeignKey时,Django 就是这样在相关类上提供一个反向属性的。
它接收的两个参数是cls,关系实际应用到的模型类,和related,关系指向的模型,其中可能还需要应用其他属性。像contribute_to_class()一样,它不应该返回任何东西,因为它无论如何都会被忽略。
改变数据行为
鉴于大多数字段类型都是为了与特定的数据类型交互而存在的,首先要考虑的事情之一就是如何告诉 Django 处理该数据类型。这包括如何将其存储在数据库中,如何确保其值的有效性,以及如何用 Python 表示该值。这些是现场行为的一些最基本的方面,适当地改变它们可以打开一个可能性的世界。
get_internal_type(自身)
该方法返回一个字符串,该字符串有助于确定数据库应该如何存储字段的值。字符串本身不是实际的数据库列类型,而是应用于数据库后端提供的映射,以确定使用哪种类型的列。通过这种方式,可以编写字段,而不必绑定到特定的数据库后端。
因为该函数的返回值被应用到一个已知的类型字典来检索数据库列名,所以该值必须是该字典中的一个有效条目。因此,这里列出了一组有限的可能返回值。
AutoFieldBigIntegerFieldBooleanFieldCharFieldCommaSeparatedIntegerFieldDateFieldDateTimeFieldDecimalFieldFileFieldFilePathFieldFloatFieldImageFieldIntegerFieldIPAddressFieldNullBooleanFieldOneToOneFieldPositiveIntegerFieldPositiveSmallIntegerFieldSlugFieldSmallIntegerFieldTextFieldTimeField
验证(自身、值、实例)
当检查模型值的准确性时,此方法用于确定字段的内容是否正确。它接收的参数是字段本身的值,以及包含所有字段的模型。这使得它不仅可以验证字段本身的值,还可以验证它在更大模型的上下文中的意义。
为什么这在验证单个字段的值时有用应该是显而易见的,但是使用模型的其余值有什么价值就不太清楚了。毕竟,当编写一个字段时,通常无法知道它旁边还会使用哪些字段。
然而,有时一个字段可能是专门为一个特定的模型编写的,因此可以预先知道整个模型看起来是什么样子。例如,在这些情况下,该字段可以检查一个人拥有什么类型的帐户,因为该字段的最大值取决于其他字段。
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."))
如您所见,它必须检查几种不同类型的值,这些值都可以可靠地强制转换为布尔值。除了本机的True和False,它还检查相同的字符串表示,以及在各种情况下可能出现的两个单字符表示。如果找到了合适的值,它会简单地返回合适的本地布尔值,如果找不到合适的值,则会引发上一节中描述的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.subclassing,SubfieldBase元类是 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 每个都有自己的期望。这是完整的列表,包括每个列表的用途:
exact和iexact—提供的值必须与数据库中的值完全匹配,iexact不区分大小写。Django 假设一个没有查找类型的过滤器表示exact,它将被传递给get_prep_lookup()。contains和icontains—提供的值必须至少存在于数据库中的部分值中,icontains不区分大小写。gt和gte—数据库值必须大于提供给查找的值,而gte也允许值相等。lt和lte—数据库值必须比提供给查找的值小,而lte也允许值相等。in—数据库值必须至少与作为查找值提供的列表中的一个值完全匹配。startswith和istartswith—数据库值必须以作为查找值提供的字符串开头,istartswith不区分大小写。endswith和iendswith—数据库值必须以作为查找值提供的字符串结尾,iendswith不区分大小写。range—数据库值必须具有由作为查找值提供的开始和结束限制的二元组指定的范围。year、month和day—数据库值必须包含指定的查找值作为其年、月或日部分,具体取决于所使用的查找类型。这仅对日期有效。isnull—数据库值必须等于NULL才能匹配。search—数据库值必须通过全文索引搜索。这只对 MySQL 有效,并且只在数据库被修改为启用必要的索引时有效。regex和iregex—数据库值必须与作为查找值提供的正则表达式指定的格式匹配,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()执行基本相同的任务,除了它的输出将被直接输入数据库查询。它接收相同的参数,但添加了connection和prepared,其工作方式与传递给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参数来暗示。FileField的contribute_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.files的File类提供了许多独立于平台和存储的方法,用于访问文件的内容和内容的属性,以及保存新文件和删除现有文件。
因为它是用于访问文件的面向公众的 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_save和post_save还有一个非常重要的区别,因为它们并不总是被称为一对。因为pre_save是在流程开始时触发的,所以您可以可靠地假设每次启动save()时都会调用它。但是,post_save只在最后发生,所以如果保存过程中出现任何问题,post_save不会被触发。
这是一个重要的区别,因为为模型保存信号注册一对侦听器似乎很方便,期望每次都调用这两个侦听器。虽然对于大多数情况来说这可能是真的,当然当没有出错时,有时事情确实会出错。示例包括具有重复主键或其他唯一列的条目、数据类型错误或数据库连接超时。
在需要这种类型行为的情况下,唯一合理合理的方法是覆盖模型上的save()方法。这允许在实际的数据库交互之前和之后运行定制代码,但是它也提供了一种方法来识别在这个过程中发生的问题。此外,它允许代码有更好的机会更充分地将两部分功能配对,因为如果确实出错了,就更容易识别,因此任何未完成的操作都可以被取消。
删除前和删除后
类似于上一节的精神,pre_delete和post_delete是与模型实例的删除相关的一对信号。除了都只提供了sender和instance参数之外,它们的功能与储蓄功能几乎相同。
使用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信号,即使没有任何变化。因此,除了sender,post_syncdb的听众还会收到三个额外的参数来更详细地指出syncdb被调用的情况,并帮助控制他们的响应行为。
app—应用对象(其models模块)代表刚刚与数据库同步的应用。这与sender参数完全相同,但是在这里命名为app是为了让监听器函数更具可读性。created_models—Pythonset,包含在syncdb执行期间实际安装到数据库中的应用的所有模型。这就是监听器如何识别那些新模型的方法,这通常是post_syncdb处理者需要知道的最重要的事情。这将一直被提供,但是在没有新东西的应用中,它将只是一个空的set。verbosity—一个整数,标识执行syncdb的用户所请求的详细级别。有效值为0、1和2,其中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 对象来表示给定值有时会非常昂贵。更糟糕的是,应用的某些部分甚至可能不使用该对象,尽管模型的其余部分可能是必需的。现实世界中的一些例子是复杂的地理表示或嵌套对象的大树。
在这些情况下,我们必须能够在必要时访问完整的对象,但是如果不使用该对象,那么不构造它对性能非常重要。理想情况下,当模型被实例化时,数据将从数据库中加载,但是原始值将位于实例中,而不会被加载到整个对象中。当属性被访问时,它将在该点被构造,然后被缓存,这样以后的访问就不必不断地重新构造对象。
再次回顾《??》第二章,描述符是完成这项任务的完美工具,因为它们允许代码在访问属性的准确时刻运行。必须注意确保完整构造的对象被正确缓存以备将来使用,但是通过使用单独的name和attname,这也相当简单。
为了说明这在实践中是如何工作的,考虑一个用于存储和检索任意 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.")
注意pickle和cPickle只支持 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()的参数就相当容易了。特别要记住,构造一个类需要三个参数:name、bases和attrs。模型的名字被清晰地映射到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
10
10 http://prodjango.com/pickle/
11