Django是当今最受欢迎的Web框架之一。它为大型网站提供动力,例如Pinterest、Instagram、Disqus和NASA。只需几行代码,您就可以快速构建一个功能齐全且安全的网站,可以扩展到数百万用户。
本书是这些模式和见解的集合。它分为几章,每章涵盖框架的一个关键领域,例如模型,或Web开发的一个方面,例如调试。重点是构建清洁、模块化和更易维护的代码。
我们已经尽力提供最新信息并使用最新版本。Django1.7充满了令人兴奋的新功能,例如内置模式迁移和应用程序重新加载。Python3.4是该语言的最前沿,具有几个新模块,例如asyncio。这两者都在这里使用了。
超级英雄是本书中的一个不断出现的主题。大多数代码示例都是关于构建SuperBook——一个超级英雄的社交网络。作为呈现Web开发项目挑战的一种新颖方式,每章都以故事框的形式编织了一个令人兴奋的虚构叙述。
第二章,“应用程序设计”,指导我们通过应用程序生命周期的早期阶段,例如收集要求和创建模型。我们还将看到如何通过我们的运行项目SuperBook将项目分解为模块化应用程序。
第三章,“模型”,让我们了解模型如何以图形方式表示,使用几种模式进行结构化,并使用迁移(内置于Django1.7)进行后续更改。
第四章,“视图和URL”,向我们展示了如何将基于函数的视图演变为具有强大混合概念的基于类的视图,使我们熟悉有用的视图模式,并教会我们如何设计简短而有意义的URL。
第五章,“模板”,通过Django模板语言构造,解释其设计选择,建议如何组织模板文件,介绍方便的模板模式,并指出几种集成和自定义Bootstrap的方法。
第六章,“管理界面”,向我们展示了如何更有效地使用Django出色的开箱即用的管理界面,以及多种自定义方式,从增强模型到改进其默认外观和感觉。
第七章,“表单”,说明了常常令人困惑的表单工作流程,以及渲染表单的不同方式,如何使用crispyforms改善表单的外观以及各种应用表单模式。
第八章,“处理遗留代码”,解决了遗留Django项目的常见问题,例如确定正确的版本、定位文件、从何处开始阅读大型代码库,以及如何通过添加新功能来增强遗留代码。
第九章,“测试和调试”,概述了各种测试和调试工具和技术,介绍了测试驱动开发、模拟、日志记录和调试器。
第十章,安全性,使您熟悉各种Web安全威胁及其对策,特别是Django如何保护您。最后,一个方便的安全性检查表提醒您常常被忽视的领域。
第十一章,准备投产,介绍了部署面向公众的应用程序的速成课程,从选择Web堆栈开始,了解托管选项,并走过典型的部署过程。我们在这个阶段深入了解监控和性能的细节。
您只需要一台计算机(PC或Mac)和互联网连接即可开始。然后,请确保已安装以下内容:
我建议使用基于Linux的系统,如Ubuntu或ArchLinux。如果您使用Windows,可以使用Vagrant或VirtualBox在Linux虚拟机上工作。这里有一个充分的披露:我更喜欢命令行界面、Emacs和荷包蛋。
某些章节可能需要安装特定的Python库或Django包。它们将被提及,比如说factory_boy包。在大多数情况下,它们可以使用pip进行安装,如下所示:
$pipinstallfactory_boy因此,强烈建议您首先创建一个单独的虚拟环境,如第二章中所述,应用程序设计。
本书旨在帮助开发人员洞察使用Django构建高度可维护的网站。它将帮助您更深入地了解框架,但也会使您熟悉几个Web开发概念。
您不必是Django或Python的专家。阅读本书不需要对模式有先验知识。更具体地说,本书不是关于经典的四人帮模式,尽管它们可能会被提及。
这里的许多实用信息可能不仅仅适用于Django,而是适用于Web开发。在本书结束时,您应该是一个更高效和务实的Web开发人员。
在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。
文本中的代码词、文件夹名称、文件名、包名称和用户输入显示如下:“HttpResponse对象被呈现为字符串。”
代码块设置如下:
fromdjango.dbimportmodelsclassSuperHero(models.Model):name=models.CharField(max_length=100)任何命令行(通常是Unix)的输入或输出都写成如下形式:
$django-admin.py--version1.6.1以美元提示符($符号)开头的行是要在shell中输入的(但跳过提示本身)。其余行是系统输出,如果输出非常长,可能会使用省略号(…)进行修剪。
每个章节(除了第一章)都将有一个故事框,样式如下:
超级书籍章节标题
那是一个漆黑而风雨交加的夜晚;披着斗篷的超级英雄的剪影在烧焦的里克森数字图书馆的废墟中移动。捡起一块看起来像是半融化的硬盘盒;显而易见队长咬紧牙关喊道:“我们需要备份!”
故事框最好按顺序阅读,以遵循线性叙事。
本书中描述的模式以《本书中的模式》一节中提到的格式编写,位于第一章Django和模式中。
提示和最佳实践的风格如下:
最佳实践
每5年更换你的超级服装。
新术语和重要单词以粗体显示。
在本章中,我们将讨论以下主题:
根据盖博伟的“世界初创企业报告”,2013年全球有超过136,000家互联网公司,仅美国就有超过60,000家。其中,87家美国公司的估值超过10亿美元。另一项研究表明,在27个国家的12,000名18至30岁的人中,超过三分之二看到了成为企业家的机会。
每个网络应用都是不同的,就像手工制作的家具一样。你很少会找到一个完全符合你需求的大规模生产的产品。即使你从一个基本需求开始,比如一个博客或一个社交网络,你的需求会慢慢增长,你很容易最终得到很多临时解决方案粗制滥造地贴在一个曾经简单的模板解决方案上。
这就是为什么像Django或Rails这样的网络框架变得极其受欢迎。框架可以加快开发速度,并且内置了所有最佳实践。然而,它们也足够灵活,可以让你获得足够的工具来完成工作。如今,网络框架是无处不在的,大多数编程语言都至少有一个类似Django的端到端框架。
Python维基列出了超过54个活跃的网络框架,其中最受欢迎的是Django、Flask、Pyramid和Zope。Python的框架也具有广泛的多样性。紧凑的Bottle微型网络框架只有一个Python文件,没有依赖性,但却能够出人意料地创建一个简单的网络应用。
开箱即用的管理界面是Django的独特功能之一,对于早期数据输入和测试非常有帮助。Django的文档因为非常适合开源项目而受到赞扬。
尽管理论上你可以使用Django构建任何类型的网络应用,但它可能并不适合每种情况。例如,要构建基于实时聊天的网络界面,你可能会想使用Tornado,而你的网络应用的其余部分仍然可以使用Django完成。选择合适的工具来完成工作。
一些内置功能,比如管理界面,如果你习惯于其他网络框架,可能会听起来有些奇怪。为了理解Django的设计,让我们找出它是如何诞生的。
当你看着埃及金字塔时,你可能会认为这样简单而简约的设计一定是相当明显的。事实上,它们是4000年建筑演变的产物。阶梯金字塔,最初(而且笨重)的设计,有六个尺寸递减的矩形块。经过几次建筑和工程改进,直到现代、玻璃化和持久的石灰石结构被发明出来。
看着Django,你可能会有类似的感觉。如此优雅地构建,一定是毫无瑕疵地构想出来的。相反,它是在一个想象得到的最高压力环境中的重写和快速迭代的结果-一个新闻编辑室!
最终,内容管理部分被分拆成一个名为EllingtonCMS的独立项目,后来成为一个成功的商业CMS产品。剩下的“CMS”是一个干净的基础框架,通用到足以用来构建任何类型的网络应用程序。
2005年7月,这个网页开发框架以Django(发音为Jang-Oh)的形式发布,采用了开源的伯克利软件分发(BSD)许可证。它以传奇爵士吉他手DjangoReinhardt的名字命名。剩下的,就像他们说的那样,就成了历史。
由于它作为内部工具的起源谦逊,Django有很多劳伦斯Journal-World特有的怪癖。为了使Django真正通用,一个名为“去除劳伦斯”的努力已经在进行中。
然而,Django开发人员必须进行的最重要的重构工作被称为“去除魔法”。这个雄心勃勃的项目涉及清理Django多年来积累的所有瑕疵,包括很多魔法(隐含功能的非正式术语),并用更自然和明确的Python代码替换它们。例如,模型类曾经是从一个名为django.models.*的魔法模块导入的,而不是直接从它们定义的models.py模块导入。
当时,Django有大约十万行代码,这是API的重大重写。2006年5月1日,这些变化,几乎相当于一本小书的大小,被整合到Django的开发版本主干中,并作为Django0.95版本发布。这是迈向Django1.0里程碑的重要一步。
每年,全球各地都会举行名为DjangoCons的会议,供Django开发人员相互交流。他们有一个可爱的传统,即在“为什么Django糟糕”上发表半幽默的主题演讲。这可能是Django社区的成员,或者是在竞争的网络框架上工作的人,或者只是任何知名人士。
多年来,令人惊讶的是Django开发人员如何积极地接受这些批评,并在随后的版本中加以缓解。以下是对应于Django曾经的缺点的改进的简要总结以及它们所解决的版本:
要真正欣赏Django,您需要窥探一下内部,看看其中的各种组成部分。这既可以启发,也可能令人不知所措。如果您已经熟悉这一点,您可能想跳过本节。
典型Django应用程序中的Web请求是如何处理的
上述图显示了来自访问者浏览器的Web请求到达您的Django应用程序并返回的简化旅程。编号路径如下:
5a.通过模型与数据库进行交谈
5b.使用模板呈现HTML或任何其他格式化响应
5c.返回纯文本响应(未显示)
5d.引发异常
尽管省略了某些细节,但这种表示应该有助于您欣赏Django的高级架构。它还展示了关键组件(如模型、视图和模板)所扮演的角色。Django的许多组件都基于几种众所周知的设计模式。
“蓝图”、“脚手架”和“维护”之间有什么共同之处?这些软件开发术语都是从建筑施工和建筑领域借来的。然而,最有影响力的术语之一来自于1977年奥地利著名建筑师克里斯托弗·亚历山大及其团队(包括MurraySilverstein、SaraIshikawa等人)撰写的一部关于建筑和城市规划的专著。
在书中,克里斯托弗·亚历山大(ChristopherAlexander)指出:“每个模式描述了一个在我们的环境中反复出现的问题,然后以这样一种方式描述了这个问题的核心解决方案,以至于您可以一百万次使用这个解决方案,而不必重复两次。”
例如,光之翼模式描述了人们更喜欢有更多自然光线的建筑,并建议安排建筑物以由翼组成。这些翼应该是长而窄的,绝不超过25英尺宽。下次你在一所古老大学的长长明亮的走廊上散步时,要感谢这种模式。
他们的书包含了253种这样的实用模式,从房间设计到整个城市的设计。最重要的是,这些模式中的每一个都给了一个抽象问题一个名称,并共同形成了一个“模式语言”。
还记得当你第一次遇到“déjàvu”这个词吗?你可能会想“哇,我从来不知道有一个词来描述那种经历。”同样,建筑师不仅能够在他们的环境中识别模式,而且最终还能以一种同行能够理解的方式来命名它们。
在软件世界中,术语“设计模式”指的是软件设计中常见问题的一般可重复解决方案。它是开发人员可以使用的最佳实践的正式化。就像在建筑世界一样,模式语言已被证明对于向其他程序员传达解决设计问题的某种方式非常有帮助。
有几种设计模式的集合,但有些比其他的影响力更大。
早期研究和记录设计模式的努力之一是一本名为《设计模式:可复用面向对象软件的元素》的书,作者是ErichGamma、RichardHelm、RalphJohnson和JohnVlissides,后来被称为四人帮(GoF)。这本书影响深远,以至于许多人认为书中的23种设计模式对软件工程本身是基本的。
实际上,这些模式主要是针对面向对象编程语言编写的,并且它在C++和Smalltalk中有代码示例。正如我们将很快看到的,许多这些模式在其他具有更好高阶抽象的编程语言中甚至可能不需要。
这23种模式已经被广泛分类为以下类型:
虽然详细解释每种模式超出了本书的范围,但在Django本身中识别一些这些模式是很有趣的:
虽然这些模式大多是对研究Django内部感兴趣的人来说,Django本身可以归类的模式是一个常见的问题。
模型-视图-控制器(MVC)是70年代由施乐PARC发明的一种架构模式。作为构建Smalltalk用户界面的框架,它在《GangofFour》一书中早早地被提及。
今天,MVC是Web应用程序框架中非常流行的模式。初学者经常问这样的问题:Django是一个MVC框架吗?
答案既是肯定的,也是否定的。MVC模式倡导将表示层与应用程序逻辑解耦。例如,在设计在线游戏网站API时,您可能会将游戏的高分榜呈现为HTML、XML或逗号分隔(CSV)文件。但是,其底层模型类将独立设计,不受数据最终呈现方式的影响。
MVC对模型、视图和控制器的功能非常严格。然而,Django对Web应用程序采取了更加实用的观点。由于HTTP协议的性质,对Web页面的每个请求都是独立的。Django的框架被设计成一个处理每个请求并准备响应的管道。
如果将此与经典的MVC进行比较——“模型”可与Django的模型相媲美,“视图”通常是Django的模板,“控制器”是处理传入的HTTP请求并将其路由到正确视图函数的框架本身。
如果这还没有让您困惑,Django更倾向于将处理每个URL的回调函数命名为“视图”函数。不幸的是,这与MVC模式中“视图”的概念无关。
2002年,MartinFowler写了《企业应用架构模式》,描述了他在构建企业应用程序时经常遇到的40多种模式。
与GoF书不同,Fowler的书是关于架构模式的。因此,它们以更高的抽象级别描述模式,并且在很大程度上与编程语言无关。
Fowler的模式组织如下:
Django还实现了许多这些模式。以下表格列出了其中的一些:
是的,当然。模式一直在不断被发现。就像生物一样,有些会变异并形成新的模式:例如,MVC的变体,如模型-视图-呈现者(MVP)、分层模型-视图-控制器(HMVC)或模型视图视图模型(MVVM)。
一些其他众所周知的模式目录书籍包括Buschmann、Meunier、Rohnert、Sommerlad和Sta的面向模式的软件架构(称为POSA);Hohpe和Woolf的企业集成模式;以及Duyne、Landay和Hong的网站设计:为打造以客户为中心的网页体验而编织的模式、原则和流程。
本书将涵盖Django特定的设计和架构模式,这对Django开发人员很有用。接下来的章节将描述每个模式将如何呈现。
模式名称
标题是模式名称。如果是一个众所周知的模式,就使用常用的名称;否则,选择一个简洁的、自我描述的名称。名称很重要,因为它有助于建立模式词汇。所有模式都将包括以下部分:
问题:这简要提到了问题。
解决方案:这总结了提出的解决方案。
问题详情:这详细阐述了问题的背景,并可能给出一个例子。
解决方案详情:这以一般术语解释了解决方案,并提供了一个Django实现的示例。
尽管它们几乎被普遍使用,但模式也有它们的批评。最常见的反对意见如下:
虽然之前的一些批评是相当合理的,但它们是基于模式被误用的情况。以下是一些建议,可以帮助你了解如何最好地使用设计模式:
除了设计模式,可能还有一种推荐的解决问题的方法。在Django中,与Python一样,可能有几种解决问题的方法,但其中一种是惯用的方法。
一般来说,Python社区使用术语“Pythonic”来描述一段惯用的代码。它通常指的是《Python之禅》中阐述的原则。这本书像一首诗一样写成,对于描述这样一个模糊的概念非常有用。
尝试在Python提示符中输入importthis来查看《Python之禅》。
虽然该文档描述了Django设计背后的思维过程,但对于使用Django构建应用程序的开发人员也是有用的。某些原则,如“不要重复自己”(DRY)、“松耦合”和“紧凑性”可以帮助您编写更易维护和成熟的Django应用程序。
本书建议的Django或Python最佳实践将以以下方式格式化:
最佳实践:
在settings.py中使用BASE_DIR,并避免硬编码目录名称。
在本章中,我们探讨了人们为什么选择Django而不是其他Web框架,它有趣的历史以及它的工作原理。我们还研究了设计模式、流行的模式集合和最佳实践。
在下一章中,我们将看一下Django项目开始阶段的前几个步骤,比如收集需求、创建模型和设置项目。
在本章中,我们将涵盖以下主题:
第一次会议会很长(可能是一整天的研讨会或几个小时的会议)。后来,当这些会议变得频繁时,你可以将它们缩短到30分钟或一个小时。
所有这一切的产出将是一个一页的写作和几张粗糙的素描。
在这本书中,我们自愿承担了一个崇高的项目,为超级英雄建立一个名为SuperBook的社交网络。根据我们与一群随机选择的超级英雄讨论的简单草图如下所示:
SuperBook网站的响应式设计草图。显示了桌面(左)和智能手机(右)布局。
那么这个一页的写作是什么?这是一个简单的文件,解释了使用该网站的感受。在我参与的几乎所有项目中,当有新成员加入团队时,他们通常不会浏览每一份文件。如果他们找到一个简短的单页文件,快速告诉他们网站的意图,他们会感到高兴。
你可以随意称呼这个文件——概念文件、市场需求文件、客户体验文档,甚至是史诗脆弱故事日志(专利申请中)。这真的无关紧要。
文件应该侧重于用户体验,而不是技术或实施细节。要简短有趣。事实上,JoelSpolsky在记录需求方面的第一条规则是“要幽默”。
这是SuperBook项目的概念文件:
SuperBook概念
以下采访是在我们的网站SuperBook在未来推出后进行的。在采访之前进行了30分钟的用户测试。
请介绍一下自己。
我的名字是阿克塞尔。我是一只灰松鼠,住在纽约市中心。不过,每个人都叫我橡子。我爸爸,著名的嘻哈明星T.贝瑞,过去常常叫我那个。我想我从来没有唱歌好到可以接手家族生意。
事实上,在我早期,我有点偷窃癖。你知道,我对坚果过敏。其他兄弟们很容易就能在公园里生活。我不得不improvisation——咖啡馆、电影院、游乐园等等。我也非常仔细地阅读标签。
好的,橡子。你为什么认为你被选中进行用户测试?
可能是因为我曾在纽约星报上被介绍为一个不太知名的超级英雄。我猜人们觉得一个松鼠能用MacBook很有趣(采访者:这次采访是通过聊天进行的)。另外,我有一个松鼠一样的注意力。
根据你看到的,你对SuperBook有什么看法?
我认为这是一个很棒的主意。我的意思是,人们经常看到超级英雄。然而,没有人关心他们。大多数都是孤独和反社会的。SuperBook可以改变这一点。
你认为Superbook有什么不同?
它是为像我们这样的人从零开始构建的。我的意思是,当你想要使用你的秘密身份时,没有“工作和教育”的废话。虽然我没有,但我能理解为什么有人会有一个。
你能简要告诉我们你注意到的一些特点吗?
当然,我认为这是一个相当不错的社交网络,你可以:
一切都很容易。弄清楚它并不需要超人的努力。
在构建Web应用程序的早期,工具如Photoshop和Flash被广泛使用来获得像素完美的模型。它们几乎不再被推荐或使用。
在智能手机、平板电脑、笔记本电脑和其他平台上提供本地和一致的体验现在被认为比获得像素完美的外观更重要。事实上,大多数网页设计师直接在HTML上创建布局。
创建HTML模型比以前快得多,也更容易。如果你的网页设计师不可用,开发人员可以使用CSS框架,如Bootstrap或ZURBFoundation框架来创建相当不错的模型。
一个好的模型可以用不到总体开发工作量的20%来提供80%的客户体验。
当你对需要构建的东西有一个相当好的想法时,你可以开始考虑在Django中的实现。再一次,开始编码是很诱人的。然而,当你花几分钟思考设计时,你会发现解决设计问题的许多不同方法。
你也可以首先开始设计测试,这是测试驱动设计(TDD)方法论所倡导的。我们将在测试章节中看到更多TDD方法的应用。
无论你采取哪种方法,最好停下来思考一下——“我可以用哪些不同的方式来实现这个?有什么权衡?在我们的情境中哪些因素更重要?最后,哪种方法是最好的?”
如果他们必须自己编写应用程序,他们会开始考虑各种设计模式,希望能够设计出优雅的设计。然而,他们首先需要将项目在顶层分解为应用程序。
Django应用程序称为项目。一个项目由多个应用程序或应用组成。应用是提供一组功能的Python包。
理想情况下,每个应用都必须是可重用的。您可以创建尽可能多的应用。永远不要害怕添加更多的应用或将现有的应用重构为多个应用。一个典型的Django项目包含15-20个应用。
在这个阶段做出的一个重要决定是是否使用第三方Django应用程序还是从头开始构建一个。第三方应用程序是现成的应用程序,不是由您构建的。大多数包都可以快速安装和设置。您可以在几分钟内开始使用它们。
另一方面,编写自己的应用通常意味着自己设计和实现模型、视图、测试用例等。Django不会区分这两种类型的应用。
安装和使用现成的应用可能听起来更容易。然而,事实并不像听起来那么简单。让我们来看看我们项目中一些第三方身份验证应用,并列出我们在撰写本文时为SuperBook未使用它们的原因:
这些包都不是坏的。它们只是暂时不符合我们的需求。它们可能对不同的项目有用。在我们的情况下,内置的Djangoauth应用程序已经足够好了。
另一方面,您可能会因以下一些原因而更喜欢使用第三方应用程序:
即使您已经为开发创建了Python虚拟环境,尝试所有这些软件包然后将它们丢弃可能会污染您的环境。因此,我通常会创建一个名为“sandbox”的单独虚拟环境,纯粹用于尝试这些应用程序。然后,我构建一个小项目来了解使用起来有多容易。
稍后,如果我对应用程序的试用感到满意,我会使用Git等版本控制工具在我的项目中创建一个分支来集成该应用程序。然后,我会在分支中继续编码和运行测试,直到必要的功能被添加。最后,这个分支将被审查并合并回主线(有时称为master)分支。
为了说明这个过程,我们的SuperBook项目可以大致分为以下应用程序(不是完整的列表):
在这里,一个应用程序被标记为从头开始构建(标记为“自定义”)或我们将使用的第三方Django应用程序。随着项目的进展,这些选择可能会改变。但是,这已经足够好了。
在准备开发环境时,请确保以下内容已经就位:
本书认为通过示例演示Django设计模式和最佳实践的实际和务实的方法。为了保持一致,我们所有的例子都将围绕构建一个名为SuperBook的社交网络项目。
SuperBook专注于被忽视的超能力人群的利基市场。您是一个开发团队中的开发人员,团队中还有其他开发人员、网页设计师、市场经理和项目经理。
该项目将在撰写时的最新版本的Python(版本3.4)和Django(版本1.7)中构建。由于选择Python3可能是一个有争议的话题,它值得更详细的解释。
尽管Python3的开发始于2006年,但它的第一个版本Python3.0是在2008年12月3日发布的。不向后兼容版本的主要原因是——将所有字符串切换为Unicode,增加迭代器的使用,清理弃用的特性,如旧式类,以及一些新的语法添加,如nonlocal语句。
2月13日,Django1.5成为第一个支持Python3的版本。开发人员已经明确表示,未来Django将使用Python3编写,并旨在向后兼容Python2。
对于本书来说,Python3是理想的,原因如下:
最后一点很重要。即使你使用Python2,这本书也会对你有所帮助。阅读附录A以了解这些变化。你只需要做最小的调整来回溯示例代码。
本节包含了SuperBook项目的安装说明,其中包含了本书中使用的所有示例代码。请查看项目的README文件以获取最新的安装说明。建议您首先创建一个名为superbook的新目录,其中包含虚拟环境和项目源代码。
理想情况下,每个Django项目都应该在自己单独的虚拟环境中。这样可以轻松安装、更新和删除软件包,而不会影响其他应用程序。在Python3.4中,建议使用内置的venv模块,因为它默认还会安装pip:
在某些情况下,最后一个导出命令可能不是必需的。如果运行pipfreeze列出的是系统包而不是空的,那么你需要输入这行。
在开始Django项目之前,请创建一个新的虚拟环境。
接下来,从GitHub克隆示例项目并安装依赖项:
$cdfinal$pythonmanage.pymigrate$pythonmanage.pycreatesuperuser$pythonmanage.pyrunserver在Django1.7中,migrate命令已经取代了syncdb命令。我们还需要显式调用createsuperuser命令来创建一个超级用户,以便我们可以访问管理员。
我们讨论了设计应用程序的许多方面,比如创建交互式模型或将其分成可重用的组件,称为应用程序。我们还讨论了设置我们的示例项目SuperBook的步骤。
在下一章中,我们将详细了解Django的每个组件,并学习围绕它们的设计模式和最佳实践。
在Django中,模型是提供与数据库打交道的面向对象方式的类。通常,每个类都指的是一个数据库表,每个属性都指的是一个数据库列。你可以使用自动生成的API对这些表进行查询。
模型可以成为许多其他组件的基础。一旦你有了一个模型,你就可以快速地派生模型管理员、模型表单和各种通用视图。在每种情况下,你都需要写一两行代码,这样它就不会显得太神奇。
此外,模型的使用范围比你想象的要广。这是因为Django可以以多种方式运行。Django的一些入口点如下:
在几乎所有这些情况下,模型模块都会被导入(作为django.setup()的一部分)。因此,最好让你的模型摆脱任何不必要的依赖,或者导入其他Django组件,如视图。
简而言之,正确设计你的模型非常重要。现在让我们开始SuperBook模型设计。
午餐袋
*作者注:SuperBook项目的进展将出现在这样的一个框中。你可以跳过这个框,但你会错过在Web应用项目中工作的见解、经验和戏剧。
史蒂夫与他的客户超级英雄情报和监控或S.H.I.M。简称,度过了一个波澜起伏的第一周。办公室非常futurist,但要做任何事情都需要一百个批准和签字。
作为首席Django开发人员,史蒂夫在两天内完成了设置一个中型开发服务器,托管了四台虚拟机。第二天早上,机器本身已经消失了。附近一个洗衣机大小的机器人说,它被带到了法证部门,因为未经批准的软件安装。
然而,CTO哈特非常乐意帮忙。他要求机器在一个小时内归还,并保持所有安装完好。他还发送了对SuperBook项目的预批准,以避免将来出现任何类似的障碍。
那天下午,史蒂夫和他一起吃了午餐。哈特穿着米色西装外套和浅蓝色牛仔裤,准时到达。尽管比大多数人高,头发光秃秃的,他看起来很酷,很平易近人。他问史蒂夫是否看过上世纪六十年代尝试建立超级英雄数据库的尝试。
史蒂夫微弱地点了点头。他从未想象过第一次会有大约十亿个超级英雄。
这是对SuperBook中模型的第一次识别。典型的早期尝试,我们只表示了基本模型及其关系,以类图的形式:
建议绘制类图来描述您的模型。在这个阶段可能会缺少一些属性,但您可以稍后详细说明。一旦整个项目在图表中表示出来,就会更容易分离应用程序。
以下是创建此表示形式的一些提示:
类图可以映射到以下Django代码(将分布在几个应用程序中):
classProfile(models.Model):user=models.OneToOneField(User)classPost(models.Model):posted_by=models.ForeignKey(User)classComment(models.Model):commented_by=models.ForeignKey(User)for_post=models.ForeignKey(Post)classLike(models.Model):liked_by=models.ForeignKey(User)post=models.ForeignKey(Post)稍后,我们将不直接引用User,而是使用更一般的settings.AUTH_USER_MODEL。
与Django的大多数组件一样,可以将大型的models.py文件拆分为包内的多个文件。包被实现为一个目录,其中可以包含多个文件,其中一个必须是一个名为__init__.py的特殊命名文件。
可以在包级别公开的所有定义都必须在__init__.py中以全局范围定义。例如,如果我们将models.py拆分为单独的类,放在models子目录内的相应文件中,如postable.py,post.py和comment.py,那么__init__.py包将如下所示:
frompostableimportPostablefrompostimportPostfromcommentimportComment现在您可以像以前一样导入models.Post。
__init__.py包中的任何其他代码在导入包时都会运行。因此,这是任何包级别初始化代码的理想位置。
本节包含几种设计模式,可以帮助您设计和构造模型。
问题:按设计,模型实例具有导致数据不一致的重复数据。
解决方案:通过规范化将模型分解为较小的模型。使用这些模型之间的逻辑关系连接这些模型。
想象一下,如果有人以以下方式设计我们的Post表(省略某些列):
我希望您注意到了最后一行中不一致的超级英雄命名(以及队长一贯的缺乏耐心)。
如果我们看第一列,我们不确定哪种拼写是正确的—CaptainTemper还是Capt.Temper。这是我们希望通过规范化消除的数据冗余。
在我们查看完全规范化的解决方案之前,让我们简要介绍一下Django模型的数据库规范化的概念。
举个快速的例子,如果我们要规范化Post表,以便我们可以明确地引用发布该消息的超级英雄,那么我们需要将用户详细信息隔离在一个单独的表中。Django已经默认创建了用户表。因此,您只需要在第一列中引用发布消息的用户的ID,如下表所示:
现在,不仅清楚地知道有三条消息是由同一用户(具有任意用户ID)发布的,而且我们还可以通过查找用户表找到该用户的正确姓名。
通常,您将设计您的模型以达到其完全规范化的形式,然后出于性能原因选择性地对其进行去规范化。在数据库中,正常形式是一组可应用于表以确保其规范化的准则。通常的正常形式有第一、第二和第三正常形式,尽管它们可以达到第五正常形式。
要符合第一正常形式,表必须具有:
让我们尝试将我们的电子表格转换为数据库表。显然,我们的'Power'列违反了第一条规则。
这里更新的表满足了第一正常形式。主键(用标记)是'Name'和'Power'*的组合,对于每一行来说应该是唯一的。
第二正常形式必须满足第一正常形式的所有条件。此外,它必须满足所有非主键列都必须依赖于整个主键的条件。
在前面的表中,注意'Origin'只取决于超级英雄,即'Name'。我们谈论的Power无关紧要。因此,Origin并不完全依赖于复合主键—Name和Power。
让我们将起源信息提取到一个名为'Origins'的单独表中,如下所示:
现在,我们更新为符合第二正常形式的Sightings表如下:
在第三范式中,表必须满足第二范式,并且还必须满足所有非主键列必须直接依赖于整个主键并且彼此独立的条件。
想一下国家列。根据纬度和经度,您可以很容易地推导出国家列。尽管超级能力出现的国家取决于名称-能力复合主键,但它只间接依赖于它们。
因此,让我们将位置细节分离到一个单独的国家表中,如下所示:
现在我们的Sightings表在第三范式中看起来像这样:
与以前一样,我们已经用对应的用户ID替换了超级英雄的名字,这可以用来引用用户表。
现在我们可以看一下这些规范化表如何表示为Django模型。Django不直接支持复合键。这里使用的解决方案是应用代理键,并在Meta类中指定unique_together属性:
classOrigin(models.Model):superhero=models.ForeignKey(settings.AUTH_USER_MODEL)origin=models.CharField(max_length=100)classLocation(models.Model):latitude=models.FloatField()longitude=models.FloatField()country=models.CharField(max_length=100)classMeta:unique_together=("latitude","longitude")classSighting(models.Model):superhero=models.ForeignKey(settings.AUTH_USER_MODEL)power=models.CharField(max_length=100)location=models.ForeignKey(Location)sighted_on=models.DateTimeField()classMeta:unique_together=("superhero","power")性能和去规范化规范化可能会对性能产生不利影响。随着模型数量的增加,回答查询所需的连接数量也会增加。例如,要找到在美国具有冰冻能力的超级英雄数量,您将需要连接四个表。在规范化之前,可以通过查询单个表找到任何信息。
您应该设计您的模型以保持数据规范化。这将保持数据完整性。但是,如果您的网站面临可扩展性问题,那么您可以有选择地从这些模型中派生数据,以创建去规范化的数据。
设计时规范化,优化时去规范化。
例如,如果在某个特定国家中计算目击事件是非常常见的,那么将其作为Location模型的一个附加字段。现在,您可以使用Django的ORM包括其他查询,而不是使用缓存值。
但是,您需要在每次添加或删除一个目击事件时更新这个计数。您需要将这个计算添加到Sighting的save方法中,添加一个信号处理程序,或者甚至使用异步作业进行计算。
如果您有一个跨多个表的复杂查询,比如按国家统计超能力的数量,那么您需要创建一个单独的去规范化表。与以前一样,每当规范化模型中的数据发生更改时,我们都需要更新这个去规范化表。
过度规范化并不一定是件好事。有时,它可能会引入一个不必要的表,从而使更新和查找变得复杂。
例如,您的用户模型可能有几个字段用于他们的家庭地址。严格来说,您可以将这些字段规范化为一个地址模型。但是,在许多情况下,引入一个额外的表到数据库中可能是不必要的。
与其追求最规范化的设计,不如在重构之前仔细权衡每个规范化的机会并考虑权衡。
问题:不同的模型具有相同的字段和/或重复的方法,违反了DRY原则。
解决方案:将常见字段和方法提取到各种可重用的模型混合中。
由于Django模型是类,因此可以使用面向对象的方法,如组合和继承。但是,组合(通过具有包含共享类实例的属性)将需要额外的间接级别来访问字段。
具体继承通过从基类派生,就像在Python类中通常做的那样。但是,在Django中,这个基类将被映射到一个单独的表中。每次访问基本字段时,都需要隐式连接。这会导致性能恶化。
代理继承只能向父类添加新行为。您不能添加新字段。因此,对于这种情况,它并不是非常有用。
最后,我们剩下了抽象继承。
抽象基类是用于在模型之间共享数据和行为的优雅解决方案。当您定义一个抽象类时,它不会在数据库中创建任何相应的表。相反,这些字段将在派生的非抽象类中创建。
访问抽象基类字段不需要JOIN语句。由于这些优势,大多数Django项目使用抽象基类来实现常见字段或方法。
抽象模型的局限性如下:
classPostable(models.Model):created=models.DateTimeField(auto_now_add=True)modified=models.DateTimeField(auto_now=True)message=models.TextField(max_length=500)classMeta:abstract=TrueclassPost(Postable):...classComment(Postable):...要将模型转换为抽象基类,您需要在其内部Meta类中提到abstract=True。在这里,Postable是一个抽象基类。但是,它并不是非常可重用的。
模型混合是可以添加为模型的父类的抽象类。Python支持多重继承,不像其他语言如Java。因此,您可以为模型列出任意数量的父类。
混合类应该是正交的并且易于组合。将混合类放入基类列表中,它们应该可以工作。在这方面,它们更类似于组合而不是继承的行为。
较小的混合类更好。每当混合类变得庞大并违反单一责任原则时,考虑将其重构为较小的类。让混合类只做一件事,并且做得很好。
classTimeStampedModel(models.Model):created=models.DateTimeField(auto_now_add=True)modified=models.DateTimeField(auto_now=True)classMeta:abstract=TrueclassPostable(TimeStampedModel):message=models.TextField(max_length=500)...classMeta:abstract=TrueclassPost(Postable):...classComment(Postable):...现在我们有两个基类。但是,功能明显分开。混合类可以分离到自己的模块中,并在其他上下文中重用。
问题:每个网站存储不同的用户配置文件详细信息。但是,Django内置的User模型是用于身份验证详细信息的。
解决方案:创建一个用户配置文件类,与用户模型有一对一的关系。
然而,大多数现实世界的项目都会保存更多关于用户的信息,比如他们的地址、喜欢的电影,或者他们的超能力。从Django1.5开始,默认的User模型可以被扩展或替换。然而,官方文档强烈建议即使在自定义用户模型中也只存储认证数据(毕竟它属于auth应用)。
某些项目需要多种类型的用户。例如,SuperBook可以被超级英雄和非超级英雄使用。根据用户类型,可能会有共同的字段和一些特殊的字段。
官方推荐的解决方案是创建一个用户配置模型。它应该与用户模型有一对一的关系。所有额外的用户信息都存储在这个模型中:
classProfile(models.Model):user=models.OneToOneField(settings.AUTH_USER_MODEL,primary_key=True)建议您将primary_key显式设置为True,以防止一些数据库后端(如PostgreSQL)中的并发问题。模型的其余部分可以包含任何其他用户详细信息,例如出生日期、喜欢的颜色等。
在设计配置模型时,建议所有配置详细字段都必须是可空的或包含默认值。直观地,我们可以理解用户在注册时无法填写所有配置详细信息。此外,我们还将确保信号处理程序在创建配置实例时也不传递任何初始参数。
理想情况下,每次创建用户模型实例时,都必须创建一个相应的用户配置实例。这通常是使用信号来完成的。
例如,我们可以监听用户模型的post_save信号,使用以下信号处理程序:
#signals.pyfromdjango.db.models.signalsimportpost_savefromdjango.dispatchimportreceiverfromdjango.confimportsettingsfrom.importmodels@receiver(post_save,sender=settings.AUTH_USER_MODEL)defcreate_profile_handler(sender,instance,created,**kwargs):ifnotcreated:return#Createtheprofileobject,onlyifitisnewlycreatedprofile=models.Profile(user=instance)profile.save()请注意,配置模型除了用户实例之外,没有传递任何额外的初始参数。
以前,没有特定的位置来初始化信号代码。通常它们被导入或实现在models.py中(这是不可靠的)。然而,随着Django1.7中的应用加载重构,应用初始化代码的位置得到了明确定义。
首先,为您的应用创建一个__init__.py包,以提及您的应用的ProfileConfig:
default_app_config="profiles.apps.ProfileConfig"接下来,在app.py中对ProfileConfig方法进行子类化,并在ready方法中设置信号:
#app.pyfromdjango.appsimportAppConfigclassProfileConfig(AppConfig):name="profiles"verbose_name='UserProfiles'defready(self):from.importsignals设置好信号后,访问user.profile应该会返回一个Profile对象给所有用户,甚至是新创建的用户。
现在,用户的详细信息将在管理员中的两个不同位置:通常用户管理员页面中的认证详细信息和同一用户的额外配置详细信息在单独的配置管理员页面中。这变得非常繁琐。
为了方便起见,可以通过定义自定义的UserAdmin将配置管理员内联到默认的用户管理员中:
#admin.pyfromdjango.contribimportadminfrom.modelsimportProfilefromdjango.contrib.auth.modelsimportUserclassUserProfileInline(admin.StackedInline):model=ProfileclassUserAdmin(admin.UserAdmin):inlines=[UserProfileInline]admin.site.unregister(User)admin.site.register(User,UserAdmin)多种配置类型假设您的应用程序需要几种不同类型的用户配置。需要有一个字段来跟踪用户拥有的配置类型。配置数据本身需要存储在单独的模型或统一的模型中。
建议使用聚合配置方法,因为它可以灵活地更改配置类型而不会丢失配置详细信息,并且可以最小化复杂性。在这种方法中,配置模型包含来自所有配置类型的所有配置字段的超集。
例如,SuperBook将需要一个SuperHero类型的配置和一个Ordinary(非超级英雄)配置。可以使用单一统一的配置模型来实现如下:
SuperHeroProfile类和OrdinaryProfile类包含特定于超级英雄和非英雄用户的配置详细信息。最后,profile类从所有这些基类派生,以创建配置详细信息的超集。
在使用这种方法时需要注意的一些细节如下:
问题:模型可能会变得庞大且难以管理。随着模型的功能变得多样化,测试和维护变得更加困难。
对于Django初学者来说,经常听到的一句话是“模型臃肿,视图薄”。理想情况下,您的视图除了呈现逻辑之外不应包含任何其他内容。
一些表明您的模型可以使用Service对象的迹象如下:
Django中的模型遵循ActiveRecord模式。理想情况下,它们封装了应用程序逻辑和数据库访问。但是,要保持应用程序逻辑最小化。
在测试过程中,如果我们发现自己在不使用数据库的情况下不必要地模拟数据库,那么我们需要考虑拆分模型类。在这种情况下,建议使用Service对象。
服务对象是封装“服务”或与系统交互的普通Python对象(POPOs)。它们通常保存在名为services.py或utils.py的单独文件中。
例如,有时将检查Web服务转储到模型方法中,如下所示:
from.servicesimportSuperHeroWebAPIdefis_superhero(self):returnSuperHeroWebAPI.is_hero(self.user.username)现在可以在services.py中定义服务对象,如下所示:
考虑将业务逻辑或领域逻辑从模型中重构到服务对象中。这样,您也可以在Django应用程序之外使用它们。
假设有一个业务原因,根据用户名将某些用户列入黑名单,以防止他们成为超级英雄类型。我们的服务对象可以很容易地修改以支持这一点:
classSuperHeroWebAPI:...@staticmethoddefis_hero(username):blacklist=set(["syndrome","kcka$$","superfake"])url=API_URL.format(username)returnusernamenotinblacklistandwebclient.get(url)理想情况下,服务对象是自包含的。这使它们易于在没有模拟的情况下进行测试,比如数据库。它们也可以很容易地被重用。
在Django中,使用诸如Celery之类的任务队列异步执行耗时服务。通常,Service对象操作作为Celery任务运行。此类任务可以定期运行或延迟运行。
本节包含处理访问模型属性或对其执行查询的设计模式。
问题:模型具有实现为方法的属性。但是,这些属性不应持久存储到数据库中。
解决方案:对这些方法使用property装饰器。
模型字段存储每个实例的属性,例如名字、姓氏、生日等。它们也存储在数据库中。但是,我们还需要访问一些派生属性,例如全名或年龄。
它们可以很容易地从数据库字段中计算出来,因此不需要单独存储。在某些情况下,它们只是一个条件检查,比如基于年龄、会员积分和活跃状态的优惠资格。
实现这一点的一个直接方法是定义函数,比如get_age,类似于以下内容:
classBaseProfile(models.Model):birthdate=models.DateField()#...defget_age(self):today=datetime.date.today()return(today.year-self.birthdate.year)-int((today.month,today.day)<(self.birthdate.month,self.birthdate.day))调用profile.get_age()将返回用户的年龄,通过计算根据月份和日期调整的年份差。
然而,调用profile.age更可读(和Pythonic)。
Python类可以使用property装饰器将函数视为属性。Django模型也可以使用它。在前面的例子中,用以下内容替换函数定义行:
@propertydefage(self):现在,我们可以通过profile.age访问用户的年龄。注意函数的名称也被缩短了。
属性的一个重要缺点是它对ORM是不可见的,就像模型方法一样。你不能在QuerySet对象中使用它。例如,这样是行不通的,Profile.objects.exclude(age__lt=18)。
也许定义一个属性来隐藏内部类的细节是一个好主意。这在正式上被称为迪米特法则。简单来说,这个法则规定你只能访问自己的直接成员或者“只使用一个点”。
例如,与其访问profile.birthdate.year,最好定义一个profile.birthyear属性。这样可以帮助隐藏birthdate字段的底层结构。
遵循迪米特法则,在访问属性时只使用一个点。
这个法则的一个不良副作用是它会导致模型中创建几个包装属性。这可能会使模型变得臃肿并且难以维护。在合适的地方使用这个法则来改进你的模型API并减少耦合是更可读(和Pythonic)的。
每次调用属性时,我们都在重新计算一个函数。如果这是一个昂贵的计算,我们可能希望缓存结果。这样,下次访问属性时,将返回缓存的值。
fromdjango.utils.functionalimportcached_property#...@cached_propertydeffull_name(self):#Expensiveoperatione.g.externalservicecallreturn"{0}{1}".format(self.firstname,self.lastname)缓存的值将作为Python实例的一部分保存。只要实例存在,就会返回相同的值。
作为一种保险机制,你可能希望强制执行昂贵操作以确保不返回陈旧的值。在这种情况下,设置一个关键字参数,比如cached=False来防止返回缓存的值。
问题:模型上的某些查询在整个代码中被定义和访问,违反了DRY原则。
解决方案:定义自定义管理器来为常见查询提供有意义的名称。
每个Django模型都有一个名为objects的默认管理器。调用objects.all(),将返回数据库中该模型的所有条目。通常,我们只对所有条目的一个子集感兴趣。
我们应用各种过滤器来找到我们需要的条目集。选择它们的标准通常是我们的核心业务逻辑。例如,我们可以通过以下代码找到对公众可访问的帖子:
public=Posts.objects.filter(privacy="public")这个标准可能会在未来发生变化。比如,我们可能还想检查帖子是否被标记为编辑。这个变化可能看起来像这样:
public=Posts.objects.filter(privacy=POST_PRIVACY.Public,draft=False)然而,这个变化需要在需要公共帖子的每个地方进行。这可能会变得非常令人沮丧。需要有一个地方来定义这样的常用查询,而不是“重复自己”。
QuerySets是一个非常强大的抽象。它们只在需要时进行延迟评估。因此,通过方法链接(一种流畅接口的形式)构建更长的QuerySets不会影响性能。
事实上,随着应用更多的过滤,结果数据集会变小。这通常会减少结果的内存消耗。
模型管理器是模型获取其QuerySet对象的便捷接口。换句话说,它们帮助你使用Django的ORM来访问底层数据库。事实上,管理器实际上是围绕QuerySet对象实现的非常薄的包装器。注意相同的接口:
>>>Post.objects.filter(posted_by__username="a")[
自定义管理器用于创建特定领域的高级API。这不仅更易读,而且不受实现细节的影响。因此,你能够在更高层次上工作,与你的领域紧密建模。
我们之前的公共帖子示例可以很容易地转换为自定义管理器,如下所示:
#managers.pyfromdjango.db.models.queryimportQuerySetclassPostQuerySet(QuerySet):defpublic_posts(self):returnself.filter(privacy="public")PostManager=PostQuerySet.as_manager这个方便的快捷方式用于从QuerySet对象创建自定义管理器,出现在Django1.7中。与以往的方法不同,这个PostManager对象可以像默认的objects管理器一样进行链式操作。
有时候,用我们的自定义管理器替换默认的objects管理器是有意义的,就像下面的代码所示:
from.managersimportPostManagerclassPost(Postable):...objects=PostManager()通过这样做,我们的代码可以更简化地访问public_posts如下:
public=Post.objects.public_posts()由于返回的值是一个QuerySet,它们可以进一步过滤:
public_apology=Post.objects.public_posts().filter(message_startswith="Sorry")QuerySets有几个有趣的属性。在接下来的几节中,我们可以看一下涉及组合QuerySets的一些常见模式。
忠于它们的名字(或名字的后半部分),QuerySets支持许多(数学)集合操作。为了举例说明,考虑包含用户对象的两个QuerySets:
>>>q1=User.objects.filter(username__in=["a","b","c"])[
使用Q对象也可以执行相同的操作:
fromdjango.db.modelsimportQ#Union>>>User.objects.filter(Q(username__in=["a","b","c"])|Q(username__in=["c","d"]))[
然而,Set的类比并不完美。QuerySets与数学集合不同,是有序的。因此,在这方面它们更接近于Python的列表数据结构。
到目前为止,我们已经组合了属于同一基类的相同类型的QuerySets。然而,我们可能需要组合来自不同模型的QuerySets并对它们执行操作。
>>>recent=list(posts)+list(comments)>>>sorted(recent,key=lambdae:e.modified,reverse=True)[:3][
一个更好的解决方案使用迭代器来减少内存消耗。使用itertools.chain方法来组合多个QuerySets如下:
>>>fromitertoolsimportchain>>>recent=chain(posts,comments)>>>sorted(recent,key=lambdae:e.modified,reverse=True)[:3]一旦评估了QuerySet,命中数据库的成本可能会相当高。因此,通过只执行将返回未评估的QuerySets的操作,尽可能地延迟它是很重要的。
尽可能保持QuerySets未评估。
迁移帮助你自信地对模型进行更改。在Django1.7中引入的迁移是开发工作流程中必不可少且易于使用的部分。
新的工作流程基本上如下:
pythonmanage.pymakemigrations
pythonmanage.pymigrate
模型设计很难做到完美。然而,对于Django开发来说,这是基础性的。在本章中,我们看了几种处理模型时常见的模式。在每种情况下,我们都看了提议解决方案的影响以及各种权衡。
在下一章中,我们将研究在处理视图和URL配置时遇到的常见设计模式。
在Django中,视图被定义为一个可调用的函数,它接受一个请求并返回一个响应。它通常是一个带有特殊类方法(如as_view())的函数或类。
在这两种情况下,我们创建一个普通的Python函数,它以HTTPRequest作为第一个参数,并返回一个HTTPResponse。URLConf也可以向该函数传递其他参数。这些参数可以从URL的部分捕获或设置为默认值。
一个简单的视图如下所示:
它在URLConf中对应的行如下:
#Inurls.pyurl(r'^hello-fn/(P
基于类的视图是在Django1.4中引入的。以下是将先前的视图重写为功能等效的基于类的视图的样子:
fromdjango.views.genericimportViewclassHelloView(View):defget(self,request,name="World"):returnHttpResponse("Hello{}!".format(name))同样,相应的URLConf将有两行,如下命令所示:
#Inurls.pyurl(r'^hello-cl/(P
使用类的优势在于需要自定义视图时会变得很明显。比如,您需要更改问候语和默认名称。然后,您可以编写一个通用视图类来适应任何类型的问候,并派生您的特定问候类如下:
classGreetView(View):greeting="Hello{}!"default_name="World"defget(self,request,**kwargs):name=kwargs.pop("name",self.default_name)returnHttpResponse(self.greeting.format(name))classSuperVillainView(GreetView):greeting="Wearethefuture,{}.Notthem."default_name="myfriend"现在,URLConf将引用派生类:
#Inurls.pyurl(r'^hello-su/(P
DjangoUnchained
在寻找优秀的Django开发人员花了2周后,史蒂夫开始打破常规。注意到最近黑客马拉松的巨大成功,他和哈特在S.H.I.M组织了一个DjangoUnchained比赛。规则很简单——每天构建一个Web应用程序。它可以很简单,但你不能跳过一天或打破链条。谁创建了最长的链条,谁就赢了。
获胜者——布拉德·扎尼真是个惊喜。作为一个传统的设计师,几乎没有任何编程背景,他曾经参加了为期一周的Django培训,只是为了好玩。他设法创建了一个由21个Django站点组成的不间断链条,大部分是从零开始。
当他们交谈时,布拉德毫不掩饰他不是程序员这一事实。事实上,他根本不需要假装。透过他那副厚框眼镜,透过他那宁静的蓝色眼睛,他解释说他的秘诀非常简单——获得灵感,然后专注。
他过去每天都以一个简单的线框开始。然后,他会使用Twitterbootstrap模板创建一个空的Django项目。他发现Django的基于类的通用视图是以几乎没有代码创建视图的绝佳方式。有时,他会从Django-braces中使用一个或两个mixin。他还喜欢通过管理界面在移动中添加数据。
他最喜欢的项目是Labyrinth——一个伪装成棒球论坛的蜜罐。他甚至设法诱捕了一些搜寻易受攻击站点的监视机器人。当史蒂夫解释了SuperBook项目时,他非常乐意接受这个提议。创建一个星际社交网络的想法真的让他着迷。
通过更多的挖掘,史蒂夫能够在S.H.I.M中找到半打像布拉德这样有趣的个人资料。他得知,他应该首先在组织内部搜索,而不是寻找外部。
基于类的通用视图通常以面向对象的方式实现(模板方法模式)以实现更好的重用。我讨厌术语通用视图。我宁愿称它们为库存视图。就像库存照片一样,您可以在稍微调整的情况下用于许多常见需求。
通用视图是因为Django开发人员觉得他们在每个项目中都在重新创建相同类型的视图。几乎每个项目都需要显示对象列表(ListView),对象的详细信息(DetailView)或用于创建对象的表单(CreateView)的页面。为了遵循DRY原则,这些可重用的视图与Django捆绑在一起。
Django1.7中通用视图的方便表格如下:
我们没有提到诸如BaseDetailView之类的基类或SingleObjectMixin之类的混合类。它们被设计为父类。在大多数情况下,您不会直接使用它们。
大多数人混淆了基于类的视图和基于类的通用视图。它们的名称相似,但它们并不是相同的东西。这导致了一些有趣的误解,如下所示:
混入是类基视图中DRY代码的本质。与模型混入一样,视图混入利用Python的多重继承来轻松重用功能块。它们通常是Python3中没有父类的类(或者在Python2中从object派生,因为它们是新式类)。
混入在明确定义的位置拦截视图的处理。例如,大多数通用视图使用get_context_data来设置上下文字典。这是插入额外上下文的好地方,比如一个feed变量,指向用户可以查看的所有帖子,如下命令所示:
classFeedMixin(object):defget_context_data(self,**kwargs):context=super().get_context_data(**kwargs)context["feed"]=models.Post.objects.viewable_posts(self.request.user)returncontextget_context_data方法首先通过调用所有基类中的同名方法来填充上下文。接下来,它使用feed变量更新上下文字典。
现在,可以很容易地使用这个混入来通过将其包含在基类列表中来添加用户的feed。比如,如果SuperBook需要一个典型的社交网络主页,其中包括一个创建新帖子的表单,然后是您的feed,那么可以使用这个混入如下:
classMyFeed(FeedMixin,generic.CreateView):model=models.Posttemplate_name="myfeed.html"success_url=reverse_lazy("my_feed")一个写得很好的混入几乎没有要求。它应该灵活,以便在大多数情况下都能派上用场。在前面的例子中,FeedMixin将覆盖派生类中的feed上下文变量。如果父类使用feed作为上下文变量,那么它可能会受到包含此混入的影响。因此,使上下文变量可定制会更有用(这留给您作为练习)。
混入能够与其他类结合是它们最大的优势和劣势。使用错误的组合可能导致奇怪的结果。因此,在使用混入之前,您需要检查混入和其他类的源代码,以确保没有方法或上下文变量冲突。
您可能已经遇到了包含几个混入的代码,如下所示:
classComplexView(MyMixin,YourMixin,AccessMixin,DetailView):确定列出基类的顺序可能会变得非常棘手。就像Django中的大多数事情一样,通常适用Python的正常规则。Python的方法解析顺序(MRO)决定了它们应该如何排列。
简而言之,混入首先出现,基类最后出现。父类越专业,它就越向左移动。在实践中,这是您需要记住的唯一规则。
要理解为什么这样做,请考虑以下简单的例子:
classA:defdo(self):print("A")classB:defdo(self):print("B")classBA(B,A):passclassAB(A,B):passBA().do()#PrintsBAB().do()#PrintsA正如您所期望的,如果在基类列表中提到B在A之前,那么将调用B的方法,反之亦然。
现在想象A是一个基类,比如CreateView,B是一个混入,比如FeedMixin。混入是对基类基本功能的增强。因此,混入代码应该首先执行,然后根据需要调用基本方法。因此,正确的顺序是BA(混入在前,基类在后)。
调用基类的顺序可以通过检查类的__mro__属性来确定:
>>>AB.__mro__(__main__.AB,__main__.A,__main__.B,object)因此,如果AB调用super(),首先会调用A;然后,A的super()将调用B,依此类推。
在类视图之前,装饰器是改变基于函数的视图行为的唯一方法。作为函数的包装器,它们不能改变视图的内部工作,因此有效地将它们视为黑匣子。
装饰器是一个接受函数并返回装饰函数的函数。感到困惑?有一些语法糖可以帮助你。使用注解符号@,如下面的login_required装饰器示例所示:
@login_requireddefsimple_view(request):returnHttpResponse()以下代码与上面完全相同:
装饰器不如mixin灵活。但它们更简单。在Django中,您可以同时使用装饰器和mixin。实际上,许多mixin都是用装饰器实现的。
让我们看一些在设计视图中看到的常见设计模式。
解决方案:使用mixin或装饰器来控制对视图的访问。
最后,有些页面只有在满足某些条件时才能访问。例如,只有帖子的创建者才能编辑帖子。其他任何人访问此页面都应该看到权限被拒绝的错误。
有两种方法可以控制对视图的访问:
@login_required(MyView.as_view())fromdjango.utils.decoratorsimportmethod_decoratorclassLoginRequiredMixin:@method_decorator(login_required)defdispatch(self,request,*args,**kwargs):returnsuper().dispatch(request,*args,**kwargs)我们这里真的不需要装饰器。推荐更明确的形式如下:
classLoginRequiredMixin:defdispatch(self,request,*args,**kwargs):ifnotrequest.user.is_authenticated():raisePermissionDeniedreturnsuper().dispatch(request,*args,**kwargs)当引发PermissionDenied异常时,Django会在您的根目录中显示403.html模板,或者在其缺失时显示标准的“403Forbidden”页面。
frombraces.viewsimportLoginRequiredMixin,AnonymousRequiredMixinclassUserProfileView(LoginRequiredMixin,DetailView):#Thisviewwillbeseenonlyifyouarelogged-inpassclassLoginFormView(AnonymousRequiredMixin,FormView):#ThisviewwillNOTbeseenifyouareloggedinauthenticated_redirect_url="/feed"Django中的工作人员是在用户模型中设置了is_staff标志的用户。同样,您可以使用一个名为UserPassesTestMixin的django-bracesmixin,如下所示:
classCheckOwnerMixin:#TobeusedwithclassesderivedfromSingleObjectMixindefget_object(self,queryset=None):obj=super().get_object(queryset)ifnotobj.owner==self.request.user:raisePermissionDeniedreturnobj模式-上下文增强器问题:基于通用视图的几个视图需要相同的上下文变量。
解决方案:创建一个设置共享上下文变量的mixin。
Django模板只能显示存在于其上下文字典中的变量。然而,站点需要在多个页面中具有相同的信息。例如,侧边栏显示您的动态中最近的帖子可能需要在多个视图中使用。
大多数通用的基于类的视图都是从ContextMixin派生的。它提供了get_context_data方法,大多数类都会重写这个方法,以添加他们自己的上下文变量。在重写这个方法时,作为最佳实践,您需要首先调用超类的get_context_data,然后添加或覆盖您的上下文变量。
我们可以将这个抽象成一个mixin的形式,就像我们之前看到的那样:
classFeedMixin(object):defget_context_data(self,**kwargs):context=super().get_context_data(**kwargs)context["feed"]=models.Post.objects.viewable_posts(self.request.user)returncontext我们可以将这个mixin添加到我们的视图中,并在我们的模板中使用添加的上下文变量。请注意,我们正在使用第三章中定义的模型管理器,模型,来过滤帖子。
一个更一般的解决方案是使用django-braces中的StaticContextMixin来处理静态上下文变量。例如,我们可以添加一个额外的上下文变量latest_profile,其中包含最新加入站点的用户:
classCtxView(StaticContextMixin,generic.TemplateView):template_name="ctx.html"static_context={"latest_profile":Profile.objects.latest('pk')}在这里,静态上下文意味着任何从一个请求到另一个请求都没有改变的东西。在这种意义上,您也可以提到QuerySets。然而,我们的feed上下文变量需要self.request.user来检索用户可查看的帖子。因此,在这里不能将其包括为静态上下文。
问题:您网站的信息经常被其他应用程序抓取和处理。
解决方案:创建轻量级服务,以机器友好的格式返回数据,如JSON或XML。
我们经常忘记网站不仅仅是人类使用的。网站流量的很大一部分来自其他程序,如爬虫、机器人或抓取器。有时,您需要自己编写这样的程序来从另一个网站提取信息。
通常,为人类消费而设计的页面对机械提取来说很麻烦。HTML页面中的信息被标记包围,需要进行大量的清理。有时,信息会分散,需要进行大量的数据整理和转换。
在这种情况下,机器接口将是理想的。您不仅可以减少提取信息的麻烦,还可以实现混搭。如果应用程序的功能以机器友好的方式暴露,其功能的持久性将大大增加。
面向服务的架构(SOA)已经推广了服务的概念。服务是向其他应用程序公开的一个独特的功能块。例如,Twitter提供了一个返回最新公共状态的服务。
一个服务必须遵循一定的基本原则:
在Django中,您可以创建一个基本的服务,而无需任何第三方包。您可以返回JSON格式的序列化数据,而不是返回HTML。这种形式的服务通常被称为Web应用程序编程接口(API)。
例如,我们可以创建一个简单的服务,返回SuperBook中最近的五篇公共帖子:
classPublicPostJSONView(generic.View):defget(self,request,*args,**kwargs):msgs=models.Post.objects.public_posts().values("posted_by_id","message")[:5]returnHttpResponse(list(msgs),content_type="application/json")为了更可重用的实现,您可以使用django-braces中的JSONResponseMixin类,使用其render_json_response方法返回JSON:
frombraces.viewsimportJSONResponseMixinclassPublicPostJSONView(JSONResponseMixin,generic.View):defget(self,request,*args,**kwargs):msgs=models.Post.objects.public_posts().values("posted_by_id","message")[:5]returnself.render_json_response(list(msgs))如果我们尝试检索这个视图,我们将得到一个JSON字符串,而不是HTML响应:
当然,如果您需要构建比这个简单API更复杂的东西,您将需要使用诸如DjangoREST框架之类的包。DjangoREST框架负责序列化(和反序列化)QuerySets,身份验证,生成可在Web上浏览的API,以及许多其他必要功能,以创建一个强大而完整的API。
Django拥有最灵活的Web框架之一。基本上,没有暗示的URL方案。您可以使用适当的正则表达式明确定义任何URL方案。
然而,正如超级英雄们喜欢说的那样——“伴随着伟大的力量而来的是巨大的责任。”您不能再随意设计URL。
URL曾经很丑陋,因为人们认为用户会忽略它们。在90年代,门户网站流行时,普遍的假设是您的用户将通过前门,也就是主页进入。他们将通过点击链接导航到网站的其他页面。
在我们深入了解设计URL的细节之前,我们需要了解URL的结构。
从技术上讲,URL属于更一般的标识符家族,称为统一资源标识符(URI)。因此,URL的结构与URI相同。
URI由几个部分组成:
URI=方案+网络位置+路径+查询+片段
![URL解剖
DjangoURL模式主要涉及URI的“路径”部分。所有其他部分都被隐藏起来。
通常有助于将urls.py视为项目的入口点。当我研究Django项目时,这通常是我打开的第一个文件。基本上,urls.py包含整个项目的根URL配置或URLConf。
它将是从patterns返回的Python列表,分配给名为urlpatterns的全局变量。每个传入的URL都会与顺序中的每个模式进行匹配。在第一次匹配时,搜索停止,并且请求被发送到相应的视图。
urlpatterns=patterns('',#Homepageurl(r'^$',views.IndexView.as_view(),name='home'),#Abouturl(r'^about/$',TemplateView.as_view(template_name="python/about.html"),name='about'),#BlogURLsurl(r'^blogs/',include('blogs.urls',namespace='blog')),#Jobarchiveurl(r'^jobs/(P
在未来的Django版本中,urlpatterns应该是一个URL模式对象的普通列表,而不是patterns函数的参数。这对于有很多模式的站点来说很棒,因为urlpatterns作为一个函数只能接受最多255个参数。
如果你是Python正则表达式的新手,你可能会觉得模式语法有点神秘。让我们试着揭开它的神秘面纱。
URL正则表达式模式有时看起来像一团令人困惑的标点符号。然而,像Django中的大多数东西一样,它只是普通的Python。
通过了解URL模式的两个功能,可以很容易地理解它:匹配以某种形式出现的URL,并从URL中提取有趣的部分。
第一部分很容易。如果你需要匹配一个路径,比如/jobs/1234,那么只需使用"^jobs/\d+"模式(这里\d代表从0到9的单个数字)。忽略前导斜杠,因为它会被吞掉。
第二部分很有趣,因为在我们的例子中,有两种提取作业ID(即1234)的方法,这是视图所需的。
最简单的方法是在要捕获的每组值周围放括号。每个值将作为位置参数传递给视图。例如,"^jobs/(\d+)"模式将把值"1234"作为第二个参数(第一个是请求)发送给视图。
位置参数的问题在于很容易混淆顺序。因此,我们有基于名称的参数,其中每个捕获的值都可以被命名。我们的例子现在看起来像"^jobs/(P
如果你有一个基于类的视图,你可以在self.args中访问你的位置参数,在self.kwargs中访问基于名称的参数。许多通用视图期望它们的参数仅作为基于名称的参数,例如self.kwargs["slug"]。
我承认基于名称的参数的语法很难记住。我经常使用一个简单的记忆法作为记忆助手。短语“ParentsQuestionPinkAction-figures”代表括号、问号、(字母)P和尖括号的首字母。
把它们放在一起,你会得到(P<。你可以输入模式的名称,然后自己找出剩下的部分。
这是一个很方便的技巧,而且很容易记住。想象一下一个愤怒的父母拿着一个粉色的浩克动作人物。
总是给你的模式命名。这有助于将你的代码与确切的URL路径解耦。例如,在以前的URLConf中,如果你想重定向到about页面,可能会诱人地使用redirect("/about")。相反,使用redirect("about"),因为它使用名称而不是路径。
以下是一些反向查找的更多示例:
>>>fromdjango.core.urlresolversimportreverse>>>print(reverse("home"))"/">>>print(reverse("job_archive",kwargs={"pk":"1234"}))"jobs/1234/"名称必须是唯一的。如果两个模式有相同的名称,它们将无法工作。因此,一些Django包用于向模式名称添加前缀。例如,一个名为blog的应用程序可能必须将其编辑视图称为'blog-edit',因为'edit'是一个常见的名称,可能会与另一个应用程序发生冲突。
命名空间是为了解决这类问题而创建的。在命名空间中使用的模式名称必须在该命名空间内是唯一的,而不是整个项目。建议您为每个应用程序都分配一个命名空间。例如,我们可以通过在根URLconf中包含此行来创建一个“blog”命名空间,其中只包括博客的URL:
url(r'^blog/',include('blog.urls',namespace='blog')),现在博客应用程序可以使用模式名称,比如“edit”或其他任何名称,只要它们在该应用程序内是唯一的。在引用命名空间内的名称时,您需要在名称之前提到命名空间,然后是“:”。在我们的例子中,它将是“blog:edit”。
正如Python之禅所说-“命名空间是一个非常棒的想法-让我们做更多这样的事情。”如果这样做可以使您的模式名称更清晰,您可以创建嵌套的命名空间,比如“blog:comment:edit”。我强烈建议您在项目中使用命名空间。
按照Django处理它们的方式,即自上而下,对您的模式进行排序以利用它们。一个很好的经验法则是将所有特殊情况放在顶部。更广泛的模式可以在更下面提到。最广泛的-如果存在的话,可以放在最后。
urlpatterns=patterns('',url(r'^about/$',AboutView.as_view(),name='about'),url(r'^(P
一致地设计网站的URL很容易被忽视。设计良好的URL不仅可以合理地组织您的网站,还可以让用户猜测路径变得容易。设计不良的URL甚至可能构成安全风险:比如,在URL模式中使用数据库ID(它以单调递增的整数序列出现)可能会增加信息窃取或网站剥离的风险。
让我们来看一些在设计URL时遵循的常见样式。
有些网站的布局就像百货商店。有一个食品区,里面有一个水果通道,通道里有不同种类的苹果摆在一起。
在URL的情况下,这意味着您将按以下层次结构找到这些页面:
Web为您提供了一些基本的HTTP动词来操作资源:GET,POST,PUT,PATCH和DELETE。请注意,这些不是URL本身的一部分。因此,如果您在URL中使用动词来操作资源,这是一个不好的做法。
例如,以下URL被认为是不好的:
相反,你应该删除动词,并使用POST操作到这个URL:
如果HTTP动词可以使用,就不要在URL中使用动词。
请注意,在URL中使用动词并不是错误的。您网站的搜索URL可以使用动词“search”,因为它不符合REST的一个资源:
RESTfulURL对于设计CRUD接口非常有用。创建、读取、更新和删除数据库操作与HTTP动词之间几乎是一对一的映射。
请注意,RESTfulURL风格是部门商店URL风格的补充。大多数网站混合使用这两种风格。它们被分开以便更清晰地理解。
下载示例代码
Django拥有非常灵活的URL分发系统。设计良好的URL需要考虑几个方面。设计良好的URL也受到用户的赞赏。
在下一章中,我们将看一下Django的模板语言以及如何最好地利用它。
是时候谈谈MTV三人组中的第三个伙伴-模板了。您的团队可能有设计师负责设计模板。或者您可能自己设计它们。无论哪种方式,您都需要非常熟悉它们。毕竟,它们直接面向您的用户。
让我们从快速介绍Django的模板语言特性开始。
每个模板都有一组上下文变量。与Python的字符串format()方法的单花括号{variable}语法类似,Django使用双花括号{{variable}}语法。让我们看看它们的比较:
>>>"
{title}
".format(title="SuperBook")'SuperBook
'>>>fromdjango.templateimportTemplate,Context>>>Template("{{title}}
").render(Context({"title":"SuperBook"}))'SuperBook
'属性在Django模板中,点是一个多功能运算符。有三种不同的操作-属性查找、字典查找或列表索引查找(按顺序)。>>>classDrOct:arms=4defspeak(self):return"Youhaveatraintocatch.">>>mydict={"key":"value"}>>>mylist=[10,20,30]让我们来看看Python对三种查找的语法:
>>>"Dr.Octhas{0}armsandsays:{1}".format(DrOct().arms,DrOct().speak())'Dr.Octhas4armsandsays:Youhaveatraintocatch.'>>>mydict["key"]'value'>>>mylist[1]20Dr.Octhas{{s.arms}}armsandsays:{{s.speak}}{{mydict.key}}{{mylist.1}}注意注意speak,一个除了self之外不带参数的方法,在这里被当作属性对待。
有时,变量需要被修改。基本上,您想要在这些变量上调用函数。Django使用管道语法{{var|method1|method2:"arg"}},而不是链接函数调用,例如var.method1().method2(arg),这类似于Unix过滤器。但是,这种语法只适用于内置或自定义的过滤器。
另一个限制是过滤器无法访问模板上下文。它只能使用传递给它的数据及其参数。因此,它主要用于更改模板上下文中的变量。
>>>title="SuperBook">>>title.upper()[:5]'SUPER'{{title|upper|slice:':5'}}"标签编程语言不仅可以显示变量。Django的模板语言具有许多熟悉的语法形式,如if和for。它们应该以标签语法编写,如{%if%}。几种特定于模板的形式,如include和block,也是以标签语法编写的。
>>>if1==1:...print("Dateis{0}".format(time.strftime("%d-%m-%Y")))Dateis31-08-2014{%if1==1%}Dateis{%now'd-m-Y'%}{%endif%}哲学-不要发明一种编程语言初学者经常问的一个问题是如何在模板中执行数值计算,比如找到百分比。作为设计哲学,模板系统故意不允许以下操作:
将业务逻辑从模板中剥离出来。
startproject命令创建的默认项目布局未定义模板的位置。这很容易解决。在项目的根目录中创建一个名为templates的目录。在您的settings.py中添加TEMPLATE_DIRS变量:
BASE_DIR=os.path.dirname(os.path.dirname(__file__))TEMPLATE_DIRS=[os.path.join(BASE_DIR,'templates')]就是这样。例如,您可以添加一个名为about.html的模板,并在urls.py文件中引用它,如下所示:
urlpatterns=patterns('',url(r'^about/$',TemplateView.as_view(template_name='about.html'),name='about'),您的模板也可以位于应用程序中。在您的app目录内创建一个templates目录是存储特定于应用程序的模板的理想选择。
以下是一些组织模板的良好实践:
从Django1.8开始,将支持多个模板引擎。将内置支持Django模板语言(前面讨论过的标准模板语言)和Jinja2。在许多基准测试中,Jinja2比Django模板要快得多。
乐观夫人
几个星期以来,史蒂夫的办公室角落第一次充满了疯狂的活动。随着更多的新成员加入,现在的五人团队包括布拉德、埃文、雅各布、苏和史蒂夫。就像一个超级英雄团队一样,他们的能力深厚而惊人地平衡。
布拉德和埃文是编码大师。埃文着迷于细节,布拉德是大局观的人。雅各布在发现边缘情况方面的才能使他成为测试的完美人选。苏负责营销和设计。
苏自加入以来一直异常安静,那时她提到她一直在使用Twitter的Bootstrap进行模拟设计。苏是团队中的增长黑客——一个热衷于编码和创意营销的人。
她承认自己只有基本的HTML技能。然而,她的模型设计非常全面,对其他当代社交网络的用户来说看起来很熟悉。最重要的是,它是响应式的,并且在从平板电脑到手机等各种设备上都能完美运行。
管理层一致同意苏的设计,除了一个名叫乐观夫人的人。一个星期五的下午,她冲进苏的办公室,开始质疑从背景颜色到鼠标指针大小的一切。苏试图以令人惊讶的镇定和冷静向她解释。
一个小时后,当史蒂夫决定介入时,乐观夫人正在争论为什么个人资料图片必须是圆形而不是方形。“但是这样的全站更改永远不会及时完成,”他说。乐观夫人转移了目光,对他微笑。突然间,史蒂夫感到一股幸福和希望的波涌上涌。这让他感到非常宽慰和振奋。他听到自己愉快地同意她想要的一切。
后来,史蒂夫得知乐观夫人是一位可以影响易受影响的心灵的次要心灵感应者。他的团队喜欢在最轻微的场合提到后一事实。
如今几乎没有人从头开始建立整个网站。Twitter的Bootstrap或Zurb的Foundation等CSS框架是具有网格系统、出色的排版和预设样式的简单起点。它们大多使用响应式网页设计,使您的网站适合移动设备。
使用Edge项目骨架构建的使用vanillaBootstrapVersion3.0.2的网站
我们将使用Bootstrap,但其他CSS框架的步骤也类似。有三种方法可以在您的网站中包含Bootstrap:
将包含css、js和fonts目录的dist目录复制到您的项目根目录下的static目录中。确保在您的settings.py中为STATICFILES_DIRS设置了这个路径:
STATICFILES_DIRS=[os.path.join(BASE_DIR,"static")]现在您可以在您的模板中包含Bootstrap资源,如下所示:
Bootstrap带有大量选项来改善其视觉吸引力。有一个名为variables.less的文件,其中包含了从主品牌颜色到默认字体等几个变量,如下所示:
@brand-primary:#428bca;@brand-success:#5cb85c;@brand-info:#5bc0de;@brand-warning:#f0ad4e;@brand-danger:#d9534f;@font-family-sans-serif:"HelveticaNeue",Helvetica,Arial,sans-serif;@font-family-serif:Georgia,"TimesNewRoman",Times,serif;@font-family-monospace:Menlo,Monaco,Consolas,"CourierNew",monospace;@font-family-base:@font-family-sans-serif;Bootstrap文档解释了如何设置构建系统(包括LESS编译器)来将这些文件编译成样式表。或者非常方便的是,您可以访问Bootstrap网站的“自定义”区域,在那里在线生成您定制的样式表。
另一种方法是覆盖Bootstrap样式。如果您发现在不同的Bootstrap版本之间升级自定义的Bootstrap样式表非常乏味,那么这是一个推荐的方法。在这种方法中,您可以在一个单独的CSS(或LESS)文件中添加站点范围的样式,并在标准Bootstrap样式表之后包含它。因此,您可以只需对站点范围的样式表进行最小的更改,就可以简单地升级Bootstrap文件。
最后但同样重要的是,您可以通过用更有意义的名称替换结构名称(例如'row'或'column-md-4'替换为'wrapper'或'sidebar')来使您的CSS类更有意义。您可以通过几行LESS代码来实现这一点,如下所示:
.wrapper{.make-row();}.sidebar{.make-md-column(4);}这是可能的,因为有一个叫做mixin的功能(听起来很熟悉吧?)。有了Less源文件,Bootstrap可以完全按照您的需求进行定制。
问题:模板中有很多重复的内容在几个页面中。
解决方案:在可能的地方使用模板继承,并在其他地方包含片段。
用户期望网站的页面遵循一致的结构。某些界面元素,如导航菜单、标题和页脚,在大多数Web应用程序中都会出现。然而,在每个模板中重复它们是很麻烦的。
大多数模板语言都有一个包含机制。另一个文件的内容,可能是一个模板,可以在调用它的位置包含进来。在一个大型项目中,这可能会变得乏味。
在每个模板中包含的片段的顺序大多是相同的。顺序很重要,很难检查错误。理想情况下,我们应该能够创建一个'基础'结构。新页面应该扩展此基础,以指定仅更改或扩展基础内容。
Django模板具有强大的扩展机制。类似于编程中的类,模板可以通过继承进行扩展。但是,为了使其工作,基础本身必须按照以下块的结构进行组织:
base.html模板通常是整个站点的基本结构。该模板通常是格式良好的HTML(即,具有前言和匹配的闭合标签),其中有几个用{%blocktags%}标记标记的占位符。例如,一个最小的base.html文件看起来像下面这样:
{%blockheading%}Untitled{%endblock%}
{%blockcontent%}{%endblock%}这里有两个块,heading和content,可以被覆盖。您可以扩展基础以创建可以覆盖这些块的特定页面。例如,这是一个about页面:{%extends"base.html"%}{%blockcontent%}
ThisisasimpleAboutpage
{%endblock%}{%blockheading%}About{%endblock%}请注意,我们不必重复结构。我们也可以按任何顺序提及块。渲染的结果将在base.html中定义的正确位置具有正确的块。如果继承模板没有覆盖一个块,那么将使用其父级的内容。在前面的例子中,如果about模板没有标题,那么它将具有默认的标题'Untitled'。
继承模板可以进一步继承形成继承链。这种模式可以用来创建具有特定布局的页面的共同派生基础,例如,单列布局。还可以为站点的某个部分创建一个共同的基础模板,例如,博客页面。
通常,所有的继承链都可以追溯到一个共同的根,base.html;因此,这种模式被称为模板继承树。当然,这并不一定要严格遵循。错误页面404.html和500.html通常不会被继承,并且会被剥离大部分标签,以防止进一步的错误。
问题:导航栏是大多数页面中的常见组件。但是,活动链接需要反映用户当前所在的页面。
解决方案:通过设置上下文变量或基于请求路径,有条件地更改活动链接标记。
在导航栏中实现活动链接的天真方式是在每个页面中手动设置它。然而,这既不符合DRY原则,也不是绝对可靠的。
有几种解决方案可以确定活动链接。除了基于JavaScript的方法之外,它们主要可以分为仅模板和基于自定义标签的解决方案。
通过在包含导航模板的同时提及active_link变量,这种解决方案既简单又易于实现。
在每个模板中,您需要包含以下行(或继承它):
{%include"_navbar.html"withactive_link='link2'%}_navbar.html文件包含了带有一组检查活动链接变量的导航菜单:
接下来,在一个适当命名的Python文件中编写您的自定义模板。例如,对于这个活动链接模式,我们可以创建一个名为nav.py的文件,其中包含以下内容:
#app/templatetags/nav.pyfromdjango.core.urlresolversimportresolvefromdjango.templateimportLibraryregister=Library()@register.simple_tagdefactive_nav(request,url):url_name=resolve(request.path).url_nameifurl_name==url:return"active"return""该文件定义了一个名为active_nav的自定义标签。它从请求参数中检索URL的路径组件(比如/about/—参见第四章,“视图和URL”中对URL路径的详细解释)。然后,使用resolve()函数来查找路径对应的URL模式名称(在urls.py中定义)。最后,只有当模式名称匹配预期的模式名称时,它才返回字符串"active"。
在模板中调用这个自定义标签的语法是{%active_navrequest'pattern_name'%}。注意,请求需要在每个使用该标签的页面中传递。
在多个视图中包含一个变量可能会变得繁琐。相反,我们可以在settings.py的TEMPLATE_CONTEXT_PROCESSORS中添加一个内置的上下文处理器,这样请求将在整个站点中以request变量的形式存在。
#settings.pyfromdjango.confimportglobal_settingsTEMPLATE_CONTEXT_PROCESSORS=\global_settings.TEMPLATE_CONTEXT_PROCESSORS+('django.core.context_processors.request',)现在,唯一剩下的就是在模板中使用这个自定义标签来设置活动属性:
在下一章中,我们将探讨Django的一个杀手功能,即管理界面,以及我们如何对其进行定制。
Django备受瞩目的管理员界面使其脱颖而出。它是一个内置应用程序,可以自动生成用户界面以添加和修改站点的内容。对许多人来说,管理员是Django的杀手应用程序,自动化了为项目中的模型创建管理员界面这一乏味任务。
管理员使您的团队能够同时添加内容并继续开发。一旦您的模型准备好并应用了迁移,您只需要添加一两行代码来创建其管理员界面。让我们看看如何做到。
然而,除非您定义相应的ModelAdmin类,否则您的模型在这里将不可见。这通常在您的应用程序的admin.py中定义如下:
fromdjango.contribimportadminfrom.importmodelsadmin.site.register(models.SuperHero)这里,register的第二个参数,一个ModelAdmin类,已被省略。因此,我们将为Post模型获得一个默认的管理员界面。让我们看看如何创建和自定义这个ModelAdmin类。
信标
“在喝咖啡吗?”角落里传来一个声音。苏差点把咖啡洒出来。一个穿着紧身红蓝色服装的高个子男人双手叉腰微笑着站在那里。他胸前的标志大大地写着“显而易见船长”。
“哦,天哪,”苏在用餐巾擦咖啡渍时说道。“抱歉,我想我吓到你了,”显而易见船长说。“有什么紧急情况吗?”
“她不知道这是显而易见的吗?”一个平静的女声从上方传来。苏抬头看到一个阴影般的人物从开放的大厅缓缓降下。她的脸部被她那几缕灰色的头发部分遮挡住。“嗨,海克萨!”船长说。“但是,超级书上的消息是什么?”
几秒钟后,页面刷新了,一个动画的红色信标显眼地出现在顶部。“那就是我说的信标!”显而易见船长惊叫道。“等一下,”史蒂夫说。他打开了当天早些时候部署的新功能的源文件。一眼看到信标功能分支代码就清楚了出了什么问题:
ifswitch_is_active(request,'beacon')andnotrequest.user.is_staff():#Displaythebeacon“对不起,各位,”史蒂夫说。“出现了逻辑错误。我们不是只为员工打开了这个功能,而是不小心为所有人打开了这个功能,除了员工。现在已经关闭了。对于任何混淆,我们深表歉意。”
“所以,没有紧急情况吗?”船长失望地说。海克萨把手搭在他肩上说:“恐怕没有,船长。”突然,传来一声巨响,所有人都跑到了走廊。一个人显然是从天花板到地板的玻璃墙中间降落在办公室里。他甩掉了碎玻璃,站了起来。“对不起,我尽快赶过来了,”他说,“我来晚了吗?”海克萨笑了。“不,闪电。一直在等你加入,”她说。
管理员应用程序足够聪明,可以自动从您的模型中推断出很多东西。但是,有时推断出的信息可以得到改进。这通常涉及向模型本身添加属性或方法(而不是在ModelAdmin类中)。
让我们首先看一个增强模型以获得更好展示的示例,包括管理员界面:
#models.pyclassSuperHero(models.Model):name=models.CharField(max_length=100)added_on=models.DateTimeField(auto_now_add=True)def__str__(self):return"{0}-{1:%Y-%m-%d%H:%M:%S}".format(self.name,self.added_on)defget_absolute_url(self):returnreverse('superhero.views.details',args=[self.id])classMeta:ordering=["-added_on"]verbose_name="superhero"verbose_name_plural="superheroes"让我们看看管理员如何使用所有这些非字段属性:
建议您不仅为管理界面定义先前的Meta属性和方法,还为更好地在shell、日志文件等中表示。
当然,通过创建ModelAdmin类,可以进一步改进管理中的表示,如下所示:
#admin.pyclassSuperHeroAdmin(admin.ModelAdmin):list_display=('name','added_on')search_fields=["name"]ordering=["name"]admin.site.register(models.SuperHero,SuperHeroAdmin)让我们更仔细地看看这些选项:
增强模型的管理页面
上述截图显示了以下插图:
在这里,我们只提到了一些常用的管理选项子集。某些类型的网站会大量使用管理界面。在这种情况下,强烈建议您阅读并了解Django文档中的管理部分。
由于管理界面很容易创建,人们往往滥用它们。一些人仅仅通过打开他们的“工作人员”标志就给予早期用户管理员访问权限。很快,这些用户开始提出功能请求,误以为管理界面是实际应用程序界面。
不幸的是,这并不是管理界面的用途。正如标志所示,它是一个内部工具,供工作人员输入内容使用。它已经准备好投入生产,但并不真正面向您网站的最终用户。
最好将管理用于简单的数据输入。例如,在我审查过的一个项目中,每个老师都被设置为Django应用程序管理大学课程的管理员。这是一个糟糕的决定,因为管理界面让老师感到困惑。
安排课程的工作流程涉及检查其他教师和学生的日程安排。使用管理界面使他们直接查看数据库。对于管理员如何修改数据,几乎没有任何控制。
不要将管理访问权限授予最终用户。
确保您的所有管理员都了解通过管理进行更改可能导致的数据不一致性。如果可能的话,手动记录或使用应用程序,例如django-audit-loglog,可以记录未来参考所做的管理更改。
在大学示例中,我们为教师创建了一个单独的界面,例如课程构建器。只有当用户具有教师配置文件时,这些工具才会可见和可访问。
基本上,纠正大多数管理界面的误用涉及为某些用户组创建更强大的工具。但是,不要采取简单(错误的)路径,授予他们管理访问权限。
开箱即用的管理界面非常有用。不幸的是,大多数人认为很难更改Django管理界面,因此将其保持原样。实际上,管理界面是非常可定制的,只需付出最少的努力即可大幅改变其外观。
许多管理界面的用户可能会被标题“Djangoadministration”困惑。更改为一些自定义的内容,例如“MySiteadmin”或者“SuperBookSecretArea”可能更有帮助。
这种更改非常容易。只需将以下行添加到站点的urls.py中:
admin.site.site_header="SuperBookSecretArea"更改基础和样式表几乎每个管理页面都是从名为admin/base_site.html的通用基础模板扩展而来。这意味着只要稍微了解HTML和CSS,您就可以进行各种自定义,改变管理界面的外观和感觉。
只需在任何templates目录中创建一个名为admin的目录。然后,从Django源目录中复制base_site.html文件,并根据需要进行修改。如果您不知道模板的位置,请在Djangoshell中运行以下命令:
>>>fromos.pathimportjoin>>>fromdjango.contribimportadmin>>>print(join(admin.__path__[0],"templates","admin"))/home/arun/env/sbenv/lib/python3.4/site-packages/django/contrib/admin/templates/admin最后一行是所有管理模板的位置。您可以覆盖或扩展这些模板中的任何一个。有关扩展模板的示例,请参考下一节。
关于自定义管理基础模板的示例,您可以将整个管理界面的字体更改为来自GoogleFonts的“SpecialElite”,这对于赋予一种模拟严肃的外观非常有用。您需要在模板目录之一中添加一个admin/base_site.html文件,内容如下:
有时,您需要在管理界面中包含JavaScript代码。常见的要求是为您的TextField使用HTML编辑器,例如CKEditor。
在Django中有几种实现这一点的方法,例如在ModelAdmin类上使用Media内部类。但是,我发现扩展管理change_form模板是最方便的方法。
例如,如果您有一个名为Posts的应用程序,则需要在templates/admin/posts/目录中创建一个名为change_form.html的文件。如果需要为该应用程序中任何模型的message字段显示CKEditor(也可以是任何JavaScript编辑器,但我更喜欢这个),则文件的内容可以如下所示:
{%extends"admin/change_form.html"%}{%blockfooter%}{{block.super}}
总的来说,管理界面设计得相当不错。然而,它是在2006年设计的,大部分看起来也是这样。它没有移动UI或其他今天已经成为标准的美化功能。
毫不奇怪,对管理自定义的最常见请求是是否可以与Bootstrap集成。有几个包可以做到这一点,比如django-admin-bootstrapped或djangosuit。
这些包提供了现成的基于Bootstrap主题的模板,易于安装和部署。基于Bootstrap,它们是响应式的,并带有各种小部件和组件。
也有人尝试完全重新构想管理界面。Grappelli是一个非常受欢迎的皮肤,它通过自动完成查找和可折叠的内联等新功能扩展了Django管理。使用django-admin-tools,您可以获得可定制的仪表板和菜单栏。
在生产中,建议将此位置更改为不太明显的位置。只需在根urls.py中更改这一行即可:
url(r'^secretarea/',include(admin.site.urls)),一个稍微更复杂的方法是在默认位置使用一个虚拟的管理站点或者蜜罐(参见django-admin-honeypot包)。然而,最好的选择是在管理区域使用HTTPS,因为普通的HTTP会将所有数据以明文形式发送到网络上。
查看您的Web服务器文档,了解如何为管理请求设置HTTPS。在Nginx上,设置这个很容易,涉及指定SSL证书的位置。最后,将所有管理页面的HTTP请求重定向到HTTPS,这样你就可以更加安心地睡觉了。
以下模式不仅限于管理界面,但仍然包括在本章中,因为它经常在管理中受到控制。
问题:向用户发布新功能和在生产环境中部署相应的代码应该是独立的。
解决方案:使用功能标志在部署后选择性地启用或禁用功能。
今天,频繁地将错误修复和新功能推向生产是很常见的。其中许多变化并不为用户所注意。然而,在可用性或性能方面有重大影响的新功能应该以分阶段的方式推出。换句话说,部署应该与发布分离。
因此,在大型网站中,重要的是将新功能的部署与在生产环境中激活它们分开。即使它们被激活,有时也只能被一小部分用户看到。这个小组可以是员工或一小部分客户用于试用目的。
许多网站使用功能标志来控制新功能的激活。功能标志是代码中的开关,用于确定是否应向某些客户提供某项功能。
几个Django包提供了功能标志,如gargoyle和django-waffle。这些包将站点的功能标志存储在数据库中。它们可以通过管理界面或管理命令激活或停用。因此,每个环境(生产、测试、开发等)都可以拥有自己激活的功能集。
功能标志可以用于各种其他情况(以下示例使用django-waffle):
defmy_view(request):ifflag_is_active(request,'flag_name'):#Behaviorifflagisactive.网站可以同时运行几个这样的试验,因此不同的用户可能会有不同的用户体验。在更广泛的部署之前,会从这些受控测试中收集指标和反馈。
defmy_view(request):ifswitch_is_active('s3_down'):#Disableuploadsandshowitisdowntime这种方法的主要缺点是代码中充斥着条件检查。但是,可以通过定期的代码清理来控制这一点,以删除对已完全接受的功能的检查,并清除永久停用的功能。
在本章中,我们探讨了Django内置的管理应用程序。我们发现它不仅可以直接使用,而且还可以进行各种自定义以改善其外观和功能。
在下一章中,我们将探讨如何通过考虑各种模式和常见用例来更有效地使用Django中的表单。
让我们把Django表单放在一边,谈谈一般的网络表单。表单不仅仅是一长串的、乏味的页面,上面有几个你必须填写的项目。表单无处不在。我们每天都在使用它们。表单驱动着从谷歌的搜索框到Facebook的赞按钮的一切。
理解表单可能有些棘手,因为与它们的交互需要多个请求-响应周期。在最简单的情况下,您需要呈现一个空表单,用户填写正确并提交它。在其他情况下,他们输入了一些无效数据,表单需要重新提交,直到整个表单有效为止。
因此,表单经历了几种状态:
请注意,用户永远不会看到表单处于最后状态。他们不必这样做。提交有效的表单应该将用户带到成功页面。
Django的form类包含每个字段的状态,通过总结它们到一个级别,还包括表单本身的状态。表单有两个重要的状态属性,如下所示:
例如,假设您需要一个简单的表单,接受用户的姓名和年龄。表单类可以定义如下:
#forms.pyfromdjangoimportformsclassPersonDetailsForm(forms.Form):name=forms.CharField(max_length=100)age=forms.IntegerField()这个类可以以绑定或未绑定的方式初始化,如下面的代码所示:
>>>f=PersonDetailsForm()>>>print(f.as_p())
表单只能在创建表单对象时绑定,也就是在构造函数中。用户输入是如何进入包含每个表单字段值的类似字典的对象中的呢?
要了解这一点,您需要了解用户如何与表单交互。在下图中,用户打开人员详细信息表单,首先填写不正确,然后提交,然后使用有效信息重新提交:
如前图所示,当用户提交表单时,视图可调用获取request.POST中的所有表单数据(QueryDict的实例)。表单使用这个类似字典的对象进行初始化,因为它的行为类似于字典并且具有一些额外的功能。
表单可以定义为以两种不同的方式发送表单数据:GET或POST。使用METHOD="GET"定义的表单将表单数据编码在URL本身中,例如,当您提交Google搜索时,您的URL将具有您的表单输入,即搜索字符串可见地嵌入其中,例如q=Cat+Pictures。GET方法用于幂等表单,它不会对世界的状态进行任何持久性更改(或者更严谨地说,多次处理表单的效果与一次处理它的效果相同)。在大多数情况下,这意味着它仅用于检索数据。
然而,绝大多数的表单都是用METHOD="POST"定义的。在这种情况下,表单数据会随着HTTP请求的主体一起发送,用户看不到。它们用于任何涉及副作用的事情,比如存储或更新数据。
取决于您定义的表单类型,当用户提交表单时,视图将在request.GET或request.POST中接收表单数据。如前所述,它们中的任何一个都将像字典一样。因此,您可以将其传递给您的表单类构造函数以获取一个绑定的form对象。
入侵
史蒂夫蜷缩着,沉沉地在他的大三座沙发上打呼噜。在过去的几个星期里,他一直在办公室呆了超过12个小时,今晚也不例外。他的手机放在地毯上发出了哔哔声。起初,他还在睡梦中说了些什么,然后,它一次又一次地响,声音越来越紧急。
史蒂夫感到一阵恶心。Sauron是他们对抗网络攻击和其他可能攻击的第一道防线。当他向任务控制团队发出警报时,已经是凌晨三点了。雅各布一直在和他聊天。他运行了所有可用的诊断工具。没有任何安全漏洞的迹象。
史蒂夫试图让自己冷静下来。他安慰自己也许只是暂时超载,应该休息一下。然而,他知道雅各布不会停止,直到找到问题所在。他也知道Sauron不会出现暂时超载的情况。感到极度疲惫,他又睡了过去。
第二天早上,史蒂夫手持一个百吉饼匆匆赶往办公楼时,听到了一阵震耳欲聋的轰鸣声。他转过身,看到一艘巨大的飞船朝他飞来。本能地,他躲到了篱笆后面。在另一边,他听到几个沉重的金属物体落到地面上的声音。就在这时,他的手机响了。是雅各布。有什么东西靠近了他。史蒂夫抬头一看,看到了一个将近10英尺高的机器人,橙色和黑色相间,直指他的头上,看起来像是一把武器。
“记得我们用UserHoller的表单小部件收集客户反馈吗?显然,他们的数据并不那么干净。我的意思是有几个严重的漏洞。嘿,有很多背景噪音。那是电视吗?”史蒂夫朝着一个大大的标志牌扑去,上面写着“安全集结点”。“别理它。告诉我发生了什么事,”他尖叫道。
“好的。所以,当我们的管理员打开他们的反馈页面时,他的笔记本电脑一定被感染了。这个蠕虫可能会传播到他有权限访问的其他系统,特别是Sauron。我必须说,雅各布,这是一次非常有针对性的攻击。了解我们安全系统的人设计了这个。我有一种不祥的预感,有可怕的事情即将发生。”
最终,您需要从表单中获取“清理后的数据”。这是否意味着用户输入的值不干净?是的,有两个原因。
首先,来自外部世界的任何东西最初都不应该被信任。恶意用户可以通过一个表单输入各种各样的漏洞,从而破坏您网站的安全性。因此,任何表单数据在使用之前都必须经过清理。
永远不要相信用户输入。
其次,request.POST或request.GET中的字段值只是字符串。即使您的表单字段可以定义为整数(比如年龄)或日期(比如生日),浏览器也会将它们作为字符串发送到您的视图。无论如何,您都希望在使用之前将它们转换为适当的Python类型。form类在清理时会自动为您执行此转换。
让我们看看这个实际操作:
>>>fill={"name":"Blitz","age":"30"}>>>g=PersonDetailsForm(fill)>>>g.is_valid()True>>>g.cleaned_data{'age':30,'name':'Blitz'}>>>type(g.cleaned_data["age"])int年龄值作为字符串(可能来自request.POST)传递给表单类。验证后,清理数据包含整数形式的年龄。这正是你所期望的。表单试图抽象出字符串传递的事实,并为您提供可以使用的干净的Python对象。
Django表单还可以帮助您创建表单的HTML表示。它们支持三种不同的表示形式:as_p(作为段落标签),as_ul(作为无序列表项)和as_table(作为,不出所料,表格)。
这些表示形式的模板代码、生成的HTML代码和浏览器渲染已经总结在下表中:
{{form.as_table}}
在模板中为每个表单编写如此多的样板代码可能会让人感到厌烦。django-crispy-forms包使得编写表单模板代码更加简洁(在长度上)。它将所有的演示和布局都移到了Django表单本身。这样,您可以编写更多的Python代码,而不是HTML。
下表显示了脆弱的表单模板标记生成了一个更完整的表单,并且外观更符合Bootstrap样式:
那么,如何获得更清晰的表单?您需要安装django-crispy-forms包并将其添加到INSTALLED_APPS中。如果您使用Bootstrap3,则需要在设置中提到这一点:
CRISPY_TEMPLATE_PACK="bootstrap3"表单初始化将需要提及FormHelper类型的辅助属性。下面的代码旨在尽量简化,并使用默认布局:
fromcrispy_forms.helperimportFormHelperfromcrispy_forms.layoutimportSubmitclassPersonDetailsForm(forms.Form):name=forms.CharField(max_length=100)age=forms.IntegerField()def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.helper=FormHelper(self)self.helper.layout.append(Submit('submit','Submit'))理解CSRF因此,您一定会注意到表单模板中有一个名为CSRF令牌的东西。它是针对您的表单的跨站请求伪造(CSRF)攻击的安全机制。
它通过注入一个名为CSRF令牌的服务器生成的随机字符串来工作,该令牌对用户的会话是唯一的。每次提交表单时,必须有一个包含此令牌的隐藏字段。此令牌确保表单是由原始站点为用户生成的,而不是攻击者创建的具有类似字段的伪造表单。
不建议为使用GET方法的表单使用CSRF令牌,因为GET操作不应更改服务器状态。此外,通过GET提交的表单将在URL中公开CSRF令牌。由于URL有更高的被记录或被窥视的风险,最好在使用POST方法的表单中使用CSRF。
我们可以通过对基于类的视图本身进行子类化来实质上处理表单:
classClassBasedFormView(generic.View):template_name='form.html'defget(self,request):form=PersonDetailsForm()returnrender(request,self.template_name,{'form':form})defpost(self,request):form=PersonDetailsForm(request.POST)ifform.is_valid():#Success!Wecanuseform.cleaned_datanowreturnredirect('success')else:#Invalidform!Reshowtheformwitherrorhighlightedreturnrender(request,self.template_name,{'form':form})将此代码与我们之前看到的序列图进行比较。这三种情况已经分别处理。
每个表单都应遵循Post/Redirect/Get(PRG)模式。如果提交的表单被发现有效,则必须发出重定向。这可以防止重复的表单提交。
但是,这不是一个非常DRY的代码。表单类名称和模板名称属性已被重复。使用诸如FormView之类的通用基于类的视图可以减少表单处理的冗余。以下代码将以更少的代码行数为您提供与以前相同的功能:
fromdjango.core.urlresolversimportreverse_lazyclassGenericFormView(generic.FormView):template_name='form.html'form_class=PersonDetailsFormsuccess_url=reverse_lazy("success")在这种情况下,我们需要使用reverse_lazy,因为在导入视图文件时,URL模式尚未加载。
让我们看一些处理表单时常见的模式。
解决方案:在表单初始化期间添加或更改字段。
每个表单实例都有一个名为fields的属性,它是一个保存所有表单字段的字典。这可以在运行时进行修改。在表单初始化期间可以添加或更改字段。
例如,如果我们需要在用户详细信息表单中添加一个复选框,只有在表单初始化时命名为"upgrade"的关键字参数为true时,我们可以实现如下:
classPersonDetailsForm(forms.Form):name=forms.CharField(max_length=100)age=forms.IntegerField()def__init__(self,*args,**kwargs):upgrade=kwargs.pop("upgrade",False)super().__init__(*args,**kwargs)#Showfirstclassoptionifupgrade:self.fields["first_class"]=forms.BooleanField(label="FlyFirstClass")现在,我们只需要传递PersonDetailsForm(upgrade=True)关键字参数,就可以使一个额外的布尔输入字段(复选框)出现。
请注意,在调用super之前,新引入的关键字参数必须被移除或弹出,以避免unexpectedkeyword错误。
如果我们在这个例子中使用FormView类,则需要通过覆盖视图类的get_form_kwargs方法传递关键字参数,如下面的代码所示:
classPersonDetailsEdit(generic.FormView):...defget_form_kwargs(self):kwargs=super().get_form_kwargs()kwargs["upgrade"]=Truereturnkwargs此模式可用于在运行时更改字段的任何属性,例如其小部件或帮助文本。它也适用于模型表单。
在许多情况下,看似需要动态表单的需求可以使用Django表单集来解决。当需要在页面中重复一个表单时,可以使用表单集。表单集的典型用例是在设计类似数据网格的视图时,逐行添加元素。这样,您不需要创建具有任意行数的动态表单。您只需要为行创建一个表单,并使用formset_factory函数创建多行。
根据用户的不同,表单可以以不同的方式呈现。某些用户可能不需要填写所有字段,而另一些用户可能需要添加额外的信息。在某些情况下,您可能需要对用户的资格进行一些检查,例如验证他们是否是某个组的成员,以确定应该如何构建表单。
正如您可能已经注意到的,您可以使用动态表单生成模式中提供的解决方案来解决这个问题。您只需要将request.user作为关键字参数传递给表单。但是,我们也可以使用django-braces包中的mixin来实现更简洁和更可重用的解决方案。
与前面的例子一样,我们需要向用户显示一个额外的复选框。但是,只有当用户是VIP组的成员时才会显示。让我们看看如何使用django-braces中的表单mixinUserKwargModelFormMixin简化了PersonDetailsForm:
frombraces.formsimportUserKwargModelFormMixinclassPersonDetailsForm(UserKwargModelFormMixin,forms.Form):...def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)#AreyouamemberoftheVIPgroupifself.user.groups.filter(name="VIP").exists():self.fields["first_class"]=forms.BooleanField(label="FlyFirstClass")请注意,mixin通过弹出user关键字参数自动使self.user可用。
classVIPCheckFormView(LoginRequiredMixin,UserFormKwargsMixin,generic.FormView):form_class=PersonDetailsForm...现在,user参数将自动传递给PersonDetailsForm表单。
请查看django-braces中的其他表单mixin,例如FormValidMessageMixin,这些都是常见表单使用模式的现成解决方案。
问题:在单个视图或页面中处理多个表单操作。
解决方案:表单可以使用单独的视图来处理表单提交,或者单个视图可以根据Submit按钮的名称来识别表单。
Django相对简单地将多个具有相同操作的表单组合在一起,例如一个单独的提交按钮。然而,大多数网页需要在同一页上显示多个操作。例如,您可能希望用户在同一页上通过两个不同的表单订阅或取消订阅通讯。
然而,Django的FormView设计为每个视图场景处理一个表单。许多其他通用的基于类的视图也有这种假设。
处理多个表单有两种方法:单独视图和单一视图。让我们先看看第一种方法。
这是一个非常直接的方法,每个表单都指定不同的视图作为它们的操作。例如,订阅和取消订阅表单。可以有两个单独的视图类来处理它们各自表单的POST方法。
在使用相同的视图类处理多个表单时,挑战在于识别哪个表单发出了POST操作。在这里,我们利用了Submit按钮的名称和值也会被提交的事实。如果Submit按钮在各个表单中具有唯一的名称,那么在处理过程中就可以识别表单。
在这里,我们使用crispyforms定义一个订阅表单,以便我们也可以命名submit按钮:
classSubscribeForm(forms.Form):email=forms.EmailField()def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.helper=FormHelper(self)self.helper.layout.append(Submit('subscribe_butn','Subscribe'))UnSubscribeForm取消订阅表单类的定义方式完全相同(因此被省略),只是其Submit按钮的名称为unsubscribe_butn。
由于FormView设计为单个表单,我们将使用一个更简单的基于类的视图,比如TemplateView,作为我们视图的基础。让我们来看看视图定义和get方法:
from.formsimportSubscribeForm,UnSubscribeFormclassNewsletterView(generic.TemplateView):subcribe_form_class=SubscribeFormunsubcribe_form_class=UnSubscribeFormtemplate_name="newsletter.html"defget(self,request,*args,**kwargs):kwargs.setdefault("subscribe_form",self.subcribe_form_class())kwargs.setdefault("unsubscribe_form",self.unsubcribe_form_class())returnsuper().get(request,*args,**kwargs)TemplateView类的关键字参数方便地插入到模板上下文中。我们只有在它们不存在时才创建任一表单的实例,借助setdefault字典方法的帮助。我们很快就会看到原因。
接下来,我们将看一下POST方法,它处理来自任一表单的提交:
defpost(self,request,*args,**kwargs):form_args={'data':self.request.POST,'files':self.request.FILES,}if"subscribe_butn"inrequest.POST:form=self.subcribe_form_class(**form_args)ifnotform.is_valid():returnself.get(request,subscribe_form=form)returnredirect("success_form1")elif"unsubscribe_butn"inrequest.POST:form=self.unsubcribe_form_class(**form_args)ifnotform.is_valid():returnself.get(request,unsubscribe_form=form)returnredirect("success_form2")returnsuper().get(request)首先,表单关键字参数,如data和files,在form_args字典中填充。接下来,在request.POST中检查第一个表单的Submit按钮是否存在。如果找到按钮的名称,则实例化第一个表单。
如果表单未通过验证,则返回由第一个表单实例创建的GET方法创建的响应。同样,我们查找第二个表单的提交按钮,以检查是否提交了第二个表单。
在同一个视图中实现相同表单的实例可以通过表单前缀以相同的方式实现。您可以使用前缀参数实例化一个表单,例如SubscribeForm(prefix="offers")。这样的实例将使用给定的参数为其所有表单字段添加前缀,有效地像表单命名空间一样工作。
问题:为模型创建CRUD接口的样板代码是重复的。
解决方案:使用通用基于类的编辑视图。
从头开始编写这样的接口可能会变得乏味。如果可以从模型类自动创建CRUD接口,这种模式就可以很容易地管理。
Django通过一组四个通用的基于类的视图简化了创建CRUD视图的过程。它们可以映射到它们对应的操作,如下所示:
让我们看一个简单的例子。我们有一个包含重要日期的模型,这对于使用我们的网站的每个人都很重要。我们需要构建简单的CRUD接口,以便任何人都可以查看和修改这些日期。让我们看看ImportantDate模型本身:
#models.pyclassImportantDate(models.Model):date=models.DateField()desc=models.CharField(max_length=100)defget_absolute_url(self):returnreverse('impdate_detail',args=[str(self.pk)])get_absolute_url()方法被CreateView和UpdateView类使用,用于在成功创建或更新对象后重定向。它已经路由到对象的DetailView。
CRUD视图本身足够简单,可以自解释,如下面的代码所示:
#views.pyfromdjango.core.urlresolversimportreverse_lazyfrom.importformsclassImpDateDetail(generic.DetailView):model=models.ImportantDateclassImpDateCreate(generic.CreateView):model=models.ImportantDateform_class=forms.ImportantDateFormclassImpDateUpdate(generic.UpdateView):model=models.ImportantDateform_class=forms.ImportantDateFormclassImpDateDelete(generic.DeleteView):model=models.ImportantDatesuccess_url=reverse_lazy("impdate_list")在这些通用视图中,模型类是唯一必须提及的成员。然而,在DeleteView的情况下,还需要提及success_url函数。这是因为在删除后,不能再使用get_absolute_url来找出要重定向用户的位置。
定义form_class属性不是强制性的。如果省略,将创建一个与指定模型对应的ModelForm方法。然而,我们希望创建自己的模型表单以利用crispyforms,如下面的代码所示:
#forms.pyfromdjangoimportformsfrom.importmodelsfromcrispy_forms.helperimportFormHelperfromcrispy_forms.layoutimportSubmitclassImportantDateForm(forms.ModelForm):classMeta:model=models.ImportantDatefields=["date","desc"]def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.helper=FormHelper(self)self.helper.layout.append(Submit('save','Save'))由于crispyforms,我们在模板中几乎不需要太多的HTML标记来构建这些CRUD表单。
请注意,明确提及ModelForm方法的字段是最佳实践,并且很快将在未来的版本中成为强制性的。
默认情况下,模板路径基于视图类和模型名称。为简洁起见,我们在这里省略了模板源。请注意,我们可以在CreateView和UpdateView中使用相同的表单。
最后,我们来看看urls.py,在那里一切都被连接在一起:
url(r'^impdates/create/$',pviews.ImpDateCreate.as_view(),name="impdate_create"),url(r'^impdates/(P
在下一章中,我们将系统地探讨如何处理遗留的Django代码库,并如何增强它以满足不断发展的客户需求。
当你被要求加入一个项目时,听起来很令人兴奋。可能会有强大的新工具和尖端技术等着你。然而,很多时候,你被要求与现有的、可能是古老的代码库一起工作。
如果要求重新创建环境,那么您可能需要在本地或网络上处理操作系统配置、数据库设置和运行服务。这个谜团有太多的部分,让你想知道如何开始和从哪里开始。
了解代码中使用的Django版本是关键信息。随着Django的发展,从默认项目结构到推荐的最佳实践,一切都发生了变化。因此,确定使用的Django版本是理解它的重要部分。
交接
“是的,”布拉德说,“哈特在哪里?”欧康夫人犹豫了一下,回答说:“嗯,他辞职了。作为IT安全主管,他对周界被突破负有道德责任。”显然受到震惊的史蒂夫摇了摇头。“对不起,”她继续说道,“但我被指派负责SuperBook,并确保我们没有障碍来满足新的截止日期。”
有一阵集体的抱怨声。欧康夫人毫不畏惧,拿起其中一张纸开始说:“这里写着,远程存档模块是未完成状态中最重要的项目。我相信伊万正在处理这个。”
“没错,”远处的伊万说。“快了,”他对其他人微笑着,他们的注意力转向了他。欧康夫人从眼镜的边缘上方凝视着,微笑得几乎太客气了。“考虑到我们在Sentinel代码库中已经有一个经过充分测试和运行良好的Archiver,我建议你利用它,而不是创建另一个多余的系统。”
“但是,”史蒂夫打断道,“这几乎不是多余的。我们可以改进传统的存档程序,不是吗?”“如果没有坏,就不要修理”,欧康夫人简洁地回答道。他说:“他正在努力,”布拉德几乎大声喊道,“他已经完成了所有的工作,那怎么办?”
“伊万,你到目前为止完成了多少工作?”欧康夫人有点不耐烦地问道。“大约12%”,他辩解地回答道。每个人都不可思议地看着他。“什么?那是最难的12%”,他补充道。
欧康夫人以同样的模式继续了会议的其余部分。每个人的工作都被重新排列,以适应新的截止日期。当她拿起她的文件准备离开时,她停顿了一下,摘下了眼镜。
“我知道你们都在想什么...真的。但你们需要知道,我们对截止日期别无选择。我现在能告诉你们的就是,全世界都指望着你们在那个日期之前完成,无论如何。”她戴上眼镜,离开了房间。
“我肯定会带上我的锡纸帽,”伊万大声对自己说。
理想情况下,每个项目都会在根目录下有一个requirements.txt或setup.py文件,并且它将包含用于该项目的Django的确切版本。让我们寻找类似于这样的一行:
Django==1.5.9请注意,版本号是精确指定的(而不是Django>=1.5.9),这被称为固定。固定每个软件包被认为是一个很好的做法,因为它减少了意外,并使您的构建更加确定。
不幸的是,有些真实世界的代码库中requirements.txt文件没有被更新,甚至完全丢失。在这种情况下,您需要探测各种迹象来找出确切的版本。
在大多数情况下,Django项目将部署在虚拟环境中。一旦找到项目的虚拟环境,您可以通过跳转到该目录并运行操作系统的激活脚本来激活它。对于Linux,命令如下:
$sourcevenv_path/bin/activate一旦虚拟环境激活,启动Pythonshell并查询Django版本如下:
$python>>>importdjango>>>print(django.get_version())1.5.9在这种情况下使用的Django版本是1.5.9版本。
或者,您可以在项目中运行manage.py脚本以获得类似的输出:
$pythonmanage.py--version1.5.9但是,如果传统项目源快照以未部署的形式发送给您,则此选项将不可用。如果虚拟环境(和包)也包括在内,那么您可以轻松地在Django目录的__init__.py文件中找到版本号(以元组形式)。例如:
$cdenvs/foo_env/lib/python2.7/site-packages/django$cat__init__.pyVERSION=(1,5,9,'final',0)...如果所有这些方法都失败了,那么您将需要查看过去Django版本的发布说明,以确定可识别的更改(例如,AUTH_PROFILE_MODULE设置自1.5版本以来已被弃用),并将其与您的传统代码进行匹配。一旦确定了正确的Django版本,那么您就可以继续分析代码。
其中最难适应的一个想法,特别是如果您来自PHP或ASP.NET世界,那就是源文件不位于您的Web服务器的文档根目录中,通常命名为wwwroot或public_html。此外,代码的目录结构与网站的URL结构之间没有直接关系。
实际上,您会发现您的Django网站的源代码存储在一个隐蔽的路径中,比如/opt/webapps/my-django-app。为什么会这样呢?在许多很好的理由中,将机密数据移出公共webroot通常更安全。这样,网络爬虫就不会意外地进入您的源代码目录。
正如您在第十一章中所读到的,生产就绪,源代码的位置可以通过检查您的Web服务器的配置文件来找到。在这里,您将找到环境变量DJANGO_SETTINGS_MODULE设置为模块路径,或者它将将请求传递给配置为指向您的project.wsgi文件的WSGI服务器。
即使您可以访问Django网站的整个源代码,弄清楚它在各种应用程序中的工作方式可能令人望而生畏。通常最好从根urls.pyURLconf文件开始,因为它实际上是将每个请求与相应视图联系起来的地图。
对于普通的Python程序,我经常从执行的开始开始阅读,比如从顶级主模块或__main__检查成语开始的地方。在Django应用程序的情况下,我通常从urls.py开始,因为根据站点具有的各种URL模式来跟踪执行流程更容易。
在Linux中,您可以使用以下find命令来定位settings.py文件和指定根urls.py的相应行:
$find.-inamesettings.py-execgrep-H'ROOT_URLCONF'{}\;./projectname/settings.py:ROOT_URLCONF='projectname.urls'$lsprojectname/urls.pyprojectname/urls.py在代码中跳转有时阅读代码感觉像在浏览没有超链接的网页。当您遇到在其他地方定义的函数或变量时,您将需要跳转到包含该定义的文件。只要告诉IDE要跟踪项目的哪些文件,一些IDE就可以自动为您执行此操作。
如果您使用Emacs或Vim,那么您可以创建一个TAGS文件以快速在文件之间导航。转到项目根目录并运行一个名为ExuberantCtags的工具,如下所示:
find.-iname"*.py"-print|etags-这将创建一个名为TAGS的文件,其中包含位置信息,其中定义了诸如类和函数之类的每个句法单元。在Emacs中,您可以使用M-.命令找到标签的定义,其中您的光标(或在Emacs中称为点)所在的位置。
虽然对于大型代码库来说,使用标签文件非常快速,但它相当基本,并不知道虚拟环境(大多数定义可能位于其中)。一个很好的替代方案是在Emacs中使用elpy包。它可以配置为检测虚拟环境。使用相同的M-.命令跳转到句法元素的定义。但是,搜索不限于标签文件。因此,您甚至可以无缝地跳转到Django源代码中的类定义。
很少能找到具有良好文档的遗留代码。即使您有文档,文档可能与代码不同步,这可能会导致进一步的问题。通常,理解应用程序功能的最佳指南是可执行的测试用例和代码本身。
大多数人发现,如果向他们展示一个高层次的图表,他们更容易理解一个应用程序。虽然理想情况下,这是由了解应用程序工作原理的人创建的,但也有工具可以创建非常有帮助的Django应用程序的高层次描述。
graph_models管理命令可以生成应用程序中所有模型的图形概述,该命令由django-command-extensions包提供。如下图所示,可以一目了然地理解模型类及其关系:
SuperBook项目中使用的模型类通过箭头连接,指示它们的关系
实际上,这个可视化是使用PyGraphviz创建的。对于甚至中等复杂的项目,这可能会变得非常庞大。因此,如果应用程序被逻辑分组并分别可视化,可能会更容易。
PyGraphviz安装和使用
在Ubuntu上,您需要安装以下软件包才能安装PyGraphviz:
$sudoapt-getinstallpython3.4-devgraphvizlibgraphviz-devpkg-config现在激活您的虚拟环境并运行pip从GitHub直接安装PyGraphviz的开发版本,该版本支持Python3:
以下是一个示例用法,用于创建仅包含两个应用程序的GraphVizdot文件,并将其转换为PNG图像以进行查看:
$pythonmanage.pygraph_modelsapp1app2>models.dot$dot-Tpngmodels.dot-omodels.png渐进式更改还是完全重写?通常情况下,你会被应用所有者交付遗留代码,并怀着真诚的希望,希望大部分代码可以立即或经过一些小的调整后就可以使用。然而,阅读和理解庞大而经常过时的代码库并不是一件容易的工作。毫不奇怪,大多数程序员更愿意从事全新的开发工作。
在最好的情况下,遗留代码应该易于测试,有良好的文档记录,并且灵活适应现代环境,以便您可以立即开始进行渐进式更改。在最坏的情况下,您可能会建议放弃现有代码,进行完全重写。或者,通常决定采取的是短期方法,即继续进行渐进式更改,并且可能正在进行完全重新实现的长期并行努力。
有时,应用领域的复杂性成为重写的巨大障碍,因为在构建旧代码过程中学到的许多知识都会丢失。通常,对遗留代码的依赖表明应用设计不佳,例如未能将业务规则从应用逻辑中外部化。
您可能进行的最糟糕的重写形式可能是转换,或者是机械地将一种语言转换为另一种语言,而不利用现有的最佳实践。换句话说,您失去了通过消除多年的混乱来现代化代码库的机会。
代码应被视为一种负债而不是一种资产。尽管这听起来可能有些违反直觉,但如果您可以用更少的代码实现业务目标,您的生产力将大大提高。拥有更少的代码需要测试、调试和维护,不仅可以减少持续成本,还可以使您的组织更具敏捷性和灵活性以应对变化。
代码是一种负债而不是一种资产。更少的代码更易维护。
无论您是在添加功能还是精简代码,都不应在没有测试的情况下触碰工作中的遗留代码。
在《与遗留代码有效工作》一书中,迈克尔·费瑟斯将遗留代码定义为简单的没有测试的代码。他解释说,有了测试,可以轻松快速地修改代码的行为并进行验证。在没有测试的情况下,无法判断更改是否使代码变得更好还是更糟。
通常情况下,我们对遗留代码了解不足,无法自信地编写测试。迈克尔建议编写保留和记录现有行为的测试,这些测试称为表征测试。
与通常的编写测试的方法不同,在编写表征测试时,您将首先编写一个带有虚拟输出(例如X)的失败测试,因为您不知道预期结果。当测试工具出现错误时,例如“预期输出为X,但得到了Y”,然后您将更改测试以期望Y。现在测试将通过,并且它成为了代码现有行为的记录。
请注意,我们可能记录有错误的行为。毕竟,这是陌生的代码。然而,在开始更改代码之前,编写这些测试是必要的。稍后,当我们更了解规格和代码时,我们可以修复这些错误并更新我们的测试(不一定按照这个顺序)。
在更改代码之前编写测试类似于在修复旧建筑之前搭建脚手架。它提供了一个结构框架,帮助您自信地进行修复。
您可能希望以以下步骤逐步进行这个过程:
如果您的代码周围有一套良好的测试,那么您可以快速找到更改代码的影响。
另一方面,如果你决定通过放弃代码而不是数据来重写,那么Django可以帮助你很多。
Django文档中有一个完整的遗留数据库部分,这是正确的,因为你会经常遇到它们。数据比代码更重要,而数据库是大多数企业数据的存储库。
您可以通过将其数据库结构导入Django来现代化使用其他语言或框架编写的遗留应用程序。作为一个直接的优势,您可以使用Django管理界面来查看和更改您的遗留数据。
Django通过inspectdb管理命令使这变得容易,如下所示:
$pythonmanage.pyinspectdb>models.py如果在设置配置为使用遗留数据库的情况下运行此命令,它可以自动生成Python代码,该代码将放入您的模型文件中。
如果您正在使用这种方法来集成到遗留数据库中,以下是一些最佳实践:
在理想的世界中,您的自动生成的模型将立即开始工作,但在实践中,这需要大量的试验和错误。有时,Django推断的数据类型可能与您的期望不符。在其他情况下,您可能希望向模型添加额外的元信息,如unique_together。
最终,你应该能够在熟悉的Django管理界面中看到那个老化的PHP应用程序中锁定的所有数据。我相信这会让你微笑。
在本章中,我们讨论了理解遗留代码的各种技术。阅读代码经常是被低估的技能。但我们需要明智地重用好的工作代码,而不是重复造轮子。在本章和本书的其余部分,我们强调编写测试用例作为编码的一个组成部分的重要性。
在下一章中,我们将讨论编写测试用例和随之而来的经常令人沮丧的调试任务。
每个程序员至少都考虑过跳过编写测试。在Django中,默认的应用程序布局具有一个带有一些占位内容的tests.py模块。这是一个提醒,需要测试。然而,我们经常会有跳过它的诱惑。
然而,最终,如果您希望其他人使用您的代码,跳过测试是毫无意义的。想象一下,您发明了一种电动剃须刀,并试图向朋友出售,说它对您来说效果很好,但您没有进行适当的测试。作为您的好朋友,他或她可能会同意,但是想象一下,如果您告诉这个情况给一个陌生人,那将是多么可怕。
软件中的测试检查它是否按预期工作。没有测试,您可能能够说您的代码有效,但您将无法证明它是否正确工作。
此外,重要的是要记住,在Python中省略单元测试可能是危险的,因为它具有鸭子类型的特性。与Haskell等语言不同,类型检查无法在编译时严格执行。在Python开发中,单元测试在运行时(尽管在单独的执行中)是必不可少的。
编写测试可能是一种令人谦卑的经历。测试将指出您的错误,并且您将有机会进行早期的调整。事实上,有些人主张在编写代码之前先编写测试。
测试驱动开发(TDD)是一种软件开发形式,您首先编写测试,运行测试(最初会失败),然后编写使测试通过所需的最少代码。这可能听起来有违直觉。为什么我们需要在知道我们还没有编写任何代码并且确定它会因此失败时编写测试呢?
然而,请再次看一看。我们最终确实会编写仅满足这些测试的代码。这意味着这些测试不是普通的测试,它们更像是规范。它们告诉你可以期待什么。这些测试或规范将直接来自您的客户的用户故事。您只需编写足够的代码使其正常工作。
测试驱动开发的过程与科学方法有许多相似之处,这是现代科学的基础。在科学方法中,重要的是首先提出假设,收集数据,然后进行可重复和可验证的实验来证明或证伪你的假设。
我的建议是,一旦您熟悉为项目编写测试,就尝试TDD。初学者可能会发现很难构建一个检查代码应该如何行为的测试用例。出于同样的原因,我不建议探索性编程使用TDD。
有不同类型的测试。但是,至少程序员需要了解单元测试,因为他们必须能够编写它们。单元测试检查应用程序的最小可测试部分。集成测试检查这些部分是否与彼此良好地配合。
这里的关键词是单元。一次只测试一个单元。让我们看一个简单的测试用例的例子:
#tests.pyfromdjango.testimportTestCasefromdjango.core.urlresolversimportresolvefrom.viewsimportHomeViewclassHomePageOpenTestCase(TestCase):deftest_home_page_resolves(self):view=resolve('/')self.assertEqual(view.func.__name__,HomeView.as_view().__name__)这是一个简单的测试,检查当用户访问我们网站域的根目录时,他们是否被正确地带到主页视图。像大多数好的测试一样,它有一个长而自描述的名称。该测试简单地使用Django的resolve()函数将视图可调用匹配到/根位置的视图函数,通过它们的名称。
更重要的是要注意这个测试中没有做什么。我们没有尝试检索页面的HTML内容或检查其状态代码。我们限制自己只测试一个单元,即resolve()函数,它将URL路径映射到视图函数。
假设此测试位于项目的app1中,可以使用以下命令运行测试:
$./manage.pytestapp1Creatingtestdatabaseforalias'default'....-----------------------------------------------------------------Ran1testin0.088sOKDestroyingtestdatabaseforalias'default'...此命令将运行app1应用程序或包中的所有测试。默认的测试运行程序将在此包中的所有模块中查找与模式test*.py匹配的测试。
Django现在使用Python提供的标准unittest模块,而不是捆绑自己的模块。您可以通过从django.test.TestCase继承来编写testcase类。该类通常具有以下命名约定的方法:
测试用例是逻辑上组织测试方法的一种方式,所有这些方法都测试一个场景。当所有测试方法都通过(即不引发任何异常)时,测试用例被视为通过。如果其中任何一个失败,则测试用例失败。
每个测试方法通常调用assert*()方法来检查测试的某些预期结果。在我们的第一个示例中,我们使用assertEqual()来检查函数名称是否与预期函数匹配。
与assertEqual()类似,Python3的unittest库提供了超过32个断言方法。Django通过超过19个特定于框架的断言方法进一步扩展了它。您必须根据您期望的最终结果选择最合适的方法,以便获得最有帮助的错误消息。
让我们通过查看具有以下setUp()方法的示例testcase来看看为什么:
defsetUp(self):self.l1=[1,2]self.l2=[1,0]我们的测试是断言l1和l2是否相等(鉴于它们的值,它应该失败)。让我们看看几种等效的方法来实现这一点:
|
assertself.l1==self.l2|
assertself.l1==self.l2AssertionError|
self.assertEqual(self.l1,self.l2)|
AssertionError:Listsdiffer:[1,2]!=[1,0]Firstdifferingelement1:20|
self.assertListEqual(self.l1,self.l2)|
self.assertListEqual(self.l1,None)|
AssertionError:Secondsequenceisnotalist:None|
第一条语句使用了Python内置的assert关键字。请注意,它抛出的错误最不有帮助。您无法推断出self.l1和self.l2变量中的值或类型。这主要是我们需要使用assert*()方法的原因。
接下来,assertEqual()抛出的异常非常有帮助,它告诉您正在比较两个列表,甚至告诉您它们开始有差异的位置。这与更专门的assertListEqual()函数抛出的异常完全相同。这是因为,正如文档所告诉您的那样,如果assertEqual()给出两个列表进行比较,那么它会将其交给assertListEqual()。
尽管如最后一个示例所证明的那样,对于测试来说,始终最好使用最具体的assert*方法。由于第二个参数不是列表,错误明确告诉您期望的是列表。
在测试中使用最具体的assert*方法。
因此,您需要熟悉所有的assert方法,并选择最具体的方法来评估您期望的结果。这也适用于当您检查应用程序是否没有执行不应该执行的操作时,即负面测试用例。您可以分别使用assertRaises和assertWarns来检查异常或警告。
我们已经看到,最好的测试用例一次测试一小部分代码。它们还需要快速。程序员需要在每次提交到源代码控制之前至少运行一次测试。即使延迟几秒钟也可能会诱使程序员跳过运行测试(这不是一件好事)。
以下是一个好的测试用例的一些特点(当然,这是一个主观的术语),以易于记忆的助记符“F.I.R.S.T”形式的类测试用例:
此外,确保您的测试是自动的。消除任何手动步骤,无论多么小。自动化测试更有可能成为团队工作流程的一部分,并且更容易用于工具化目的。
也许,编写测试用例时更重要的是要记住的一些不要做的事情:
当然,您可以(也应该)在有充分理由的情况下打破规则(就像我在我的第一个例子中所做的那样)。最终,您在编写测试时越有创意,就越早发现错误,您的应用程序就会越好。
大多数现实项目的各个组件之间存在各种相互依赖关系。在测试一个组件时,其结果不能受到其他组件行为的影响。例如,您的应用程序可能调用一个可能在网络连接方面不可靠或响应速度慢的外部网络服务。
模拟对象通过具有相同接口来模拟这些依赖关系,但它们会对方法调用做出预先设定的响应。在测试中使用模拟对象后,您可以断言是否调用了某个特定方法,并验证预期的交互是否发生。
以模式:服务对象(见第三章,模型)中提到的超级英雄资格测试为例。我们将使用Python3的unittest.mock库在测试中模拟对服务对象方法的调用:
#profiles/tests.pyfromdjango.testimportTestCasefromunittest.mockimportpatchfromdjango.contrib.auth.modelsimportUserclassTestSuperHeroCheck(TestCase):deftest_checks_superhero_service_obj(self):withpatch("profiles.models.SuperHeroWebAPI")asws:ws.is_hero.return_value=Trueu=User.objects.create_user(username="t")r=u.profile.is_superhero()ws.is_hero.assert_called_with('t')self.assertTrue(r)在这里,我们在with语句中使用patch()作为上下文管理器。由于配置文件模型的is_superhero()方法将调用SuperHeroWebAPI.is_hero()类方法,我们需要在models模块内对其进行模拟。我们还将硬编码此方法的返回值为True。
最后两个断言检查方法是否使用正确的参数进行了调用,以及is_hero()是否返回了True。由于SuperHeroWebAPI类的所有方法都已被模拟,这两个断言都将通过。
模拟对象来自一个称为测试替身的家族,其中包括存根、伪造等。就像电影替身代替真正的演员一样,这些测试替身在测试时代替真实对象使用。虽然它们之间没有明确的界限,但模拟对象是可以测试行为的对象,而存根只是占位符实现。
问题:测试一个组件需要在测试之前创建各种先决对象。在每个测试方法中显式创建它们会变得重复。
解决方案:利用工厂或固件来创建测试数据对象。
在运行每个测试之前,Django会将数据库重置为其初始状态,就像运行迁移后的状态一样。大多数测试都需要创建一些初始对象来设置状态。通常情况下,不同的初始对象不会为不同的场景创建,而是通常创建一组通用的初始对象。
在大型测试套件中,这可能很快变得难以管理。这些初始对象的种类繁多,很难阅读和理解。这会导致测试数据本身中难以找到的错误!
作为一个常见的问题,有几种方法可以减少混乱并编写更清晰的测试用例。
我们将首先看一下Django文档中提供的解决方案-测试固件。在这里,测试固件是一个包含一组数据的文件,可以导入到数据库中,使其达到已知状态。通常情况下,它们是从同一数据库中导出的YAML或JSON文件,当时数据库中有一些数据。
例如,考虑以下使用测试固件的测试用例:
fromdjango.testimportTestCaseclassPostTestCase(TestCase):fixtures=['posts']defsetUp(self):#Createadditionalcommonobjectspassdeftest_some_post_functionality(self):#BynowfixturesandsetUp()objectsareloadedpass在每个测试用例中调用setUp()之前,指定的固件posts会被加载。粗略地说,固件将在固件目录中搜索具有某些已知扩展名的文件,例如app/fixtures/posts.json。
出于所有这些原因,许多人认为使用固件是一种反模式。建议您改用工厂。工厂类创建特定类的对象,可以在测试中使用。这是一种DRY的方式来创建初始测试对象。
让我们使用模型的objects.create方法来创建一个简单的工厂:
fromdjango.testimportTestCasefrom.modelsimportPostclassPostFactory:defmake_post(self):returnPost.objects.create(message="")classPostTestCase(TestCase):defsetUp(self):self.blank_message=PostFactory().makePost()deftest_some_post_functionality(self):pass与使用固件相比,初始对象的创建和测试用例都在一个地方。固件将静态数据原样加载到数据库中,而不调用模型定义的save()方法。由于工厂对象是动态生成的,它们更有可能通过应用程序的自定义验证。
将先前的代码重写为使用factory_boy,我们得到以下结果:
总之,我建议大多数需要初始测试对象的项目使用工厂,特别是factory_boy。尽管人们可能仍然希望使用固件来存储静态数据,例如国家列表或T恤尺寸,因为它们很少改变。
可怕的预测
在MadamO的坚持下,30分钟的会议在S.H.I.M.总部下面20层的隔音大厅举行。周一,团队站在一个灰色金属表面的大圆桌周围。史蒂夫笨拙地站在桌子前,用手掌做了一个僵硬的挥动手势。
尽管每个人都曾见过全息图像活跃起来,但每次看到它们都让他们惊叹不已。这个圆盘几乎分成了数百个金属方块,并像未来模型城市中的迷你摩天大楼一样升起。他们花了一秒钟才意识到他们正在看一个3D柱状图。
“我们的燃尽图似乎显示出放缓的迹象。我猜这是我们最近用户测试的结果,这是一件好事。但是……”史蒂夫的脸上似乎带着压抑打喷嚏的表情。他小心翼翼地用食指在空中轻轻一弹,图表顺利地向右延伸。
史蒂夫捂住嘴,打了一个响亮的喷嚏。全息图将这解释为放大图表中一个特别无聊的部分的迹象。史蒂夫咒骂着关掉了它。他借了一张餐巾纸,开始用普通的笔记下每个人的建议。
史蒂夫最喜欢的建议之一是编写一个编码清单,列出最常见的错误,比如忘记应用迁移。他还喜欢在开发过程中早期让用户参与并提供反馈的想法。他还记下了一些不寻常的想法,比如为连续集成服务器的状态发布推特。
会议结束时,史蒂夫注意到埃文不见了。“埃文在哪里?”他问。“不知道,”布拉德看起来很困惑地说,“刚才还在这。”
多年来,Django的默认测试运行器已经有了很大的改进。然而,像py.test和nose这样的测试运行器在功能上仍然更胜一筹。它们使你的测试更容易编写和运行。更好的是,它们与你现有的测试用例兼容。
你可能也对知道你的代码有多少百分比是由测试覆盖的感兴趣。这被称为代码覆盖,coverage.py是一个非常流行的工具,可以找出这一点。
今天的大多数项目往往使用了大量的JavaScript功能。为它们编写测试通常需要一个类似浏览器的环境来执行。Selenium是一个用于执行此类测试的出色的浏览器自动化工具。
尽管在本书的范围之外详细讨论Django中的测试,我强烈建议你了解更多关于它的知识。
如果没有别的,我想通过这一部分传达的两个主要观点是,首先,编写测试,其次,一旦你对编写测试有信心,就要练习TDD。
尽管进行了最严格的测试,悲哀的现实是,我们仍然不得不处理错误。Django尽最大努力在报告错误时提供帮助。然而,要识别问题的根本原因需要很多技巧。
幸运的是,通过正确的工具和技术,我们不仅可以识别错误,还可以深入了解代码的运行时行为。让我们来看看其中一些工具。
如果您在开发中遇到任何异常,即DEBUG=True时,那么您可能已经看到了类似以下截图的错误页面:
由于它经常出现,大多数开发人员倾向于忽略此页面中的丰富信息。以下是一些要查看的地方:
通常,您可能希望在默认的Django错误页面中获得更多的交互性。django-extensions软件包附带了出色的Werkzeug调试器,提供了这个功能。在相同异常的以下截图中,请注意在调用堆栈的每个级别上都有一个完全交互式的Python解释器:
要启用此功能,除了将django_extensions添加到您的INSTALLED_APPS中,您还需要按照以下方式运行测试服务器:
$pythonmanage.pyrunserver_plus尽管调试信息减少了,但我发现Werkzeug调试器比默认错误页面更有用。
在代码中到处添加print()函数进行调试可能听起来很原始,但对许多程序员来说,这是首选的技术。
通常,在发生异常的行之前添加print()函数。它可以用于打印导致异常的各行中变量的状态。您可以通过在达到某一行时打印某些内容来跟踪执行路径。
在开发中,打印输出通常会出现在运行测试服务器的控制台窗口中。而在生产中,这些打印输出可能会出现在服务器日志文件中,从而增加运行时开销。
无论如何,在生产中使用它都不是一个好的调试技术。即使您这样做,也应该从提交到源代码控制中的print函数中删除。
日志记录对于专业的Web开发至关重要。您的生产堆栈中的几个应用程序,如Web服务器和数据库,已经使用日志。调试可能会带您到所有这些日志,以追溯导致错误的事件。您的应用程序遵循相同的最佳实践并采用日志记录以记录错误、警告和信息消息是合适的。
与普遍看法不同,使用记录器并不涉及太多工作。当然,设置稍微复杂,但这仅仅是对整个项目的一次性努力。而且,大多数项目模板(例如edge模板)已经为您做到了这一点。
一旦您在settings.py中配置了LOGGING变量,像这样向现有代码添加记录器就非常容易:
#views.pyimportlogginglogger=logging.getLogger(__name__)defcomplicated_view():logger.debug("Enteredthecomplicated_view()!")logging模块提供了各种级别的日志消息,以便您可以轻松过滤掉不太紧急的消息。日志输出也可以以各种方式格式化,并路由到许多位置,例如标准输出或日志文件。阅读Python的logging模块文档以了解更多信息。
DjangoDebugToolbar不仅是调试的必不可少的工具,还可以跟踪每个请求和响应的详细信息。工具栏不仅在异常发生时出现,而且始终出现在呈现的页面中。
最初,它会出现在浏览器窗口右侧的可点击图形上。单击后,工具栏将作为一个深色半透明的侧边栏出现,并带有几个标题:
每个标题都包含有关页面的详细信息,从执行的SQL查询数量到用于呈现页面的模板。由于当DEBUG设置为False时,工具栏会消失,因此它基本上只能作为开发工具使用。
在调试过程中,您可能需要在Django应用程序执行中间停止以检查其状态。实现这一点的简单方法是在所需位置使用简单的assertFalse行引发异常。
如果您想要从那一行开始逐步执行,可以使用交互式调试器,例如Python的pdb。只需在想要停止执行的位置插入以下行并切换到pdb:
importpdb;pdb.set_trace()一旦输入pdb,您将在控制台窗口中看到一个命令行界面,带有(Pdb)提示。与此同时,您的浏览器窗口不会显示任何内容,因为请求尚未完成处理。
pdb命令行界面非常强大。它允许您逐行查看代码,通过打印它们来检查变量,或执行甚至可以更改运行状态的任意代码。该界面与GNU调试器GDB非常相似。
有几种可替换pdb的工具。它们通常具有更好的界面。以下是一些基于控制台的调试器:
PuDB是我首选的pdb替代品。它非常直观,即使是初学者也可以轻松使用这个界面。与pdb一样,只需插入以下代码来中断程序的执行:
importpudb;pudb.set_trace()执行此行时,将启动全屏调试器,如下所示:
按下键以获取有关可以使用的完整键列表的帮助。
此外,还有几种图形调试器,其中一些是独立的,例如winpdb,另一些是集成到IDE中的,例如PyCharm,PyDev和Komodo。我建议您尝试其中几种,直到找到适合您工作流程的调试器。
项目的模板中可能有非常复杂的逻辑。在创建模板时出现细微错误可能导致难以找到的错误。我们需要在settings.py中将TEMPLATE_DEBUG设置为True(除了DEBUG),以便Django在模板出现错误时显示更好的错误页面。
有几种粗糙的调试模板的方法,例如插入感兴趣的变量,如{{variable}},或者如果要转储所有变量,可以使用内置的debug标签,如下所示(在一个方便的可点击文本区域内):
然而,您可能希望在模板的中间暂停以检查状态(比如在循环内)。调试器对于这种情况非常完美。事实上,可以使用前面提到的任何一个Python调试器来为您的模板使用自定义模板标签。
这是一个简单的模板标签的实现。在templatetag包目录下创建以下文件:
#templatetags/debug.pyimportpudbasdbg#Changetoany*dbfromdjango.templateimportLibrary,Noderegister=Library()classPdbNode(Node):defrender(self,context):dbg.set_trace()#Debuggerwillstopherereturn''@register.tagdefpdb(parser,token):returnPdbNode()在您的模板中,加载模板标签库,将pdb标签插入到需要执行暂停的地方,并进入调试器:
{%loaddebug%}{%foriteminitems%}{#Someplaceyouwanttobreak#}{%pdb%}{%endfor%}在调试器中,您可以检查任何东西,包括使用context字典的上下文变量:
>>>print(context["item"])Item0如果您需要更多类似的模板标签用于调试和内省,我建议您查看django-template-debug包。
在本章中,我们看了Django中测试的动机和概念。我们还发现了编写测试用例时应遵循的各种最佳实践。在调试部分,我们熟悉了在Django代码和模板中查找错误的各种调试工具和技术。
在下一章中,我们将通过了解各种安全问题以及如何减少各种恶意攻击威胁,使代码更接近生产代码。
一些知名的行业报告表明,网站和Web应用程序仍然是网络攻击的主要目标之一。然而,2013年一家领先的安全公司测试的所有网站中,约86%都存在至少一个严重的漏洞。
我相信要保护您的网站免受攻击,您需要像攻击者一样思考。因此,让我们熟悉一下常见的攻击。
跨站脚本(XSS)被认为是当今最普遍的Web应用程序安全漏洞,它使攻击者能够在用户查看的网页上执行其恶意脚本(通常是JavaScript)。通常,服务器会被欺骗以在受信任的内容中提供他们的恶意内容。
恶意代码如何到达服务器?将外部数据输入网站的常见方式如下:
这些都无法完全避免。真正的问题是当外部数据在未经验证或未经过滤的情况下被使用时(如下图所示)。永远不要相信外部数据:
例如,让我们看一下一段有漏洞的代码,以及如何对其进行XSS攻击。强烈建议不要在任何形式中使用此代码:
classXSSDemoView(View):defget(self,request):#WARNING:ThiscodeisinsecureandpronetoXSSattacks#***Donotuseit!!!***if'q'inrequest.GET:returnHttpResponse("Searchedfor:{}".format(request.GET['q']))else:returnHttpResponse("""
现在在一个过时的浏览器(比如IE8)中打开这个视图,并在表单中输入以下搜索词并提交:
毫不奇怪,浏览器将显示一个带有不祥消息的警报框。请注意,这种攻击在最新的Webkit浏览器(如Chrome)中会失败,并在控制台中显示错误——拒绝执行JavaScript脚本。在请求中找到脚本的源代码。
如果你想知道一个简单的警报消息会造成什么伤害,记住任何JavaScript代码都可以以相同的方式执行。在最坏的情况下,用户的Cookie可以通过输入以下搜索词被发送到攻击者控制的站点:
值得了解的是,为什么Cookie是几种攻击的目标。简而言之,访问Cookie允许攻击者冒充您,甚至控制您的网络帐户。
会话由客户端端(即浏览器)的“会话ID”和服务器端存储的类似字典的对象组成。“会话ID”是一个随机的32个字符的字符串,存储为浏览器中的Cookie。每当用户向网站发出请求时,包括这个“会话ID”在内的所有Cookie都会随请求一起发送。
在服务器端,Django维护一个会话存储,将此会话ID映射到会话数据。默认情况下,Django将会话数据存储在django_session数据库表中。
您可能已经注意到,我的示例是Django中实现视图的一种非常不寻常的方式,原因有两个:它没有使用模板进行渲染,也没有使用表单类。它们都有防止XSS的措施。
默认情况下,Django模板会自动转义HTML特殊字符。因此,如果您在模板中显示搜索字符串,所有标记都将被HTML编码。这使得无法注入脚本,除非您通过将内容标记为安全来明确关闭它们。
在Django中使用表单来验证和清理输入也是一种非常有效的对策。例如,如果您的应用程序需要数字员工ID,则使用IntegerField类而不是更宽松的CharField类。
在我们的示例中,我们可以在搜索词字段中使用RegexValidator类,以限制用户只能使用您的搜索模块识别的字母数字字符和允许的标点符号。尽可能严格地限制用户输入的可接受范围。
Django可以通过模板中的自动转义来防止80%的XSS攻击。对于剩下的情况,您必须注意:
输入时过滤,输出时转义。
跨站点请求伪造(CSRF)是一种欺骗用户在访问其他网站时对已经经过身份验证的网站进行不需要的操作的攻击。比如,在论坛中,攻击者可以在页面中放置一个IMG或IFRAME标记,向经过身份验证的网站发送一个精心制作的请求。
对抗CSRF的基本保护措施是对具有副作用的任何操作使用HTTPPOST(或PUT和DELETE,如果支持)。任何GET(或HEAD)请求必须用于信息检索,例如只读。
Django通过嵌入令牌来提供对POST、PUT或DELETE方法的对策。您可能已经熟悉每个Django表单模板中提到的{%csrf_token%}。这是一个必须在提交表单时出现的随机值。
这种工作方式是,攻击者在向您的经过身份验证的站点发送请求时无法猜到令牌。由于令牌是强制性的,并且必须与显示表单时呈现的值匹配,因此表单提交失败,攻击被挫败。
一些人使用@csrf_exempt装饰器在视图中关闭CSRF检查,特别是对于AJAX表单提交。除非您仔细考虑了涉及的安全风险,否则不建议这样做。
SQL注入是跨站脚本(XSS)之后Web应用程序的第二大常见漏洞。攻击涉及将恶意SQL代码输入到在数据库上执行的查询中。这可能导致数据盗窃,通过转储数据库内容,或数据的破坏,比如使用DROPTABLE命令。
如果您熟悉SQL,那么您可以理解以下代码片段。它根据给定的username查找电子邮件地址:
name=request.GET['user']sql="SELECTemailFROMusersWHEREusername='{}';".format(name)乍一看,似乎只会返回与作为GET参数提到的用户名对应的电子邮件地址。但是,想象一下,如果攻击者在表单字段中输入'OR'1'='1,那么SQL代码将如下所示:
SELECTemailFROMusersWHEREusername=''OR'1'='1';由于这个WHERE子句将始终为真,您应用程序中所有用户的电子邮件都将被返回。这可能是严重的机密信息泄漏。
同样,如果攻击者愿意,他可以执行更危险的查询,如下所示:
SELECTemailFROMusersWHEREusername='';DELETEFROMusersWHERE'1'='1';现在所有用户条目都将从您的数据库中删除!
防范SQL注入的措施非常简单。使用DjangoORM而不是手工编写SQL语句。前面的示例应该这样实现:
User.objects.get(username=name).email在这里,Django的数据库驱动程序将自动转义参数。这将确保它们被视为纯粹的数据,因此它们是无害的。然而,正如我们很快将看到的那样,即使ORM也有一些逃生口。
可能会有一些情况需要使用原始SQL,比如由于DjangoORM的限制。例如,查询集的extra()方法的where子句允许原始SQL。这些SQL代码不会受到SQL注入的影响。
如果您正在使用低级数据库操作,比如execute()方法,那么您可能希望传递绑定参数,而不是自己插入SQL字符串。即使这样,强烈建议您检查每个标识符是否已经被正确转义。
最后,如果您使用的是MongoDB等第三方数据库API,则需要手动检查SQL注入。理想情况下,您希望在这些接口中只使用经过彻底清理的数据。
点击劫持是一种误导用户在浏览器中点击隐藏的链接或按钮的手段,当他们本来打算点击其他东西时。这通常是通过使用包含目标网站的不可见IFRAME在用户可能点击的虚拟网页上实现的:
由于不可见框架中的操作按钮将与虚拟页面中的按钮完全对齐,用户的点击将在目标网站上执行操作,而不是在虚拟页面上。
Django通过使用可以使用几个装饰器进行微调的中间件来保护您的网站免受点击劫持的影响。默认情况下,'django.middleware.clickjacking.XFrameOptionsMiddleware'中间件将包含在您的设置文件中的MIDDLEWARE_CLASSES中。它通过为每个传出的HttpResponse设置X-Frame-Options头为SAMEORIGIN来工作。
大多数现代浏览器都识别该标头,这意味着该页面不应该在其他域中的框架内。可以使用装饰器(如@xframe_options_deny和@xframe_options_exempt)为某些视图启用和禁用保护。
顾名思义,shell注入或命令注入允许攻击者向系统shell(如bash)注入恶意代码。即使Web应用程序也使用命令行程序来方便和实现功能。这些进程通常在shell中运行。
例如,如果要显示用户给定名称的文件的所有详细信息,一个天真的实现如下:
os.system("ls-l{}".format(filename))攻击者可以将filename输入为manage.py;rm-rf*并删除目录中的所有文件。一般来说,不建议使用os.system。subprocess模块是一个更安全的选择(或者更好的是,您可以使用os.stat()来获取文件的属性)。
由于shell会解释命令行参数和环境变量,因此在其中设置恶意值可以允许攻击者执行任意系统命令。
Django主要依赖于WSGI进行部署。由于WSGI不像CGI那样根据请求设置环境变量,因此在默认配置中,该框架本身不容易受到shell注入的影响。
这里有数百种攻击技术,我们没有涵盖到,而且随着新攻击的发现,这个列表每天都在增长。重要的是要保持对它们的了解。
你的应用程序的安全性取决于它最薄弱的环节。即使你的Django代码可能完全安全,但你的堆栈中有很多层和组件。更不用说人类,他们也可以被各种社会工程技术欺骗,比如网络钓鱼。
一个领域的漏洞,比如操作系统、数据库或Web服务器,可以被利用来访问系统的其他部分。因此,最好对您的堆栈有一个整体的视图,而不是分别查看每个部分。
安全室
他走进自己的小屋,再次打开幻灯片。在引入清单后,琐碎错误的数量急剧下降。不可能在发布版中包含的基本功能是通过与Hexa和Aksel等乐于助人的用户进行早期合作解决的。
由于Sue出色的营销活动,Beta网站的注册人数已经超过了9,000人。史蒂夫在他的职业生涯中从未见过如此多的对于一个发布的兴趣。就在那时,他注意到桌子上的报纸有些奇怪。
“我想给你看点东西,”他笑着回答道。他拿起笔记本电脑走了出去。他停在2110房间和消防出口之间。他跪下来,用右手摸索着褪色的墙纸。“这里一定有个门闩,”他喃喃自语。
然后,他的手停了下来,转动了一把从墙上微微突出的把手。墙的一部分转动并停了下来。它露出了一个用红灯光照亮的房间的入口。屋顶上悬挂着一个标志,上面写着“21B安全室”。
当他们进入时,许多屏幕和灯光自行打开。墙上的一个大屏幕上写着“需要验证。插入密钥。”埃文稍微欣赏了一下,然后开始连接他的笔记本电脑。
“还记得奥女士要我调查哨兵代码库吗?我做到了。我意识到我们得到的是经过审查的源代码。我是说我可以理解在某些地方删除一些密码,但成千上万行的代码呢?我一直在想——肯定有什么事情发生了。
“所以,通过我的访问存档,我找到了一些旧的备份。磁介质未被擦除的几率出奇地高。无论如何,我能够恢复大部分被擦除的代码。你不会相信我看到了什么。
“哨兵不是一个普通的社交网络项目。它是一个监视计划。也许是人类已知的最大的监视计划。在冷战后,一群国家加入成立了一个网络,共享情报信息。一个由人类和哨兵组成的网络。哨兵是拥有难以置信的计算能力的半自主计算机。有人认为它们是量子计算机。
“哨兵被部署在世界各地的数千个战略位置——主要是海床,那里通过了主要的光纤电缆。它们以地热能源为动力,几乎不可摧毁。它们几乎可以访问大多数国家的几乎所有互联网通信。
“也许是在九十年代的某个时候,可能是出于对公众审查的担忧,哨兵计划被关闭了。这就是真正有趣的地方。代码历史表明,哨兵的开发是由一个名叫Cerebos的人继续的。代码已经从监视能力大大增强,发展成了一种类似于大规模并行超级计算机的东西。一个数值计算的野兽,对任何加密算法都构成了重大挑战。
“还记得那次入侵吗?我觉得很难相信在超级英雄到达之前没有任何进攻性行动。所以,我做了一些研究。S.H.I.M.的网络安全设计为五个同心圆。我们,员工,处于最外层,权限最低的环,由索伦保护。内部环采用了越来越强大的加密算法。这个房间在4级。
“内鬼?”史蒂夫惊恐地问道。
“是的。然而,哨兵只需要在5级时才需要帮助。一旦它们获得了4级的公钥,它们就开始攻击4级系统。听起来很疯狂,但这就是它们的策略。”
“为什么疯狂?”
“你是说哨兵网络可以?”
“事实上,它们可以用于更小的密钥。根据我现在正在进行的测试,它们的能力已经显著增长。按照这个速度,它们应该在不到24小时内准备好进行另一次攻击。”
“该死,那时候SuperBook上线了!”
安全不是事后想到的,而是写应用程序的方式的一部分。然而,作为人类,有一个清单可以提醒你常见的遗漏是很方便的。
以下要点是在将Django应用程序公开之前应执行的最低安全检查:
请记住,这个列表绝不是详尽无遗的,也不能替代专业的安全审计。
在本章中,我们看了影响网站和Web应用程序的常见攻击类型。在许多情况下,为了清晰起见,我们对技术的解释进行了简化,但这也牺牲了细节。然而,一旦我们了解了攻击的严重性,我们就能欣赏Django提供的对策。
在我们的最后一章中,我们将更详细地查看预部署活动。我们还将研究各种部署策略,例如基于云的主机用于部署Django应用程序。
因此,您已经在Django中开发和测试了一个完全功能的Web应用程序。部署此应用程序可能涉及从选择托管提供商到执行安装等多种活动。更具挑战性的可能是保持生产站点在没有中断的情况下运行,并处理意外的流量突发情况。
系统管理的学科是广泛的。因此,本章将涵盖很多内容。然而,鉴于空间有限,我们将尝试让您熟悉构建生产环境的各个方面。
尽管我们大多数人直觉上理解生产环境是什么,但值得澄清它的真正含义。生产环境只是最终用户使用您的应用程序的地方。它应该是可用的、有弹性的、安全的、响应迅速的,并且必须具有当前(和未来)需求的充足容量。
与开发环境不同,生产环境中出现任何问题可能会导致真正的业务损失。因此,在进入生产环境之前,代码会被移动到各种测试和验收环境,以尽可能消除尽可能多的错误。为了方便追踪,对生产环境所做的每一次更改都必须进行跟踪、记录并向团队中的每个人提供访问权限。
因此,绝对不应该直接在生产环境中进行开发。事实上,生产环境中不需要安装开发工具,如编译器或调试器。任何额外软件的存在都会增加您站点的攻击面,并可能构成安全风险。
因此,选择在生产环境中运行的软件集合至关重要。
到目前为止,我们还没有讨论您的应用程序将在其上运行的堆栈。尽管我们在最后才谈论它,但最好不要将这样的决定推迟到应用程序生命周期的后期阶段。理想情况下,您的开发环境必须尽可能接近生产环境,以避免“但它在我的机器上运行”这种论点。
通过Web堆栈,我们指的是用于构建Web应用程序的一组技术。它通常被描述为一系列组件,如操作系统、数据库和Web服务器,都堆叠在一起。因此,它被称为堆栈。
生产DjangoWeb堆栈是使用多种应用程序(或层,根据您的术语)构建的。在构建Web堆栈时,您可能需要做出以下选择:
可能还有其他几种选择,这些选择也不是互斥的。有些人同时使用这些应用程序。例如,用户名的可用性可能在Redis上查找,而主数据库可能是PostgreSQL。
在选择堆栈时,没有一个“一刀切”的答案。不同的组件有不同的优势和劣势。只有经过慎重考虑和测试后才选择它们。例如,你可能听说过Nginx是一个流行的Web服务器选择,但你实际上可能需要Apache的丰富模块或选项。
有时,选择堆栈是基于各种非技术原因的。你的组织可能已经将特定的操作系统,比如Debian,作为所有服务器的标准。或者你的云托管提供商可能只支持有限的堆栈。
因此,你选择如何托管你的Django应用程序是确定你的生产设置的关键因素之一。
在托管方面,你需要确保是否选择像Heroku这样的托管平台。如果你不太了解如何管理服务器,或者团队中没有人具备这方面的知识,那么托管平台是一个方便的选择。
在大多数情况下,部署Django应用程序应该就像选择堆栈的服务或组件,然后推送你的源代码一样简单。你不需要进行任何系统管理或自己设置。平台完全由管理。
与大多数云服务一样,基础设施也可以根据需求进行扩展。如果你需要额外的数据库服务器或者服务器上的更多内存,可以很容易地从Web界面或命令行进行配置。定价主要基于你的使用情况。
这些托管平台的底线是它们非常容易设置,非常适合较小的项目。随着用户群体的增长,它们往往会变得更加昂贵。
另一个缺点是,你的应用程序可能会与某个平台绑定,或者变得难以移植。例如,GoogleAppEngine只支持非关系型数据库,这意味着你需要使用django-nonrel,这是Django的一个分支。现在,谷歌云SQL已经在一定程度上缓解了这个限制。
虚拟专用服务器(VPS)是在共享环境中托管的虚拟机。从开发者的角度来看,它看起来像是一个预装有操作系统的专用机器(因此称为私有)。你需要自己安装和设置整个堆栈,尽管许多VPS提供商,比如WebFaction和DigitalOcean,提供了更简单的Django设置。
与PaaS相比,VPS可能会更有性价比,特别是对于高流量的网站。你可能还可以从同一台服务器上运行多个站点。
尽管在平台或VPS上托管是迄今为止最流行的两种托管选项,但还有很多其他选择。如果你想最大化性能,你可以选择从提供商(比如Rackspace)那里获得裸金属服务器的托管。
在托管光谱的较轻端,您可以通过在Docker容器中托管多个应用程序来节省成本。Docker是一种将应用程序和依赖项打包到虚拟容器中的工具。与传统虚拟机相比,Docker容器启动更快,开销更小(因为没有捆绑的操作系统或hypervisor)。
Docker非常适合托管基于微服务的应用程序。它正变得像虚拟化一样普遍,几乎每个PaaS和VPS提供商都支持它们。它也是一个很好的开发平台,因为Docker容器封装了整个应用程序状态,可以直接部署到生产环境。
一旦您确定了您的托管解决方案,您的部署过程中可能会有几个步骤,从运行回归测试到生成后台服务。
成功的部署过程的关键是自动化。由于部署应用程序涉及一系列明确定义的步骤,因此可以正确地将其视为一个编程问题。一旦您有了自动化的部署,您就不必担心部署,以免错过任何步骤。
事实上,部署应该是无痛的,并且可以根据需要频繁进行。例如,Facebook团队可以每天发布代码到生产环境多达两次。考虑到Facebook庞大的用户群和代码库,这是一个令人印象深刻的壮举,然而,由于紧急错误修复和补丁需要尽快部署,这变得必要。
一个良好的部署过程也是幂等的。换句话说,即使您意外地运行了部署工具两次,操作也不应该执行两次(或者它应该保持在相同的状态)。
让我们看一些用于部署Django应用程序的流行工具。
Fabric在PythonWeb开发者中备受青睐,因为它简单易用。它期望一个名为fabfile.py的文件,定义项目中的所有操作(部署或其他)。这些操作可以是本地或远程shell命令。远程主机通过SSH连接。
Fabric的关键优势在于其能够在一组远程主机上运行命令。例如,您可以定义一个包含生产环境中所有Web服务器主机名的web组。您可以通过在命令行上指定web组名称来仅针对这些Web服务器运行Fabric操作。
为了说明使用Fabric部署站点涉及的任务,让我们看一个典型的部署场景。
假设您在单个Web服务器上部署了一个中等规模的Web应用程序。Git被选择为版本控制和协作工具。一个与所有用户共享的中央仓库已经以裸Git树的形式创建。
假设您的生产服务器已经完全设置好。当您运行Fabric部署命令,比如fabdeploy时,以下脚本化的一系列操作会发生:
整个过程是自动的,应该在几秒钟内完成。默认情况下,如果任何步骤失败,则部署将中止。尽管没有明确提到,但会有检查确保该过程是幂等的。
请注意,Fabric目前还不兼容Python3,尽管开发人员正在进行移植。与此同时,您可以在Python2.x虚拟环境中运行Fabric,或者查看类似的工具,比如PyInvoke。
使用Fabric在不同状态下管理多个服务器可能很困难。Chef、Puppet或Ansible等配置管理工具试图将服务器带到特定的期望状态。
例如,如果你想确保Nginx服务在所有的Web服务器上启动时运行,那么你需要定义一个服务器状态,其中Nginx服务既在运行又在启动时启动。另一方面,使用Fabric,你需要指定确切的步骤来安装和配置Nginx以达到这种状态。
配置管理工具最重要的优势之一是它们默认是幂等的。你的服务器可以从一个未知状态变为已知状态,从而实现更容易的服务器配置管理和可靠的部署。
在配置管理工具中,Chef和Puppet因为是这一类别中最早的工具之一,所以受到了广泛的欢迎。然而,它们在Ruby中的根源可能会让Python程序员感到有些陌生。对于这样的人来说,我们有Salt和Ansible作为很好的替代品。
与简单的工具(如Fabric)相比,配置管理工具有相当大的学习曲线。然而,它们是创建可靠的生产环境的必要工具,绝对值得学习。
即使是一个中等规模的网站也可能非常复杂。Django可能是数百个应用程序和服务之一,它们运行并相互交互。与人体的心跳和其他生命体征可以不断监测以评估人体健康的方式相同,大多数生产系统中也会收集、分析和呈现各种指标。
虽然日志记录各种事件,比如Web请求的到达或异常,监控通常是指定期收集关键信息,比如内存利用率或网络延迟。然而,在应用程序级别,差异变得模糊,比如,监控数据库查询性能,这很可能是从日志中收集的。
监控还有助于早期发现问题。异常模式,比如突然上升或逐渐增加的负载,可能是更大潜在问题的迹象,比如内存泄漏。一个良好的监控系统可以在问题发生之前提醒网站所有者。
监控工具通常需要一个后端服务(有时称为代理)来收集统计数据,以及一个前端服务来显示仪表板或生成报告。流行的数据收集后端包括StatsD和Monit。这些数据可以传递给前端工具,比如Graphite。
有一些托管的监控工具,比如NewRelic和Status.io,更容易设置和使用。
性能测量是监控的另一个重要作用。正如我们将很快看到的,任何提出的优化在实施之前都必须经过仔细的测量和监控。
令人放心的是,一些高性能的Web应用程序,如Disqus和Instagram,都是基于Django构建的。在Disqus,2013年,他们可以处理150万个并发连接用户,每秒新建45000个连接,每秒发送165000条消息,端到端延迟不到0.2秒。
改善性能的关键是找出瓶颈所在。与其依赖猜测,建议您始终测量和分析您的应用程序,以确定这些性能瓶颈。正如开尔文勋爵所说:
如果你不能测量它,你就不能改善它。
在大多数Web应用程序中,瓶颈可能在浏览器端或数据库端,而不是在Django内部。但是,对于用户来说,整个应用程序都需要响应。
让我们看一些改善Django应用程序性能的方法。由于技术差异很大,这些建议被分成了两部分:前端和后端。
Django程序员可能会快速忽视前端性能,因为它涉及了解客户端,通常是浏览器,的工作原理。然而,引用SteveSouders对Alexa排名前10的网站的研究:
前端优化的一个很好的起点是使用GooglePageSpeed或Yahoo!YSlow(通常用作浏览器插件)检查您的网站。这些工具将对您的网站进行评分,并推荐各种最佳实践,比如最小化HTTP请求的数量或对内容进行gzip压缩。
作为最佳实践,您的静态资产,如图像、样式表和JavaScript文件,不应通过Django提供。而是应该由静态文件服务器、云存储(如AmazonS3)或内容传递网络(CDN)为其提供更好的性能。
即使如此,Django可以帮助您以多种方式改善前端性能:
CachedStaticFilesStorage通过将资产的MD5哈希附加到其文件名中来优雅地解决了这个问题。这样,您可以无限扩展这些文件的缓存TTL。
要使用这个功能,将STATICFILES_STORAGE设置为CachedStaticFilesStorage,或者如果您有自定义存储,可以继承CachedFilesMixin。此外,最好配置缓存以使用本地内存缓存后端来执行静态文件名到其哈希名称的查找。
后端性能改进的范围涵盖了整个服务器端Web堆栈,包括数据库查询、模板渲染、缓存和后台作业。您将希望从中获得最高的性能,因为这完全在您的控制范围内。
对于快速和简单的分析需求,django-debug-toolbar非常方便。我们还可以使用Python分析工具,比如hotshot模块进行详细分析。在Django中,您可以使用几个分析中间件片段之一来在浏览器中显示hotshot的输出。
最近的实时分析解决方案是django-silk。它将所有请求和响应存储在配置的数据库中,允许在整个用户会话中进行聚合分析,比如查找性能最差的视图。它还可以通过添加装饰器来对任何Python代码进行分析。
与以前一样,我们将看一些改善后端性能的方法。但是,考虑到它们本身是广泛的主题,它们已被分成了几个部分。这些方法中的许多已经在前几章中进行了介绍,但在这里进行了总结以便易于参考。
如文档建议的那样,应在生产中启用缓存模板加载程序。这样可以避免每次需要呈现时重新解析和重新编译模板的开销。缓存模板在首次需要时编译,然后存储在内存中。对相同模板的后续请求将从内存中提供。
如果发现其他模板语言(如Jinja2)呈现页面的速度明显更快,则可以很容易地替换内置的Django模板语言。有几个库可以集成Django和Jinja2,如django-jinja。预计Django1.8将默认支持多个模板引擎。
有时,DjangoORM可以生成低效的SQL代码。有几种优化模式可以改善这一点:
Django有一个灵活的缓存系统,允许您从模板片段到整个站点进行缓存。它允许各种可插拔的后端,如基于文件或基于数据的后端存储。
大多数生产系统使用基于内存的缓存系统,如Redis或Memcached。这纯粹是因为易失性内存比基于磁盘的存储快得多。
这样的缓存存储非常适合存储频繁使用但短暂的数据,比如用户会话。
默认情况下,Django将用户会话存储在数据库中。通常会为每个请求检索。为了提高性能,可以通过更改SESSION_ENGINE设置将会话数据存储在内存中。例如,可以在settings.py中添加以下内容来将会话数据存储在缓存中:
SESSION_ENGINE="django.contrib.sessions.backends.cache"由于一些缓存存储可能会清除过期数据导致会话数据丢失,最好使用Redis或Memcached作为会话存储,内存限制足够支持最大数量的活动用户会话。
对于基本的缓存策略,使用缓存框架可能更容易。两个流行的框架是django-cache-machine和django-cachalot。它们可以处理常见的情况,比如自动缓存查询结果,以避免每次执行读取时都要访问数据库。
其中最简单的是Django-cachalot,它是JohnnyCache的后继者。它需要非常少的配置。它非常适合那些有多次读取和不经常写入的站点(也就是绝大多数应用程序),它以一致的方式缓存所有DjangoORM读取查询。
一旦您的站点开始受到大量访问,您将需要开始在整个堆栈中探索几种缓存策略。使用Varnish,一个位于用户和Django之间的缓存服务器,您的许多请求甚至可能根本不会到达Django服务器。
Varnish可以使页面加载速度极快(有时比正常快数百倍)。然而,如果使用不当,它可能会向用户提供静态页面。Varnish可以很容易地配置为识别动态页面或页面的动态部分,比如购物车。
不要把缓存视为站点工作的一部分。即使缓存系统崩溃,站点也必须退回到一个速度较慢但可工作的状态。
Cranos
清晨六点,S.H.I.M.大楼被一层灰蒙蒙的雾气所包围。在某个地方,一个小会议室被指定为“作战室”。在过去的三个小时里,SuperBook团队一直在这里努力执行他们的上线前计划。
当脚本输出不断从墙上滚动时,房间里变得一片寂静。史蒂夫想,只要有一个错误,他们就有可能被拖回数小时。几秒钟后,命令提示符重新出现了。它是活的!团队中爆发出了欢乐。他们从椅子上跳起来,互相高五。有些人因为幸福而流泪。经过数周的不确定和辛苦工作,一切都显得不真实。
然而,庆祝活动很快就结束了。楼上传来一声巨响,整栋建筑都震动了。史蒂夫知道第二次入侵已经开始。他对埃文喊道:“在收到我的消息之前不要打开信标”,然后冲出了房间。
当史蒂夫匆匆赶上楼梯到达屋顶时,他听到楼上脚步声。那是欧小姐。她打开门,扑了进来。他听到她尖叫着“不!”然后不久后是一声震耳欲聋的爆炸声。
当他到达屋顶时,他看到奥小姐靠在墙上坐着。她抱着左臂,面部带着疼痛的表情。史蒂夫慢慢地探头向墙后张望。远处,一个高个秃头男子似乎正在和两个机器人一起忙碌着。
“他看起来像……”史蒂夫停顿了,不确定自己。
“是的,哈特。不如说现在他是克拉诺斯了。”
“什么?”
“是的,一个分裂的人格。一个隐藏在哈特心中多年的怪物。我曾试图帮他控制它。多年前,我以为我已经阻止它再次出现。然而,所有这些压力对他造成了影响。可怜的家伙,要是我能靠近他就好了。”
可怜的家伙,他几乎试图杀了她。史蒂夫掏出手机,发送了一条消息打开信标。他必须临时应对。
他双手高举,交叉着手指,走了出去。两个机器人立刻对准了他。克拉诺斯示意它们停下。
“噢,我们这里是谁?超级书先生本人。我撞上了你的发布派对,史蒂夫?”
“这是我们的启动,哈特。”
“别叫我那个,”克拉诺斯咆哮道。“那家伙是个傻瓜。他写了哨兵代码,但他从来没有理解它的潜力。我是说,看看哨兵能做什么——解开人类已知的每个密码算法。当它进入星际网络时会发生什么?”
史蒂夫没有错过这个暗示。“超级书?”他慢慢地问道。
克拉诺斯露出了一丝邪恶的笑容。在他身后,机器人们正忙着连接到S.H.I.M.的核心网络。“当你们的超级书用户忙着玩超级城市时,哨兵的触手将扩展到新的毫无戒备的世界。每个智慧物种的关键系统都将受到破坏。超级英雄们将不得不向一个新的星际超级恶棍——克拉诺斯屈服。”
克拉诺斯正在发表这篇长篇演说时,史蒂夫注意到他眼角的动静。那是松鼠阿科恩,一只超级聪明的松鼠,在屋顶的右边沿匆匆而过。他还看到赫克萨在另一边策略性地盘旋。他向他们点了点头。
赫克萨悬浮着一个垃圾箱,朝机器人扔了过去。阿科恩用尖锐的口哨声分散了它们的注意力。“杀了他们!”克拉诺斯恼怒地说道。当他转身看向入侵者时,史蒂夫掏出手机,拨通了FaceTime,把它对准了克拉诺斯。
“向你的老朋友克拉诺斯问好,”史蒂夫说道。
克拉诺斯脸上的表情瞬间改变。那股愤怒消失了。他现在看起来像他们曾经认识的那个人。
“发生了什么?”哈特困惑地问道。
一年后
“所以,你果然是个歌手,”显而易见队长端着一杯马天尼说道。
“我想是的,”阿科恩回答道。他穿着一套金色礼服,闪闪发光。
史蒂夫带着赫克萨出现了,她穿着一条流动的银色长裙,看起来迷人极了。
“嘿,史蒂夫,赫克萨……好久不见了。超级书还让你加班到很晚吗,史蒂夫?”
“这些天没怎么发生。碰碰木头,”赫克萨微笑着回答。
“啊,你们做得太棒了。我对超级书欠了很多。我的第一支单曲《警告:含坚果》在Tucana星系大获成功。他们在超级书上观看了视频超过十亿次!”
“顺便问一下,哈特最近怎么样?”
“好多了,”史蒂夫说。“他得到了专业的帮助。哨兵被交还给了S.H.I.M。他们正在开发一种新的量子密码算法,这将更加安全。”
“所以,我猜我们在下一个超级恶棍出现之前是安全的,”显而易见船长犹豫地说道。
“嘿,至少信标起作用了,”史蒂夫说,人群爆发出笑声。
在这最后一章中,我们探讨了各种方法来使您的Django应用程序稳定、可靠和快速。换句话说,使其达到生产就绪状态。虽然系统管理可能是一个完整的学科,但对Web堆栈的基本了解是必不可少的。我们探讨了几种托管选项,包括PaaS和VPS。
我们还研究了几种自动化部署工具和典型的部署场景。最后,我们介绍了几种改进前端和后端性能的技术。
网站最重要的里程碑是完成并将其投入生产。然而,这绝不是您开发之旅的终点。将会有新的功能、修改和重写。
每次重新访问代码时,利用机会退一步,找到更清晰的设计,识别隐藏的模式,或者考虑更好的实现方式。其他开发人员,有时甚至是您未来的自己,会因此而感谢您。
这本书中的所有代码示例都是为Python3.4编写的。除了非常小的更改,它们也可以在Python2.7中运行。作者认为Python3已经成为新的Django项目的首选选择。
Python2.7的开发原计划在2015年结束,但通过2020年延长了五年。不会有Python2.8。很快,所有主要的Linux发行版都将完全转换为使用Python3作为默认版本。许多PaaS提供商,如Heroku,已经支持Python3.4。
PythonWallofSuperpowers中列出的大多数软件包已经变成了绿色(表示它们支持Python3)。几乎所有红色的软件包都有一个正在积极开发的Python3版本。
Django从1.5版本开始支持Python3。事实上,策略是用Python3重写代码,并将Python2作为向后兼容的要求。这主要是使用Six这个Python2和3兼容性库的实用函数实现的。
正如你很快会看到的,Python3在许多方面都是一种更优越的语言,因为它有许多改进,主要是为了一致性。然而,如果你正在用Django构建Web应用程序,那么在转向Python3时可能会遇到的差异是相当微不足道的。
如果你被困在Python2.7的环境中,那么示例项目可以很容易地回溯。项目根目录下有一个名为backport3to2.py的自定义脚本,可以执行一次性转换为Python2.x。请注意,它不适用于其他项目。
然而,如果你想知道为什么Python3更好,那就继续阅读。
Python3的诞生是出于必要性。Python2的一个主要问题是其对非英语字符的处理不一致(通常表现为臭名昭著的UnicodeDecodeError异常)。Guido启动了Python3项目,清理了许多这样的语言问题,同时打破了向后兼容性。
Python3.0的第一个alpha版本是在2007年8月发布的。从那时起,Python2和Python3一直在并行开发,由核心开发团队开发了多年。最终,Python3有望成为该语言的未来。
本节涵盖了从Django开发者的角度看Python3的最重要的变化。有关所有变化的完整列表,请参考本章末尾的推荐阅读部分。
示例分别以Python3和Python2给出。根据你的安装,所有Python3命令可能需要从python更改为python3或python3.4。
在Python3中,用于模型的字符串表示调用__str__()方法,而不是尴尬的__unicode__()方法。这是识别Python3移植代码最明显的方法之一:
classPerson(models.Model):name=models.TextField()def__unicode__(self):returnself.name|
classPerson(models.Model):name=models.TextField()def__str__(self):returnself.name|
前面的表反映了Python3处理字符串的方式的不同。在Python2中,类的可读表示可以通过__str__()(字节)或__unicode__()(文本)返回。然而,在Python3中,可读表示只是通过__str__()(文本)返回。
Python2有两种类:旧式(经典)和新式。新式类是直接或间接继承自object的类。只有新式类才能使用Python的高级特性,如slots、描述符和属性。其中许多被Django使用。然而,出于兼容性原因,类仍然默认为旧式。
在Python3中,旧式类不再存在。如下表所示,即使你没有明确地提到任何父类,object类也会作为基类存在。因此,所有的类都是新式的。
>>>classCoolMixin:...pass>>>CoolMixin.__bases__()|
>>>classCoolMixin:...pass>>>CoolMixin.__bases__(
在Python3中,更简单的调用super(),不带任何参数,将为你节省一些输入。
classCoolMixin(object):defdo_it(self):returnsuper(CoolMixin,self).do_it()|
classCoolMixin:defdo_it(self):returnsuper().do_it()|
指定类名和实例是可选的,从而使你的代码更加干燥,减少了重构时出错的可能性。
想象一个名为app1的包的以下目录结构:
/app1/__init__.py/models.py/tests.py现在,在Python3中,让我们在app1的父目录中运行以下代码:
$echo"importmodels">app1/tests.py$python-mapp1.testsTraceback(mostrecentcalllast):...omitted...ImportError:Nomodulenamed'models'$echo"from.importmodels">app1/tests.py$python-mapp1.tests#Successfullyimported在一个包内,当引用一个兄弟模块时,你应该使用显式相对导入。在Python3中,你可以省略__init__.py,尽管它通常用于标识一个包。
在Python2中,你可以使用importmodels成功导入models.py模块。然而,这是模棱两可的,可能会意外地导入Python路径中的任何其他models.py。因此,在Python3中是被禁止的,在Python2中也是不鼓励的。
在Python3中,根据PEP3333(WSGI标准的修正),我们要小心不要混合通过HTTP进入或离开的数据,这些数据将是字节,而不是框架内的文本,这些文本将是本地(Unicode)字符串。
基本上,对于HttpRequest和HttpResponse对象:
与Python2不同,字符串和字节在执行彼此的比较或连接时不会被隐式转换。字符串只意味着Unicode字符串。
在Python3中,异常处理的语法和功能得到了显著改进。
在Python3中,你不能使用逗号分隔的语法来处理except子句。而是使用as关键字:
try:passexcepte,BaseException:pass|
try:passexcepteasBaseException:pass|
新的语法也建议在Python2中使用。
在Python3中,所有的异常都必须派生(直接或间接)自BaseException。在实践中,你会通过从Exception类派生来创建你自己的自定义异常。
作为错误报告的一个重大改进,如果在处理异常时发生了异常,那么整个异常链都会被报告:
>>>try:...print(undefined)...exceptException:...print(oops)...Traceback(mostrecentcalllast):File"
>>>try:...print(undefined)...exceptException:...print(oops)...Traceback(mostrecentcalllast):File"
Traceback(mostrecentcalllast):File"
一旦你习惯了这个特性,你肯定会在Python2中想念它。
$python-mSimpleHTTPServerServingHTTPon0.0.0.0port8000...|
Python3不仅仅是关于语言修复。这也是Python最前沿的开发发生的地方。这意味着语言在语法、性能和内置功能方面的改进。
一些值得注意的新模块添加到Python3中如下:
即使其中一些模块已经回溯到Python2,迁移到Python3并利用它们作为内置模块更具吸引力。
大多数严肃的Python开发者更喜欢使用虚拟环境。virtualenv非常流行,可以将项目设置与系统范围的Python安装隔离开来。值得庆幸的是,Python3.3集成了类似的功能,使用venv模块。
自Python3.4开始,一个新的虚拟环境将预先安装pip,一个流行的安装程序:
$python-mvenvdjenv[djenv]$sourcedjenv/bin/activate[djenv]$pipinstalldjango请注意,命令提示符会更改以指示你的虚拟环境已被激活。
我们不可能在这个附录中涵盖所有Python3的变化和改进。然而,其他常见的变化如下: