标签 python 下的文章

这 12 个步骤能确保成功发布。

你写了一个 Python 库。自己觉着这太棒了!如果让人们能够轻松使用它不是很优雅么?这有一个需要考虑的清单,以及在开源 Python 库时要采取的具体步骤。

1、源码

将代码放在 GitHub 上,这里有很多开源项目,并且人们很容易提交拉取请求。

2、许可证

选择一个开源许可证。一般来说 MIT 许可证是一个挺好的宽容许可证。如果你有特定要求,Creative Common 的选择许可证可以指导你完成其它选择。最重要的是,在选择许可证时要记住三条规则:

  • 不要创建自己的许可证。
  • 不要创建自己的许可证。
  • 不要创建自己的许可证。

3、README

将一个名为 README.rst 的文件(使用 ReStructured Text 格式化)放在项目树的顶层。

GitHub 将像 Markdown 一样渲染 ReStructured Text,而 ReST 在 Python 的文档生态系统中的表现更好。

4、测试

写测试。这对你来说没有用处。但对于想要编写避免破坏相关功能的补丁的人来说,它非常有用。

测试可帮助协作者进行协作。

通常情况下,如果可以用 pytest 运行就最好了。还有其他测试工具 —— 但很少有理由去使用它们。

5、样式

使用 linter 制定样式:PyLint、Flake8 或者带上 --check 的 Black 。除非你使用 Black,否则请确保在一个文件中指定配置选项,并签入到版本控制系统中。

6、API 文档

使用 docstrings 来记录模块、函数、类和方法。

你可以使用几种样式。我更喜欢 Google 风格的 docstrings,但 ReST docstrings 也是一种选择。

Sphinx 可以同时处理 Google 风格和 ReST 的 docstrings,以将零散的文档集成为 API 文档。

7、零散文档

使用 Sphinx。(阅读我们这篇文章。)教程很有用,但同样重要的是要指明这是什么、它有什么好处、它有什么坏处、以及任何特殊的考虑因素。

8、构建

使用 tox 或 nox 自动运行测试和 linter,并构建文档。这些工具支持“依赖矩阵”。这些矩阵往往会快速增长,但你可以尝试针对合理的样本进行测试,例如 Python 版本、依赖项版本以及可能安装的可选依赖项。

9、打包

使用 setuptools 工具。写一个 setup.py 和一个 setup.cfg。如果同时支持 Python 2 和 3,请在 setup.cfg 中指定 universal 格式的 wheel。

tox 或 nox 应该做的一件事是构建 wheel 并对已安装的 wheel 进行测试。

避免使用 C 扩展。如果出于性能或绑定的原因一定需要它们,请将它们放在单独的包中。正确打包 C 扩展可以写一篇新的文章。这里有很多问题!

10、持续集成

使用公共持续工具。TravisCICircleCI 为开源项目提供免费套餐。将 GitHub 或其他仓库配置为在合并拉请求之前需要先通过检查,那么你就不必担心在代码评审中告知用户修复测试或样式。

11、版本

使用 SemVerCalVer。有许多工具可以帮助你管理版本:incrementalbumpversionsetuptools\_scm 等都是 PyPI 上的包,都可以帮助你管理版本。

12、发布

通过运行 tox 或 nox 并使用 twine 将文件上传到 PyPI 上发布。你可以通过在 DevPI 中“测试上传”。


via: https://opensource.com/article/18/12/tips-open-sourcing-python-libraries

作者:Moshe Zadka 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用此框架编写断言,提高开发测试的准确性。

测试金字塔的底部是单元测试。单元测试每次只测试一个代码单元,通常是一个函数或方法。

通常,设计单个单元测试是为了测试通过一个函数或特定分支的特定执行流程,这使得将失败的单元测试和导致失败的 bug 对应起来变得容易。

理想情况下,单元测试很少使用或不使用外部资源,从而隔离它们并使它们更快。

单元测试套件通过在开发过程的早期发现问题来帮助维护高质量的产品。有效的单元测试可以在代码离开开发人员机器之前捕获 bug,或者至少可以在特定分支上的持续集成环境中捕获 bug。这标志着好的和坏的单元测试之间的区别:好的测试通过尽早捕获 bug 并使测试更快来提高开发人员的生产力。坏的测试降低了开发人员的工作效率。

当测试附带的特性时,生产率通常会降低。当代码更改时测试会失败,即使它仍然是正确的。发生这种情况是因为输出的不同,但在某种程度上是因为它不是 函数契约 function’s contract 的一部分。

因此,一个好的单元测试可以帮助执行函数所提交的契约。

如果单元测试中断,那意味着该契约被违反了,应该(通过更改文档和测试)明确修改,或者(通过修复代码并保持测试不变)来修复。

虽然将测试限制为只执行公共契约是一项需要学习的复杂技能,但有一些工具可以提供帮助。

其中一个工具是 Hamcrest,这是一个用于编写断言的框架。最初是为基于 Java 的单元测试而发明的,但它现在支持多种语言,包括 Python

Hamcrest 旨在使测试断言更容易编写和更精确。

def add(a, b):
    return a + b

from hamcrest import assert_that, equal_to

def test_add():
    assert_that(add(2, 2), equal_to(4))  

这是一个用于简单函数的断言。如果我们想要断言更复杂的函数怎么办?

def test_set_removal():
    my_set = {1, 2, 3, 4}
    my_set.remove(3)
    assert_that(my_set, contains_inanyorder([1, 2, 4]))
    assert_that(my_set, is_not(has_item(3)))

注意,我们可以简单地断言其结果是任何顺序的 124,因为集合不保证顺序。

我们也可以很容易用 is_not 来否定断言。这有助于我们编写精确的断言,使我们能够把自己限制在执行函数的公共契约方面。

然而,有时候,内置的功能都不是我们真正需要的。在这些情况下,Hamcrest 允许我们编写自己的 匹配器 matchers

想象一下以下功能:

def scale_one(a, b):
    scale = random.randint(0, 5)
    pick = random.choice([a,b])
    return scale * pick

我们可以自信地断言其结果均匀地分配到至少一个输入。

匹配器继承自 hamcrest.core.base_matcher.BaseMatcher,重写两个方法:

class DivisibleBy(hamcrest.core.base_matcher.BaseMatcher):
    def __init__(self, factor):
        self.factor = factor

    def _matches(self, item):
        return (item % self.factor) == 0

    def describe_to(self, description):
        description.append_text('number divisible by')
        description.append_text(repr(self.factor))

编写高质量的 describe_to 方法很重要,因为这是测试失败时显示的消息的一部分。

def divisible_by(num):
    return DivisibleBy(num)

按照惯例,我们将匹配器包装在一个函数中。有时这给了我们进一步处理输入的机会,但在这种情况下,我们不需要进一步处理。

def test_scale():
    result = scale_one(3, 7)
    assert_that(result,
                any_of(divisible_by(3),
                divisible_by(7)))

请注意,我们将 divisible_by 匹配器与内置的 any_of 匹配器结合起来,以确保我们只测试函数提交的内容。

在编辑这篇文章时,我听到一个传言,取 “Hamcrest” 这个名字是因为它是 “matches” 字母组成的字谜。嗯…

>>> assert_that("matches", contains_inanyorder(*"hamcrest")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/moshez/src/devops-python/build/devops/lib/python3.6/site-packages/hamcrest/core/assert_that.py", line 43, in assert_that
    _assert_match(actual=arg1, matcher=arg2, reason=arg3)
  File "/home/moshez/src/devops-python/build/devops/lib/python3.6/site-packages/hamcrest/core/assert_that.py", line 57, in _assert_match
    raise AssertionError(description)
AssertionError:
Expected: a sequence over ['h', 'a', 'm', 'c', 'r', 'e', 's', 't'] in any order
      but: no item matches: 'r' in ['m', 'a', 't', 'c', 'h', 'e', 's']

经过进一步的研究,我找到了传言的来源:它是 “matchers” 字母组成的字谜。

>>> assert_that("matchers", contains_inanyorder(*"hamcrest"))
>>>

如果你还没有为你的 Python 代码编写单元测试,那么现在是开始的好时机。如果你正在为你的 Python 代码编写单元测试,那么使用 Hamcrest 将允许你使你的断言更加精确,既不会比你想要测试的多也不会少。这将在修改代码时减少误报,并减少修改工作代码的测试所花费的时间。


via: https://opensource.com/article/18/8/robust-unit-tests-hamcrest

作者:Moshe Zadka 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Pelican 是那些想要自我托管简单网站或博客的 Python 用户的绝佳选择。

如果你想创建一个自定义网站或博客,有很多选择。许多提供商可以托管你的网站并为你完成大部分工作。(WordPress 是一个非常受欢迎的选项。)但是使用托管方式,你会失去一些灵活性。作为一名软件开发人员,我更喜欢管理我自己的服务器,并在我的网站如何运行方面保持更多的自由。

然而,管理 Web 服务器需要大量的工作。安装它并获得一个简单的应用程序来提供内容是非常容易的。但是,维护安全补丁和更新是非常耗时得。如果你只想提供静态网页,那么拥有一个 Web 服务器和一系列应用程序可能会得不偿失。手动创建 HTML 页面也不是一个好选择。

这是静态网站生成器的用武之地。这些应用程序使用模板来创建所需的静态页面,并将它们与关联的元数据交叉链接。(例如,所有显示的页面都带有公共标签或关键词。)静态网站生成器可以帮助你使用导航区域、页眉和页脚等元素创建一个具有公共外观的网站。

我使用 Pyhton 已经很多年了,所以,当我第一次开始寻找生成静态 HTML 页面的东西时,我想要用 Python 编写的东西。主要原因是我经常想要了解应用程序如何工作的内部细节,而使用一种我已经了解的语言使这一点更容易。(如果这对你不重要或者你不使用 Python,那么还有一些其他很棒的静态网站生成器,它们使用 Ruby、JavaScript 和其它语言。)

我决定试试 Pelican。它是一个用 Python 编写的常用静态网站生成器。它支持 reStructuredText(LCTT 译注:这是一种用于文本数据的文件格式,主要用于 Python 社区的技术文档),并且也支持 Markdown,这需要通过安装必需的包来完成。所有任务都是通过命令行界面(CLI)工具执行的,这使得熟悉命令行的任何人都可以轻松完成。它简单的 quickstart CLI 工具使得创建一个网站非常容易。

在本文中,我将介绍如何安装 Pelican 4,添加一篇文章以及更改默认主题。(注意:我是在 MacOS 上开发的,使用其它 Unix/Linux 实验结果都将相同,但我没有 Windows 主机可以测试。)

安装和配置

第一步是创建一个虚拟环境,在虚拟环境中安装 Pelican。

$ mkdir test-site
$ cd test-site
$ python3 -m venv venv
$ ./venv/bin/pip install --upgrade pip
...
Successfully installed pip-18.1
$ ./venv/bin/pip install pelican
Collecting pelican
...
Successfully installed MarkupSafe-1.1.0 blinker-1.4 docutils-0.14 feedgenerator-1.9 jinja2-2.10 pelican-4.0.1 pygments-2.3.1 python-dateutil-2.7.5 pytz-2018.7 six-1.12.0 unidecode-1.0.23

Pelican 的 quickstart CLI 工具将创建基本布局和一些文件来帮助你开始,运行 pelican-quickstart 命令。为了简单起见,我输入了网站标题作者的名字,并对 URL 前缀和文章分页选择了 “N”。(对于其它选项,我使用了默认值。)稍后在配置文件中更改这些设置非常容易。

$ ./venv/bin/pelicanquickstart
Welcome to pelicanquickstart v4.0.1.

This script will help you create a new Pelican-based website.

Please answer the following questions so this script can generate the files needed by Pelican.

> Where do you want to create your new web site? [.]
> What will be the title of this web site? My Test Blog
> Who will be the author of this web site? Craig
> What will be the default language of this web site? [en]
> Do you want to specify a URL prefix? e.g., https://example.com (Y/n) n
> Do you want to enable article pagination? (Y/n) n
> What is your time zone? [Europe/Paris]
> Do you want to generate a tasks.py/Makefile to automate generation and publishing? (Y/n)
> Do you want to upload your website using FTP? (y/N)
> Do you want to upload your website using SSH? (y/N)
> Do you want to upload your website using Dropbox? (y/N)
> Do you want to upload your website using S3? (y/N)
> Do you want to upload your website using Rackspace Cloud Files? (y/N)
> Do you want to upload your website using GitHub Pages? (y/N)
Done. Your new project is available at /Users/craig/tmp/pelican/test-site

你需要启动的所有文件都准备好了。

quickstart 默认为欧洲/巴黎时区,所以在继续之前更改一下。在你喜欢的文本编辑器中打开 pelicanconf.py 文件,寻找 TIMEZONE 变量。

TIMEZONE = 'Europe/Paris'

将其改为 UTC

TIMEZONE = 'UTC'

要更新公共设置,在 pelicanconf.py 中查找 SOCIAL 变量。

SOCIAL = (('You can add links in your config file', '#'),
          ('Another social link', '#'),)

我将添加一个我的 Twitter 账户链接。

SOCIAL = (('Twitter (#craigs55)', 'https://twitter.com/craigs55'),)

注意末尾的逗号,它很重要。这个逗号将帮助 Python 识别变量实际上是一个集合。确保你没有删除这个逗号。

现在你已经有了网站的基本知识。quickstart 创建了一个包含许多目标的 Makefile。将 devserver 传给 make 命令将在你的计算机上启动一个开发服务器,以便你可以预览所有内容。Makefile 中使用的 CLI 命令假定放在 PATH 搜索路径中,因此你需要首先激活该虚拟环境。

$ source ./venv/bin/activate
$ make devserver
pelican -lr /Users/craig/tmp/pelican/test-site/content o
/Users/craig/tmp/pelican/test-site/output -s /Users/craig/tmp/pelican/test-site/pelicanconf.py

-> Modified: theme, settings. regenerating...
WARNING: No valid files found in content for the active readers:
   | BaseReader (static)
   | HTMLReader (htm, html)
   | RstReader (rst)
Done: Processed 0 articles, 0 drafts, 0 pages, 0 hidden pages and 0 draft pages in 0.18 seconds.

在你最喜欢的浏览器中打开 http://localhost:8000 来查看你的简单测试博客。

你可以在右侧看到 Twitter 链接,左侧有 Pelican、Python 和 Jinja 的一些链接。(Jinja 是 Pelican 可以使用的一种很棒的模板语言。你可以在 Jinja 的文档中了解更多相关信息。)

添加内容

现在你又了一个基本的网站,试着添加一些内容。首先,将名为 welcome.rst 的文件添加到网站的 content 目录中。在你喜欢的文本编辑器中,使用以下文本创建一个文件:

$ pwd
/Users/craig/tmp/pelican/test-site
$ cat content/welcome.rst

Welcome to my blog!
###################

:date: 20181216 08:30
:tags: welcome
:category: Intro
:slug: welcome
:author: Craig
:summary: Welcome document

Welcome to my blog.
This is a short page just to show how to put up a static page.

Pelican 会自动解析元数据行,包括日期、标签等。

编写完文件后,开发服务器应该输出以下内容:

-> Modified: content. regenerating...
Done: Processed 1 article, 0 drafts, 0 pages, 0 hidden pages and 0 draft pages in 0.10 seconds.

在浏览器中刷新你的测试网站来查看更改。

元数据(例如日期和标签)会自动添加到页面中。此外,Pelican 会自动检测到 intro 栏目,并将该部分添加到顶部导航中。

更改主题

使用像 Pelican 这样流行的开源软件的好处之一是,非常多的用户会做出更改并将其贡献给项目。许多都是以主题形式贡献的。

网站的主题会设置颜色、布局选项等。尝试一个新主题非常容易,你可以在 Pelican 主题中预览其中的许多内容。

首先,克隆 GitHub 仓库:

$ cd ..
$ git clone --recursive https://github.com/getpelican/pelicanthemes
Cloning into 'pelicanthemes'...

我喜欢蓝色,那么试试 blueidea

编辑 pelicanconf.py,添加以下行:

THEME = '/Users/craig/tmp/pelican/pelican-themes/blueidea/'

开发服务器将重新生成你的输出。在浏览器中刷新网页来查看新主题。

主题控制布局的方方面面。例如,在默认主题中,你可以看到文章旁边带有元标记的栏目(Intro),但这个栏目并未显示在 blueidea 主题中。

其他考虑因素

本文是对 Pelican 的快速介绍,所以我并没有涉及一些重要的主题。

首先,我对迁移到静态站点犹豫不决的一个原因是它无法对文章评论。幸运的是,有一些第三方服务商将为你提供评论功能。我目前正在关注的是 Disqus

接下来,上面的所有内容都是在我的本地机器上完成的。如果我希望其他人查看我的网站,我将不得不将预先生成的 HTML 文件上传到某个地方。如果你查看 pelican-quickstart 输出,你将看到使用 FTP、 SSH、S3 甚至 GitHub 页面的选项,每个选项都有其优点和缺点。但是,如果我必须选择一个,那么我可能会选择发布到 GitHub 页面。

Pelican 还有许多其他功能,我每天都在学习它。如果你想自托管一个网站或博客,内容简单并且是静态内容,同时你想使用 Python,那么 Pelican 是一个很好的选择。它有一个活跃的用户社区,可以修复 bug,添加特性,而且还会创建新的和有趣的主题。试试看吧!


via: https://opensource.com/article/19/1/getting-started-pelican

作者:Craig Sebenik 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

R 还是 Python ? Python 脚本模仿易使用的 R 风格函数,使得数据统计变得简单易行。

“Python vs. R” 是数据科学和机器学习的现代战争之一。毫无疑问,近年来这两者发展迅猛,成为数据科学、预测分析和机器学习领域的顶级编程语言。事实上,根据 IEEE 最近的一篇文章,Python 已在 最受欢迎编程语言排行榜 中超越 C++ 成为排名第一的语言,并且 R 语言也稳居前 10 位。

但是,这两者之间存在一些根本区别。R 语言设计的初衷主要是作为统计分析和数据分析问题的快速原型设计的工具,另一方面,Python 是作为一种通用的、现代的面向对象语言而开发的,类似 C++ 或 Java,但具有更简单的学习曲线和更灵活的语言风格。因此,R 仍在统计学家、定量生物学家、物理学家和经济学家中备受青睐,而 Python 已逐渐成为日常脚本、自动化、后端 Web 开发、分析和通用机器学习框架的顶级语言,拥有广泛的支持基础和开源开发社区。

在 Python 环境中模仿函数式编程

R 作为函数式编程语言的本质为用户提供了一个极其简洁的用于快速计算概率的接口,还为数据分析问题提供了必不可少的描述统计和推论统计方法(LCTT 译注:统计学从功能上分为描述统计学和推论统计学)。例如,只用一个简洁的函数调用来解决以下问题难道不是很好吗?

  • 如何计算数据向量的平均数 / 中位数 / 众数。
  • 如何计算某些服从正态分布的事件的累积概率。如果服 从泊松分布 Poisson distribution 又该怎样计算呢?
  • 如何计算一系列数据点的四分位距。
  • 如何生成服从学生 t 分布的一些随机数(LCTT 译注: 在概率论和统计学中,学生 t-分布(Student’s t-distribution)可简称为 t 分布,用于根据小样本来估计呈正态分布且方差未知的总体的均值)。

R 编程环境可以完成所有这些工作。

另一方面,Python 的脚本编写能力使分析师能够在各种分析流程中使用这些统计数据,具有无限的复杂性和创造力。

要结合二者的优势,你只需要一个简单的 Python 封装的库,其中包含与 R 风格定义的概率分布和描述性统计相关的最常用函数。 这使你可以非常快速地调用这些函数,而无需转到正确的 Python 统计库并理解整个方法和参数列表。

便于调用 R 函数的 Python 包装脚本

我编写了一个 Python 脚本 ,用 Python 简单统计分析定义了最简洁和最常用的 R 函数。导入此脚本后,你将能够原生地使用这些 R 函数,就像在 R 编程环境中一样。

此脚本的目标是提供简单的 Python 函数,模仿 R 风格的统计函数,以快速计算密度估计和点估计、累积分布和分位数,并生成重要概率分布的随机变量。

为了延续 R 风格,脚本不使用类结构,并且只在文件中定义原始函数。因此,用户可以导入这个 Python 脚本,并在需要单个名称调用时使用所有功能。

请注意,我使用 mimic 这个词。 在任何情况下,我都声称要模仿 R 的真正的函数式编程范式,该范式包括深层环境设置以及这些环境和对象之间的复杂关系。 这个脚本允许我(我希望无数其他的 Python 用户)快速启动 Python 程序或 Jupyter 笔记本程序、导入脚本,并立即开始进行简单的描述性统计。这就是目标,仅此而已。

如果你已经写过 R 代码(可能在研究生院)并且刚刚开始学习并使用 Python 进行数据分析,那么你将很高兴看到并在 Jupyter 笔记本中以类似在 R 环境中一样使用一些相同的知名函数。

无论出于何种原因,使用这个脚本很有趣。

简单的例子

首先,只需导入脚本并开始处理数字列表,就好像它们是 R 中的数据向量一样。

from R_functions import *
lst=[20,12,16,32,27,65,44,45,22,18]
<more code, more statistics...>

假设你想从数据向量计算 Tuckey 五数摘要。 你只需要调用一个简单的函数 fivenum,然后将向量传进去。 它将返回五数摘要,存在 NumPy 数组中。

lst=[20,12,16,32,27,65,44,45,22,18]
fivenum(lst)
> array([12. , 18.5, 24.5, 41. , 65. ])

或许你想要知道下面问题的答案:

假设一台机器平均每小时输出 10 件成品,标准偏差为 2。输出模式遵循接近正态的分布。 机器在下一个小时内输出至少 7 个但不超过 12 个单位的概率是多少?

答案基本上是这样的:

使用 pnorm ,你可以只用一行代码就能获得答案:

pnorm(12,10,2)-pnorm(7,10,2)
> 0.7745375447996848

或者你可能需要回答以下问题:

假设你有一个不公平硬币,每次投它时有 60% 可能正面朝上。 你正在玩 10 次投掷游戏。 你如何绘制并给出这枚硬币所有可能的胜利数(从 0 到 10)的概率?

只需使用一个函数 dbinom 就可以获得一个只有几行代码的美观条形图:

probs=[]
import matplotlib.pyplot as plt
for i in range(11):
    probs.append(dbinom(i,10,0.6))
plt.bar(range(11),height=probs)
plt.grid(True)
plt.show()

简单的概率计算接口

R 提供了一个非常简单直观的接口,可以从基本概率分布中快速计算。 接口如下:

  • d 分布:给出点 x 处的密度函数值
  • p 分布:给出 x 点的累积值
  • q 分布:以概率 p 给出分位数函数值
  • r 分布:生成一个或多个随机变量

在我们的实现中,我们坚持使用此接口及其关联的参数列表,以便你可以像在 R 环境中一样执行这些函数。

目前已实现的函数

脚本中实现了以下 R 风格函数,以便快速调用。

  • 平均数、中位数、方差、标准差
  • Tuckey 五数摘要、 四分位距 interquartile range (IQR)
  • 矩阵的协方差或两个向量之间的协方差
  • 以下分布的密度、累积概率、分位数函数和随机变量生成:正态、均匀、二项式、 泊松 Poisson 、F、 学生 t Student’s t 卡方 Chi-square 贝塔 beta 伽玛 gamma

进行中的工作

显然,这是一项正在进行的工作,我计划在此脚本中添加一些其他方便的R函数。 例如,在 R 中,单行命令 lm 可以为数字数据集提供一个简单的最小二乘拟合模型,其中包含所有必要的推理统计(P 值,标准误差等)。 这非常简洁! 另一方面,Python 中的标准线性回归问题经常使用 Scikit-learn 库来处理,此用途需要更多的脚本,所以我打算使用 Python 的 statsmodels 库合并这个单函数线性模型来拟合功能。

如果你喜欢这个脚本,并且愿意在工作中使用,请 GitHub 仓库点个 star 或者 fork 帮助其他人找到它。 另外,你可以查看我其他的 GitHub 仓库,了解 Python、R 或 MATLAB 中的有趣代码片段以及一些机器学习资源。

如果你有任何问题或想法要分享,请通过 [tirthajyoti [AT] gmail.com](mailto:[email protected]) 与我联系。 如果你像我一样热衷于机器学习和数据科学,请 在 LinkedIn 上加我为好友或者在 Twitter 上关注我

本篇文章最初发表于走向数据科学。 请在 CC BY-SA 4.0 协议下转载。


via: https://opensource.com/article/18/10/write-favorite-r-functions-python

作者:Tirthajyoti Sarkar 选题:lujun9972 译者:yongshouzhang 校对:Flowsnow

本文由 LCTT 原创编译,Linux中国 荣誉推出

在这个比较 Python 框架的最后一篇中,让我们看看 Django。

在本系列(由四部分组成)的前三篇文章中,我们讨论了 PyramidFlaskTornado 这 3 个 Web 框架。我们已经构建了三次相同的应用程序,最终我们遇到了 Django。总的来说,Django 是目前 Python 开发人员使用的主要 Web 框架,并且原因显而易见。它擅长隐藏大量的配置逻辑,让你专注于能够快速构建大型应用程序。

也就是说,当涉及到小型项目时,比如我们的待办事项列表应用程序,Django 可能有点像用消防水管来进行水枪大战。让我们来看看它们是如何结合在一起的。

关于 Django

Django 将自己定位为“一个鼓励快速开发和整洁、实用的设计的高级 Python Web 框架。它由经验丰富的开发人员构建,解决了 Web 开发的很多麻烦,因此你可以专注于编写应用程序而无需重新发明轮子”。而且它确实做到了!这个庞大的 Web 框架附带了非常多的工具,以至于在开发过程中,如何将所有内容组合在一起协同工作可能是个谜。

除了框架本身很大,Django 社区也是非常庞大的。事实上,它非常庞大和活跃,以至于有一个网站专门用于为人们收集第三方包,这些第三方包可集成进 Django 来做一大堆事情。包括从身份验证和授权到完全基于 Django 的内容管理系统,电子商务附加组件以及与 Stripe(LCTT 译注:美版“支付宝”)集成的所有内容。至于不要重新发明轮子:如果你想用 Django 完成一些事情,有人可能已经做过了,你只需将它集成进你的项目就行。

为此,我们希望使用 Django 构建 REST API,因此我们将使用流行的 Django REST 框架。它的工作是将 Django 框架(Django 使用自己的模板引擎构建 HTML 页面)转换为专门用于有效地处理 REST 交互的系统。让我们开始吧。

Django 启动和配置

$ mkdir django_todo
$ cd django_todo
$ pipenv install --python 3.6
$ pipenv shell
(django-someHash) $ pipenv install django djangorestframework

作为参考,我们使用的是 django-2.0.7djangorestframework-3.8.2

与 Flask, Tornado 和 Pyramid 不同,我们不需要自己编写 setup.py 文件,我们并不是在做一个可安装的 Python 发布版。像很多事情一样,Django 以自己的方式处理这个问题。我们仍然需要一个 requirements.txt 文件来跟踪我们在其它地方部署的所有必要安装。但是,就 Django 项目中的目标模块而言,Django 会让我们列出我们想要访问的子目录,然后允许我们从这些目录中导入,就像它们是已安装的包一样。

首先,我们必须创建一个 Django 项目。

当我们安装了 Django 后,我们还安装了命令行脚本 django-admin。它的工作是管理所有与 Django 相关的命令,这些命令有助于我们将项目整合在一起,并在我们继续开发的过程中对其进行维护。django-admin 并不是让我们从头开始构建整个 Django 生态系统,而是让我们从标准 Django 项目所需的所有必要文件(以及更多)的基础上开始。

调用 django-adminstart-project 命令的语法是 django-admin startproject <项目名称> <存放目录>。我们希望文件存于当前的工作目录中,所以:

(django-someHash) $ django-admin startproject django_todo .

输入 ls 将显示一个新文件和一个新目录。

(django-someHash) $ ls
manage.py   django_todo

manage.py 是一个可执行命令行 Python 文件,它最终成为 django-admin 的封装。因此,它的工作与 django-admin 是一样的:帮助我们管理项目。因此得名 manage.py

它在 django_todo 目录里创建了一个新目录 django_todo,其代表了我们项目的配置根目录。现在让我们深入研究一下。

配置 Django

可以将 django_todo 目录称为“配置根目录”,我们的意思是这个目录包含了通常配置 Django 项目所需的文件。几乎所有这个目录之外的内容都只关注与项目模型、视图、路由等相关的“业务逻辑”。所有连接项目的点都将在这里出现。

django_todo 目录中调用 ls 会显示以下四个文件:

(django-someHash) $ cd django_todo
(django-someHash) $ ls
__init__.py settings.py urls.py     wsgi.py
  • __init__.py 文件为空,之所以存在是为了将此目录转换为可导入的 Python 包。
  • settings.py 是设置大多数配置项的地方。例如项目是否处于 DEBUG 模式,正在使用哪些数据库,Django 应该定位文件的位置等等。它是配置根目录的“主要配置”部分,我们将在一会深入研究。
  • urls.py 顾名思义就是设置 URL 的地方。虽然我们不必在此文件中显式写入项目的每个 URL,但我们需要让此文件知道在其他任何地方已声明的 URL。如果此文件未指向其它 URL,则那些 URL 就不存在。
  • wsgi.py 用于在生产环境中提供应用程序。就像 Pyramid、 Tornado 和 Flask 暴露了一些 “app” 对象一样,它们用来提供配置好的应用程序,Django 也必须暴露一个,就是在这里完成的。它可以和 GunicornWaitress 或者 uWSGI 一起配合来提供服务。

设置 settings

看一看 settings.py,它里面有大量的配置项,那些只是默认值!这甚至不包括数据库、静态文件、媒体文件、任何集成的钩子,或者可以配置 Django 项目的任何其它几种方式。让我们从上到下看看有什么:

  • BASE_DIR 设置目录的绝对路径,或者是 manage.py 所在的目录。这对于定位文件非常有用。
  • SECRET_KEY 是用于 Django 项目中加密签名的密钥。在实际中,它用于会话、cookie、CSRF 保护和身份验证令牌等。最好在第一次提交之前,尽快应该更改 SECRET_KEY 的值并将其放置到环境变量中。
  • DEBUG 告诉 Django 是以开发模式还是生产模式运行项目。这是一个非常关键的区别。

    • 在开发模式下,当弹出一个错误时,Django 将显示导致错误的完整堆栈跟踪,以及运行项目所涉及的所有设置和配置。如果在生产环境中将 DEBUG 设置为 True,这可能成为一个巨大的安全问题。
    • 在生产模式下,当出现问题时,Django 会显示一个简单的错误页面,即除错误代码外不提供任何信息。
    • 保护我们项目的一个简单方法是将 DEBUG 设置为环境变量,如 bool(os.environ.get('DEBUG', ''))
  • ALLOWED_HOSTS 是应用程序提供服务的主机名的列表。在开发模式中,这可能是空的;但是在生产环境中,如果为项目提供服务的主机不在 ALLOWED_HOSTS 列表中,Django 项目将无法运行。这是设置为环境变量的另一种情况。
  • INSTALLED_APPS 是我们的 Django 项目可以访问的 Django “apps” 列表(将它们视为子目录,稍后会详细介绍)。默认情况下,它将提供:

    • 内置的 Django 管理网站
    • Django 的内置认证系统
    • Django 的数据模型通用管理器
    • 会话管理
    • Cookie 和基于会话的消息传递
    • 站点固有的静态文件的用法,比如 css 文件、js 文件、任何属于我们网站设计的图片等。
  • MIDDLEWARE 顾名思义:帮助 Django 项目运行的中间件。其中很大一部分用于处理各种类型的安全,尽管我们可以根据需要添加其它中间件。
  • ROOT_URLCONF 设置基本 URL 配置文件的导入路径。还记得我们之前见过的那个 urls.py 吗?默认情况下,Django 指向该文件以此来收集所有的 URL。如果我们想让 Django 在其它地方寻找,我们将在这里设置 URL 位置的导入路径。
  • TEMPLATES 是 Django 用于我们网站前端的模板引擎列表,假如我们依靠 Django 来构建我们的 HTML。我们在这里不需要,那就无关紧要了。
  • WSGI_APPLICATION 设置我们的 WSGI 应用程序的导入路径 —— 在生产环境下使用的东西。默认情况下,它指向 wsgi.py 中的 application 对象。这很少(如果有的话)需要修改。
  • DATABASES 设置 Django 项目将访问那些数据库。必须设置 default 数据库。我们可以通过名称设置别的数据库,只要我们提供 HOSTUSERPASSWORDPORT、数据库名称 NAME 和合适的 ENGINE。可以想象,这些都是敏感的信息,因此最好将它们隐藏在环境变量中。查看 Django 文档了解更多详情。

    • 注意:如果不是提供数据库的每个单个部分,而是提供完整的数据库 URL,请查看 djdatabaseurl
  • AUTH_PASSWORD_VALIDATORS 实际上是运行以检查输入密码的函数列表。默认情况下我们有一些,但是如果我们有其它更复杂的验证需求:不仅仅是检查密码是否与用户的属性匹配,是否超过最小长度,是否是 1000 个最常用的密码之一,或者密码完全是数字,我们可以在这里列出它们。
  • LANGUAGE_CODE 设置网站的语言。默认情况下它是美国英语,但我们可以将其切换为其它语言。
  • TIME_ZONE 是我们 Django 项目后中自动生成的时间戳的时区。我强调坚持使用 UTC 并在其它地方执行任何特定于时区的处理,而不是尝试重新配置此设置。正如这篇文章 所述,UTC 是所有时区的共同点,因为不需要担心偏移。如果偏移很重要,我们可以根据需要使用与 UTC 的适当偏移来计算它们。
  • USE_I18N 将让 Django 使用自己的翻译服务来为前端翻译字符串。I18N = 国际化(internationalization,“i” 和 “n” 之间共 18 个字符)。
  • USE_L10N L10N = 本地化(localization,在 l 和 n 之间共 10 个字符) 。如果设置为 True,那么将使用数据的公共本地格式。一个很好的例子是日期:在美国它是 MM-DD-YYYY。在欧洲,日期往往写成 DD-MM-YYYY。
  • STATIC_URL 是用于提供静态文件的主体部分。我们将构建一个 REST API,因此我们不需要考虑静态文件。通常,这会为每个静态文件的域名设置根路径。所以,如果我们有一个 Logo 图像,那就是 http://<domainname>/<STATIC_URL>/logo.gif

默认情况下,这些设置已准备就绪。我们必须改变的一个选项是 DATABASES 设置。首先,我们创建将要使用的数据库:

(django-someHash) $ createdb django_todo

我们想要像使用 Flask、Pyramid 和 Tornado 一样使用 PostgreSQL 数据库,这意味着我们必须更改 DATABASES 设置以允许我们的服务器访问 PostgreSQL 数据库。首先是引擎。默认情况下,数据库引擎是 django.db.backends.sqlite3,我们把它改成 django.db.backends.postgresql

有关 Django 可用引擎的更多信息,请查看文档。请注意,尽管技术上可以将 NoSQL 解决方案整合到 Django 项目中,但为了开箱即用,Django 强烈偏向于 SQL 解决方案。

接下来,我们必须为连接参数的不同部分指定键值对。

  • NAME 是我们刚刚创建的数据库的名称。
  • USER 是 Postgres 数据库用户名。
  • PASSWORD 是访问数据库所需的密码。
  • HOST 是数据库的主机。当我们在本地开发时,localhost127.0.0.1 都将起作用。
  • PORT 是我们为 Postgres 开放的端口,它通常是 5432

settings.py 希望我们为每个键提供字符串值。但是,这是高度敏感的信息。任何负责任的开发人员都不应该这样做。有几种方法可以解决这个问题,一种是我们需要设置环境变量。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', ''),
        'USER': os.environ.get('DB_USER', ''),
        'PASSWORD': os.environ.get('DB_PASS', ''),
        'HOST': os.environ.get('DB_HOST', ''),
        'PORT': os.environ.get('DB_PORT', ''),
    }
}

在继续之前,请确保设置环境变量,否则 Django 将无法工作。此外,我们需要在此环境中安装 psycopg2,以便我们可以与数据库通信。

Django 路由和视图

让我们在这个项目中实现一些函数。我们将使用 Django REST 框架来构建 REST API,所以我们必须确保在 settings.py 中将 rest_framework 添加到 INSTALLED_APPS 的末尾。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework'
]

虽然 Django REST 框架并不专门需要基于类的视图(如 Tornado)来处理传入的请求,但类是编写视图的首选方法。让我们来定义一个类视图。

让我们在 django_todo 创建一个名为 views.py 的文件。在 views.py 中,我们将创建 “Hello, world!” 视图。

# in django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

class HelloWorld(APIView):
    def get(self, request, format=None):
        """Print 'Hello, world!' as the response body."""
        return JsonResponse("Hello, world!")

每个 Django REST 框架基于类的视图都直接或间接地继承自 APIViewAPIView 处理大量的东西,但针对我们的用途,它做了以下特定的事情:

* 根据 HTTP 方法(例如 GET、POST、PUT、DELETE)来设置引导对应请求所需的方法
* 用我们需要的所有数据和属性来填充 `request` 对象,以便解析和处理传入的请求 
* 采用 `Response` 或 `JsonResponse`,每个调度方法(即名为 `get`、`post`、`put`、`delete` 的方法)返回并构造格式正确的 HTTP 响应。

终于,我们有一个视图了!它本身没有任何作用,我们需要将它连接到路由。

如果我们跳转到 django_todo/urls.py,我们会到达默认的 URL 配置文件。如前所述:如果 Django 项目中的路由不包含在此处,则它不存在。

我们在给定的 urlpatterns 列表中添加所需的 URL。默认情况下,我们有一整套 URL 用于 Django 的内置管理后端系统。我们会完全删除它。

我们还得到一些非常有用的文档字符串,它告诉我们如何向 Django 项目添加路由。我们需要调用 path(),伴随三个参数:

  • 所需的路由,作为字符串(没有前导斜线)
  • 处理该路由的视图函数(只能有一个函数!)
  • 在 Django 项目中路由的名称

让我们导入 HelloWorld 视图并将其附加到主路径 / 。我们可以从 urlpatterns 中删除 admin 的路径,因为我们不会使用它。

# django_todo/urls.py, after the big doc string
from django.urls import path
from django_todo.views import HelloWorld

urlpatterns = [
    path('', HelloWorld.as_view(), name="hello"),
]

好吧,这里有一点不同。我们指定的路由只是一个空白字符串,为什么它会工作?Django 假设我们声明的每个路由都以一个前导斜杠开头,我们只是在初始域名后指定资源路由。如果一条路由没有去往一个特定的资源,而只是一个主页,那么该路由是 '',实际上是“没有资源”。

HelloWorld 视图是从我们刚刚创建的 views.py 文件导入的。为了执行此导入,我们需要更新 settings.py 中的 INSTALLED_APPS 列表使其包含 django_todo。是的,这有点奇怪。以下是一种理解方式。

INSTALLED_APPS 指的是 Django 认为可导入的目录或包的列表。它是 Django 处理项目的各个组件的方式,比如安装了一个包,而不需要经过 setup.py 的方式。我们希望将 django_todo 目录视为可导入的包,因此我们将该目录包含在 INSTALLED_APPS 中。现在,在该目录中的任何模块也是可导入的。所以我们得到了我们的视图。

path 函数只将视图函数作为第二个参数,而不仅仅是基于类的视图。幸运的是,所有有效的基于 Django 类的视图都包含 .as_view() 方法。它的工作是将基于类的视图的所有优点汇总到一个视图函数中并返回该视图函数。所以,我们永远不必担心转换的工作。相反,我们只需要考虑业务逻辑,让 Django 和 Django REST 框架处理剩下的事情。

让我们在浏览器中打开它!

Django 提供了自己的本地开发服务器,可通过 manage.py 访问。让我们切换到包含 manage.py 的目录并输入:

(django-someHash) $ ./manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
August 01, 2018 - 16:47:24
Django version 2.0.7, using settings 'django_todo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

runserver 执行时,Django 会检查以确保项目(或多或少)正确连接在一起。这不是万无一失的,但确实会发现一些明显的问题。如果我们的数据库与代码不同步,它会通知我们。毫无疑问,因为我们没有将任何应用程序的东西提交到我们的数据库,但现在这样做还是可以的。让我们访问 http://127.0.0.1:8000 来查看 HelloWorld 视图的输出。

咦?这不是我们在 Pyramid、Flask 和 Tornado 中看到的明文数据。当使用 Django REST 框架时,HTTP 响应(在浏览器中查看时)是这样呈现的 HTML,以红色显示我们的实际 JSON 响应。

但不要担心!如果我们在命令行中使用 curl 快速访问 http://127.0.0.1:8000,我们就不会得到任何花哨的 HTML,只有内容。

# 注意:在不同的终端口窗口中执行此操作,在虚拟环境之外
$ curl http://127.0.0.1:8000
"Hello, world!"

棒极了!

Django REST 框架希望我们在使用浏览器浏览时拥有一个人性化的界面。这是有道理的,如果在浏览器中查看 JSON,通常是因为人们想要检查它是否正确,或者在设计一些消费者 API 时想要了解 JSON 响应。这很像你从 Postman 中获得的东西。

无论哪种方式,我们都知道我们的视图工作了!酷!让我们概括一下我们做过的事情:

  1. 使用 django-admin startproject <项目名称> 开始一个项目
  2. 使用环境变量来更新 django_todo/settings.py 中的 DEBUGSECRET_KEY,还有 DATABASES 字典
  3. 安装 Django REST 框架,并将它添加到 INSTALLED_APPS
  4. 创建 django_todo/views.py 来包含我们的第一个类视图,它返回响应 “Hello, world!”
  5. 更新 django_todo/urls.py,其中包含我们的根路由
  6. django_todo/settings.py 中更新 INSTALLED_APPS 以包含 django_todo

创建模型

现在让我们来创建数据模型吧。

Django 项目的整个基础架构都是围绕数据模型构建的,它是这样编写的,每个数据模型够可以拥有自己的小天地,拥有自己的视图,自己与其资源相关的 URL 集合,甚至是自己的测试(如果我们想要的话)。

如果我们想构建一个简单的 Django 项目,我们可以通过在 django_todo 目录中编写我们自己的 models.py 文件并将其导入我们的视图来避免这种情况。但是,我们想以“正确”的方式编写 Django 项目,因此我们应该尽可能地将模型拆分成符合 Django Way™(Django 风格)的包。

Django Way 涉及创建所谓的 Django “应用程序”,它本身并不是单独的应用程序,它们没有自己的设置和诸如此类的东西(虽然它们也可以)。但是,它们可以拥有一个人们可能认为属于独立应用程序的东西:

  • 一组自建的 URL
  • 一组自建的 HTML 模板(如果我们想要提供 HTML)
  • 一个或多个数据模型
  • 一套自建的视图
  • 一套自建的测试

它们是独立的,因此可以像独立应用程序一样轻松共享。实际上,Django REST 框架是 Django 应用程序的一个例子。它包含自己的视图和 HTML 模板,用于提供我们的 JSON。我们只是利用这个 Django 应用程序将我们的项目变成一个全面的 RESTful API 而不用那么麻烦。

要为我们的待办事项列表项创建 Django 应用程序,我们将要使用 manage.pystartapp 命令。

(django-someHash) $ ./manage.py startapp todo

startapp 命令成功执行后没有输出。我们可以通过使用 ls 来检查它是否完成它应该做的事情。

(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    todo

看看:我们有一个全新的 todo 目录。让我们看看里面!

(django-someHash) $ ls todo
__init__.py admin.py    apps.py     migrations  models.py   tests.py    views.py

以下是 manage.py startapp 创建的文件:

  • __init__.py 是空文件。它之所以存在是因为此目录可看作是模型、视图等的有效导入路径。
  • admin.py 不是空文件。它用于在 Django admin 中规范化这个应用程序的模型,我们在本文中没有涉及到它。
  • apps.py 这里基本不起作用。它有助于规范化 Django admin 的模型。
  • migrations 是一个包含我们数据模型快照的目录。它用于更新数据库。这是少数几个内置了数据库管理的框架之一,其中一部分允许我们更新数据库,而不必拆除它并重建它以更改 Schema。
  • models.py 是数据模型所在。
  • tests.py 是测试所在的地方,如果我们需要写测试。
  • views.py 用于我们编写的与此应用程序中的模型相关的视图。它们不是一定得写在这里。例如,我们可以在 django_todo/views.py 中写下我们所有的视图。但是,它在这个应用程序中更容易将我们的概念理清。在覆盖了许多概念的扩展应用程序的关系之间会变得更加密切。

它并没有为这个应用程序创建 urls.py 文件,但我们可以自己创建。

(django-someHash) $ touch todo/urls.py

在继续之前,我们应该帮自己一个忙,将这个新 Django 应用程序添加到 django_todo/settings.py 中的 INSTALLED_APPS 列表中。

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'django_todo',
    'todo' # <--- 添加了这行
]

检查 todo/models.py 发现 manage.py 已经为我们编写了一些代码。不同于在 Flask、Tornado 和 Pyramid 实现中创建模型的方式,Django 不利用第三方来管理数据库会话或构建其对象实例。它全部归入 Django 的 django.db.models 子模块。

然而,建立模型的方式或多或少是相同的。要在 Django 中创建模型,我们需要构建一个继承自 models.Modelclass,将应用于该模型实例的所有字段都应视为类属性。我们不像过去那样从 SQLAlchemy 导入列和字段类型,而是直接从 django.db.models 导入。

# todo/models.py
from django.db import models

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)

虽然 Django 的需求和基于 SQLAlchemy 的系统之间存在一些明显的差异,但总体内容和结构或多或少相同。让我们来指出这些差异。

我们不再需要为对象实例声明自动递增 ID 的单独字段。除非我们指定一个不同的字段作为主键,否则 Django 会为我们构建一个。

我们只是直接引用数据类型作为列本身,而不是实例化传递数据类型对象的 Column 对象。

Unicode 字段变为 models.CharFieldmodels.TextFieldCharField 用于特定最大长度的小文本字段,而 TextField 用于任何数量的文本。

TextField 应该是空白的,我们以两种方式指定它。blank = True 表示当构建此模型的实例,并且正在验证附加到该字段的数据时,该数据是可以为空的。这与 null = True 不同,后者表示当构造此模型类的表时,对应于 note 的列将允许空白或为 NULL。因此,总而言之,blank = True 控制如何将数据添加到模型实例,而 null = True 控制如何构建保存该数据的数据库表。

DateTime 字段增加了一些属性,并且能够为我们做一些工作,使得我们不必修改类的 __init__ 方法。对于 creation_date 字段,我们指定 auto_now_add = True。在实际意义上意味着,当创建一个新模型实例时,Django 将自动记录现在的日期和时间作为该字段的值。这非常方便!

auto_now_add 及其类似属性 auto_now 都没被设置为 True 时,DateTimeField 会像其它字段一样需要预期的数据。它需要提供一个适当的 datetime 对象才能生效。due_date 列的 blanknull 属性都设置为 True,这样待办事项列表中的项目就可以成为将来某个时间点完成,没有确定的日期或时间。

BooleanField 最终可以取两个值:TrueFalse。这里,默认值设置为 False

管理数据库

如前所述,Django 有自己的数据库管理方式。我们可以利用 Django 提供的 manage.py 脚本,而不必编写任何关于数据库的代码。它不仅可以管理我们数据库的表的构建,还可以管理我们希望对这些表进行的任何更新,而不必将整个事情搞砸!

因为我们构建了一个新模型,所以我们需要让数据库知道它。首先,我们需要将与此模型对应的模式放入代码中。manage.pymakemigrations 命令对我们构建的模型类及其所有字段进行快照。它将获取该信息并将其打包成一个 Python 脚本,该脚本将存在于特定 Django 应用程序的 migrations 目录中。永远没有理由直接运行这个迁移脚本。它的存在只是为了让 Django 可以使用它作为更新数据库表的基础,或者在我们更新模型类时继承信息。

(django-someHash) $ ./manage.py makemigrations
Migrations for 'todo':
  todo/migrations/0001_initial.py
    - Create model Task

这将查找 INSTALLED_APPS 中列出的每个应用程序,并检查这些应用程序中存在的模型。然后,它将检查相应的 migrations 目录中的迁移文件,并将它们与每个 INSTALLED_APPS 中的模型进行比较。如果模型已经升级超出最新迁移所应存在的范围,则将创建一个继承自最新迁移文件的新迁移文件,它将自动命名,并且还会显示一条消息,说明自上次迁移以来发生了哪些更改。

如果你上次处理 Django 项目已经有一段时间了,并且不记得模型是否与迁移同步,那么你无需担心。makemigrations 是一个幂等操作。无论你运行 makemigrations 一次还是 20 次,migrations 目录只有一个与当前模型配置的副本。更棒的是,当我们运行 ./manage.py runserver 时,Django 检测到我们的模型与迁移不同步,它会用彩色文本告诉我们以便我们可以做出适当的选择。

下一个要点是至少让每个人访问一次:创建一个迁移文件不会立即影响我们的数据库。当我们运行 makemigrations 时,我们布置我们的 Django 项目定义了给定的表应该如何创建和最终查找。我们仍要将这些更改应用于数据库。这就是 migrate 命令的用途。

(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK

当我们应用这些迁移时,Django 首先检查其他 INSTALLED_APPS 是否有需要应用的迁移,它大致按照列出的顺序检查它们。我们希望我们的应用程序最后列出,因为我们希望确保,如果我们的模型依赖于任何 Django 的内置模型,我们所做的数据库更新不会受到依赖性问题的影响。

我们还有另一个要构建的模型:User 模型。但是,因为我们正在使用 Django,事情有一些变化。许多应用程序需要某种类型的用户模型,Django 的 django.contrib.auth 包构建了自己的用户模型供我们使用。如果无需用户所需要的身份验证令牌,我们可以继续使用它而不是重新发明轮子。

但是,我们需要那个令牌。我们可以通过两种方式来处理这个问题。

  • 继承 Django 的 User 对象,我们自己的对象通过添加 token 字段来扩展它
  • 创建一个与 Django 的 User 对象一对一关系的新对象,其唯一目的是持有一个令牌

我习惯于建立对象关系,所以让我们选择第二种选择。我们称之为 Owner,因为它基本上具有与 User 类似的内涵,这就是我们想要的。

出于纯粹的懒惰,我们可以在 todo/models.py 中包含这个新的 Owner 对象,但是不要这样做。Owner 没有明确地与任务列表上的项目的创建或维护有关。从概念上讲,Owner 只是任务的所有者。甚至有时候我们想要扩展这个 Owner 以包含与任务完全无关的其他数据。

为了安全起见,让我们创建一个 owner 应用程序,其工作是容纳和处理这个 Owner 对象。

(django-someHash) $ ./manage.py startapp owner

不要忘记在 settings.py 文件中的 INSTALLED_APPS 中添加它。 `INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_todo',
'todo',
'owner'
]`

如果我们查看 Django 项目的根目录,我们现在有两个 Django 应用程序:

(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    owner        todo

owner/models.py 中,让我们构建这个 Owner 模型。如前所述,它与 Django 的内置 User 对象有一对一的关系。我们可以用 Django 的 models.OneToOneField 强制实现这种关系。

# owner/models.py
from django.db import models
from django.contrib.auth.models import User
import secrets

class Owner(models.Model):
    """The object that owns tasks."""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256)
    def __init__(self, *args, **kwargs):
        """On construction, set token."""
        self.token = secrets.token_urlsafe(64)
        super().__init__(*args, **kwargs)

这表示 Owner 对象对应到 User 对象,每个 user 实例有一个 owner 实例。on_delete = models.CASCADE 表示如果相应的 User 被删除,它所对应的 Owner 实例也将被删除。让我们运行 makemigrationsmigrate 来将这个新模型放入到我们的数据库中。

(django-someHash) $ ./manage.py makemigrations
Migrations for 'owner':
  owner/migrations/0001_initial.py
    - Create model Owner
(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, owner, sessions, todo
Running migrations:
  Applying owner.0001_initial... OK

现在我们的 Owner 需要拥有一些 Task 对象。它与上面看到的 OneToOneField 非常相似,只不过我们会在 Task 对象上贴一个 ForeignKey 字段指向 Owner

# todo/models.py
from django.db import models
from owner.models import Owner

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE)

每个待办事项列表任务只有一个可以拥有多个任务的所有者。删除该所有者后,他们拥有的任务都会随之删除。

现在让我们运行 makemigrations 来获取我们的数据模型设置的新快照,然后运行 migrate 将这些更改应用到我们的数据库。

(django-someHash) django $ ./manage.py makemigrations
You are trying to add a non-nullable field 'owner' to task without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

不好了!出现了问题!发生了什么?其实,当我们创建 Owner 对象并将其作为 ForeignKey 添加到 Task 时,要求每个 Task 都需要一个 Owner。但是,我们为 Task 对象进行的第一次迁移不包括该要求。因此,即使我们的数据库表中没有数据,Django 也会对我们的迁移进行预先检查,以确保它们兼容,而我们提议的这种新迁移不是。

有几种方法可以解决这类问题:

  1. 退出当前迁移并构建一个包含当前模型配置的新迁移
  2. 将一个默认值添加到 Task 对象的 owner 字段
  3. 允许任务为 owner 字段设置 NULL

方案 2 在这里没有多大意义。我们建议,默认情况下,任何创建的 Task 都会对应到某个默认所有者,尽管默认所有者不一定存在。 方案 1 要求我们销毁和重建我们的迁移,而我们应该把它们留下。

让我们考虑选项 3。在这种情况下,如果我们允许 Task 表为所有者提供空值,它不会很糟糕。从这一点开始创建的任何任务都必然拥有一个所有者。如果你的数据库表并非不能接受重新架构,请删除该迁移、删除表并重建迁移。

# todo/models.py
from django.db import models
from owner.models import Owner

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE, null=True)
(django-someHash) $ ./manage.py makemigrations
Migrations for 'todo':
  todo/migrations/0002_task_owner.py
    - Add field owner to task
(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, owner, sessions, todo
Running migrations:
  Applying todo.0002_task_owner... OK

酷!我们有模型了!欢迎使用 Django 声明对象的方式。

出于更好的权衡,让我们确保无论何时制作 User,它都会自动与新的 Owner 对象对应。我们可以使用 Django 的 signals 系统来做到这一点。基本上,我们确切地表达了意图:“当我们得到一个新的 User 被构造的信号时,构造一个新的 Owner 并将新的 User 设置为 Owneruser 字段。”在实践中看起来像这样:

# owner/models.py
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

import secrets


class Owner(models.Model):
    """The object that owns tasks."""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256)

    def __init__(self, *args, **kwargs):
        """On construction, set token."""
        self.token = secrets.token_urlsafe(64)
        super().__init__(*args, **kwargs)


@receiver(post_save, sender=User)
def link_user_to_owner(sender, **kwargs):
    """If a new User is saved, create a corresponding Owner."""
    if kwargs['created']:
        owner = Owner(user=kwargs['instance'])
        owner.save()

我们设置了一个函数,用于监听从 Django 中内置的 User 对象发送的信号。它正在等待 User 对象被保存之后的情况。这可以来自新的 User 或对现有 User 的更新。我们在监听功能中辨别出两种情况。

如果发送信号的东西是新创建的实例,kwargs ['created'] 将具有值 True。如果是 True 的话,我们想做点事情。如果它是一个新实例,我们创建一个新的 Owner,将其 user 字段设置为创建的新 User 实例。之后,我们 save() 新的 Owner。如果一切正常,这将提交更改到数据库。如果数据没通过我们声明的字段的验证,它将失败。

现在让我们谈谈我们将如何访问数据。

访问模型数据

在 Flask、Pyramid 和 Tornado 框架中,我们通过对某些数据库会话运行查询来访问模型数据。也许它被附加到 request 对象,也许它是一个独立的 session 对象。无论如何,我们必须建立与数据库的实时连接并在该连接上进行查询。

这不是 Django 的工作方式。默认情况下,Django 不利用任何第三方对象关系映射(ORM)与数据库进行通信。相反,Django 允许模型类维护自己与数据库的对话。

django.db.models.Model 继承的每个模型类都会附加一个 objects 对象。这将取代我们熟悉的 sessiondbsession。让我们打开 Django 给我们的特殊 shell,并研究这个 objects 对象是如何工作的。

(django-someHash) $ ./manage.py shell
Python 3.7.0 (default, Jun 29 2018, 20:13:13)
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

Django shell 与普通的 Python shell 不同,因为它知道我们正在构建的 Django 项目,可以轻松导入我们的模型、视图、设置等,而不必担心安装包。我们可以通过简单的 import 访问我们的模型。

>>> from owner.models import Owner
>>> Owner
<class 'owner.models.Owner'>

目前,我们没有 Owner 实例。我们可以通过 Owner.objects.all() 查询它们。

>>> Owner.objects.all()
<QuerySet []>

无论何时我们在 <Model> .objects 对象上运行查询方法,我们都会得到 QuerySet。为了我们的目的,它实际上是一个列表,这个列表向我们显示它是空的。让我们通过创建一个 User 来创建一个 Owner

>>> from django.contrib.auth.models import User
>>> new_user = User(username='kenyattamurphy', email='[email protected]')
>>> new_user.set_password('wakandaforever')
>>> new_user.save()

如果我们现在查询所有的 Owner,我们应该会找到 Kenyatta。

>>> Owner.objects.all()
<QuerySet [<Owner: Owner object (1)>]>

棒极了!我们得到了数据!

序列化模型

我们将在 “Hello World” 之外来回传递数据。因此,我们希望看到某种类似于 JSON 类型的输出,它可以很好地表示数据。获取该对象的数据并将其转换为 JSON 对象以通过 HTTP 提交是数据序列化的一种方式。在序列化数据时,我们正在获取我们目前拥有的数据并重新格式化以适应一些标准的、更易于理解的形式。

如果我用 Flask、Pyramid 和 Tornado 这样做,我会在每个模型上创建一个新方法,让用户可以直接调用 to_json()to_json() 的唯一工作是返回一个 JSON 可序列化的(即数字、字符串、列表、字典)字典,其中包含我想要为所讨论的对象显示的任何字段。

对于 Task 对象,它可能看起来像这样:

class Task(Base):
    ...all the fields...

    def to_json(self):
        """Convert task attributes to a JSON-serializable dict."""
        return {
            'id': self.id,
            'name': self.name,
            'note': self.note,
            'creation_date': self.creation_date.strftime('%m/%d/%Y %H:%M:%S'),
            'due_date': self.due_date.strftime('%m/%d/%Y %H:%M:%S'),
            'completed': self.completed,
            'user': self.user_id
        }

这不花哨,但它确实起到了作用。

然而,Django REST 框架为我们提供了一个对象,它不仅可以为我们这样做,还可以在我们想要创建新对象实例或更新现有实例时验证输入,它被称为 ModelSerializer

Django REST 框架的 ModelSerializer 是我们模型的有效文档。如果没有附加模型,它们就没有自己的生命(因为那里有 Serializer 类)。它们的主要工作是准确地表示我们的模型,并在我们的模型数据需要序列化并通过线路发送时,将其转换为 JSON。

Django REST 框架的 ModelSerializer 最适合简单对象。举个例子,假设我们在 Task 对象上没有 ForeignKey。我们可以为 Task 创建一个序列化器,它将根据需要将其字段值转换为 JSON,声明如下:

# todo/serializers.py
from rest_framework import serializers
from todo.models import Task

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    class Meta:
        model = Task
        fields = ('id', 'name', 'note', 'creation_date', 'due_date', 'completed')

在我们新的 TaskSerializer 中,我们创建了一个 Meta 类。Meta 的工作就是保存关于我们试图序列化的东西的信息(或元数据)。然后,我们会注意到要显示的特定字段。如果我们想要显示所有字段,我们可以简化过程并使用 __all __。或者,我们可以使用 exclude 关键字而不是 fields 来告诉 Django REST 框架我们想要除了少数几个字段以外的每个字段。我们可以拥有尽可能多的序列化器,所以也许我们想要一个用于一小部分字段,而另一个用于所有字段?在这里都可以。

在我们的例子中,每个 Task 和它的所有者 Owner 之间都有一个关系,必须在这里反映出来。因此,我们需要借用 serializers.PrimaryKeyRelatedField 对象来指定每个 Task 都有一个 Owner,并且该关系是一对一的。它的所有者将从已有的全部所有者的集合中找到。我们通过对这些所有者进行查询并返回我们想要与此序列化程序关联的结果来获得该集合:Owner.objects.all()。我们还需要在字段列表中包含 owner,因为我们总是需要一个与 Task 相关联的 Owner

# todo/serializers.py
from rest_framework import serializers
from todo.models import Task
from owner.models import Owner

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    owner = serializers.PrimaryKeyRelatedField(queryset=Owner.objects.all())

    class Meta:
        model = Task
        fields = ('id', 'name', 'note', 'creation_date', 'due_date', 'completed', 'owner')

现在构建了这个序列化器,我们可以将它用于我们想要为我们的对象做的所有 CRUD 操作:

  • 如果我们想要 GET 一个特定的 Task 的 JSON 类型版本,我们可以做 TaskSerializer((some_task).data
  • 如果我们想接受带有适当数据的 POST 来创建一个新的 Task,我们可以使用 TaskSerializer(data = new_data).save()
  • 如果我们想用 PUT 更新一些现有数据,我们可以用 TaskSerializer(existing_task, data = data).save()

我们没有包括 delete,因为我们不需要对 delete 操作做任何事情。如果你可以删除一个对象,只需使用 object_instance.delete()

以下是一些序列化数据的示例:

>>> from todo.models import Task
>>> from todo.serializers import TaskSerializer
>>> from owner.models import Owner
>>> from django.contrib.auth.models import User
>>> new_user = User(username='kenyatta', email='[email protected]')
>>> new_user.save_password('wakandaforever')
>>> new_user.save() # creating the User that builds the Owner
>>> kenyatta = Owner.objects.first() # 找到 kenyatta 的所有者
>>> new_task = Task(name="Buy roast beef for the Sunday potluck", owner=kenyatta)
>>> new_task.save()
>>> TaskSerializer(new_task).data
{'id': 1, 'name': 'Go to the supermarket', 'note': None, 'creation_date': '2018-07-31T06:00:25.165013Z', 'due_date': None, 'completed': False, 'owner': 1}

使用 ModelSerializer 对象可以做更多的事情,我建议查看文档以获得更强大的功能。否则,这就是我们所需要的。现在是时候深入视图了。

查看视图

我们已经构建了模型和序列化器,现在我们需要为我们的应用程序设置视图和 URL。毕竟,对于没有视图的应用程序,我们无法做任何事情。我们已经看到了上面的 HelloWorld 视图的示例。然而,这总是一个人为的、概念验证的例子,并没有真正展示 Django REST 框架的视图可以做些什么。让我们清除 HelloWorld 视图和 URL,这样我们就可以从我们的视图重新开始。

我们要构建的第一个视图是 InfoView。与之前的框架一样,我们只想打包并发送一个我们用到的路由的字典。视图本身可以存在于 django_todo.views 中,因为它与特定模型无关(因此在概念上不属于特定应用程序)。

# django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

class InfoView(APIView):
    """List of routes for this API."""
    def get(self, request):
        output = {
            'info': 'GET /api/v1',
            'register': 'POST /api/v1/accounts',
            'single profile detail': 'GET /api/v1/accounts/<username>',
            'edit profile': 'PUT /api/v1/accounts/<username>',
            'delete profile': 'DELETE /api/v1/accounts/<username>',
            'login': 'POST /api/v1/accounts/login',
            'logout': 'GET /api/v1/accounts/logout',
            "user's tasks": 'GET /api/v1/accounts/<username>/tasks',
            "create task": 'POST /api/v1/accounts/<username>/tasks',
            "task detail": 'GET /api/v1/accounts/<username>/tasks/<id>',
            "task update": 'PUT /api/v1/accounts/<username>/tasks/<id>',
            "delete task": 'DELETE /api/v1/accounts/<username>/tasks/<id>'
        }
        return JsonResponse(output)

这与我们在 Tornado 中所拥有的完全相同。让我们将它放置到合适的路由并继续。为了更好的测试,我们还将删除 admin/ 路由,因为我们不会在这里使用 Django 管理后端。

# in django_todo/urls.py
from django_todo.views import InfoView
from django.urls import path

urlpatterns = [
    path('api/v1', InfoView.as_view(), name="info"),
]

连接模型与视图

让我们弄清楚下一个 URL,它将是创建新的 Task 或列出用户现有任务的入口。这应该存在于 todo 应用程序的 urls.py 中,因为它必须专门处理 Task对象而不是整个项目的一部分。

# in todo/urls.py
from django.urls import path
from todo.views import TaskListView

urlpatterns = [
    path('', TaskListView.as_view(), name="list_tasks")
]

这个路由处理的是什么?我们根本没有指定特定用户或路径。由于会有一些路由需要基本路径 /api/v1/accounts/<username>/tasks,为什么我们只需写一次就能一次又一次地写它?

Django 允许我们用一整套 URL 并将它们导入 django_todo/urls.py 文件。然后,我们可以为这些导入的 URL 中的每一个提供相同的基本路径,只关心可变部分,你知道它们是不同的。

# in django_todo/urls.py
from django.urls import include, path
from django_todo.views import InfoView

urlpatterns = [
    path('api/v1', InfoView.as_view(), name="info"),
    path('api/v1/accounts/<str:username>/tasks', include('todo.urls'))
]

现在,来自 todo/urls.py 的每个 URL 都将以路径 api/v1/accounts/<str:username>/tasks 为前缀。

让我们在 todo/views.py 中构建视图。

# todo/views.py
from django.shortcuts import get_object_or_404
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

from owner.models import Owner
from todo.models import Task
from todo.serializers import TaskSerializer


class TaskListView(APIView):
    def get(self, request, username, format=None):
        """Get all of the tasks for a given user."""
        owner = get_object_or_404(Owner, user__username=username)
        tasks = Task.objects.filter(owner=owner).all()
        serialized = TaskSerializer(tasks, many=True)
        return JsonResponse({
            'username': username,
            'tasks': serialized.data
        })

这里一点代码里面有许多要说明的,让我们来看看吧。

我们从与我们一直使用的 APIView 的继承开始,为我们的视图奠定基础。我们覆盖了之前覆盖的相同 get 方法,添加了一个参数,允许我们的视图从传入的请求中接收 username

然后我们的 get 方法将使用 username 来获取与该用户关联的 Owner。这个 get_object_or_404 函数允许我们这样做,添加一些特殊的东西以方便使用。

如果无法找到指定的用户,那么查找任务是没有意义的。实际上,我们想要返回 404 错误。get_object_or_404 根据我们传入的任何条件获取单个对象,并返回该对象或引发 Http 404 异常。我们可以根据对象的属性设置该条件。Owner 对象都通过 user 属性附加到 User。但是,我们没有要搜索的 User 对象,我们只有一个 username。所以,当你寻找一个 Owner 时,我们对 get_object_or_404 说:通过指定 user__username(这是两个下划线)来检查附加到它的 User 是否具有我想要的 username。通过 QuerySet 过滤时,这两个下划线表示 “此嵌套对象的属性”。这些属性可以根据需要进行深度嵌套。

我们现在拥有与给定用户名相对应的 Owner。我们使用 Owner 来过滤所有任务,只用 Task.objects.filter 检索它拥有的任务。我们可以使用与 get_object_or_404 相同的嵌套属性模式来钻入连接到 TasksOwnerUsertasks = Task.objects.filter(owner__user__username = username)).all()),但是没有必要那么宽松。

Task.objects.filter(owner = owner).all() 将为我们提供与我们的查询匹配的所有 Task 对象的QuerySet。很棒。然后,TaskSerializer 将获取 QuerySet 及其所有数据以及 many = True 标志,以通知其为项目集合而不是仅仅一个项目,并返回一系列序列化结果。实际上是一个词典列表。最后,我们使用 JSON 序列化数据和用于查询的用户名提供传出响应。

处理 POST 请求

post 方法看起来与我们之前看到的有些不同。

# still in todo/views.py
# ...other imports...
from rest_framework.parsers import JSONParser
from datetime import datetime

class TaskListView(APIView):
    def get(self, request, username, format=None):
        ...

    def post(self, request, username, format=None):
        """Create a new Task."""
        owner = get_object_or_404(Owner, user__username=username)
        data = JSONParser().parse(request)
        data['owner'] = owner.id
        if data['due_date']:
            data['due_date'] = datetime.strptime(data['due_date'], '%d/%m/%Y %H:%M:%S')

        new_task = TaskSerializer(data=data)
        if new_task.is_valid():
            new_task.save()
            return JsonResponse({'msg': 'posted'}, status=201)

        return JsonResponse(new_task.errors, status=400)

当我们从客户端接收数据时,我们使用 JSONParser().parse(request) 将其解析为字典。我们将所有者添加到数据中并格式化任务的 due_date(如果存在)。

我们的 TaskSerializer 完成了繁重的任务。它首先接收传入的数据并将其转换为我们在模型上指定的字段。然后验证该数据以确保它适合指定的字段。如果附加到新 Task 的数据有效,它将使用该数据构造一个新的 Task 对象并将其提交给数据库。然后我们发回适当的“耶!我们做了一件新东西!”响应。如果没有,我们收集 TaskSerializer 生成的错误,并将这些错误发送回客户端,并返回 400 Bad Request 状态代码。

如果我们要构建 put 视图来更新 Task,它看起来会非常相似。主要区别在于,当我们实例化 TaskSerializer 时,我们将传递旧对象和该对象的新数据,如 TaskSerializer(existing_task,data = data)。我们仍然会进行有效性检查并发回我们想要发回的响应。

总结

Django 作为一个框架是高度可定制的,每个人都有自己打造 Django 项目的方式。我在这里写出来的方式不一定是 Django 建立项目的确切方式。它只是 a) 我熟悉的方式,以及 b) 利用 Django 的管理系统。当你将概念切分到不同的小块时,Django 项目的复杂性会增加。这样做是为了让多个人更容易为整个项目做出贡献,而不会麻烦彼此。

然而,作为 Django 项目的大量文件映射并不能使其更高效或自然地偏向于微服务架构。相反,它很容易成为一个令人困惑的独石应用,这可能对你的项目仍然有用,它也可能使你的项目难以管理,尤其是随着项目的增长。

仔细考虑你的需求并使用合适的工具来完成正确的工作。对于像这样的简单项目,Django 可能不是合适的工具。

Django 旨在处理多种模型,这些模型涵盖了不同的项目领域,但它们可能有一些共同点。这个项目是一个小型的双模型项目,有一些路由。即便我们把它构建更复杂,也只有七条路由,而仍然只是相同的两个模型。这还不足以证明一个完整的 Django 项目。

如果我们期望这个项目能够拓展,那么将会是一个很好的选择。这不是那种项目。这就是使用火焰喷射器来点燃蜡烛,绝对是大材小用了。

尽管如此,Web 框架就是一个 Web 框架,无论你使用哪个框架。它都可以接收请求并做出任何响应,因此你可以按照自己的意愿进行操作。只需要注意你选择的框架所带来的开销。

就是这样!我们已经到了这个系列的最后!我希望这是一次启发性的冒险。当你在考虑如何构建你的下一个项目时,它将帮助你做出的不仅仅是最熟悉的选择。请务必阅读每个框架的文档,以扩展本系列中涉及的任何内容(因为它没有那么全面)。每个人都有一个广阔的世界。愉快地写代码吧!


via: https://opensource.com/article/18/8/django-framework

作者:Nicholas Hunt-Walker 选题:lujun9972 译者:MjSeven 校对:Bestony, wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Python 测试工具最好的一方面是其强大的生态系统。这里列出了八个最好的插件。

我们是 pytest 的忠实粉丝,并将其作为工作和开源项目的默认 Python 测试工具。在本月的 Python 专栏中,我们分享了为什么我们喜欢 pytest 以及一些让 pytest 测试工作更有趣的插件。

什么是 pytest?

正如该工具的网站所说,“pytest 框架可以轻松地编写小型测试,也能进行扩展以支持应用和库的复杂功能测试。”

pytest 允许你在任何名为 test_*.py 的文件中定义测试,并将其定义为以 test_* 开头的函数。然后,pytest 将在整个项目中查找所有测试,并在控制台中运行 pytest 时自动运行这些测试。pytest 接受标志和参数,它们可以在测试运行器停止时更改,这些包含如何输出结果,运行哪些测试以及输出中包含哪些信息。它还包括一个 set_trace() 函数,它可以进入到你的测试中。它会暂停您的测试, 并允许你与变量进行交互,不然你只能在终端中“四处翻弄”来调试你的项目。

pytest 最好的一方面是其强大的插件生态系统。因为 pytest 是一个非常流行的测试库,所以多年来创建了许多插件来扩展、定制和增强其功能。这八个插件是我们的最爱。

8 个很棒的插件

1、pytest-sugar

pytest-sugar 改变了 pytest 的默认外观,添加了一个进度条,并立即显示失败的测试。它不需要配置,只需 pip install pytest-sugar,用 pytest 运行测试,来享受更漂亮、更有用的输出。

2、pytest-cov

pytest-covpytest 中增加了覆盖率支持,来显示哪些代码行已经测试过,哪些还没有。它还将包括项目的测试覆盖率。

3、pytest-picked

pytest-picked 对你已经修改但尚未提交 git 的代码运行测试。安装库并运行 pytest --picked 来仅测试自上次提交后已更改的文件。

4、pytest-instafail

pytest-instafail 修改 pytest 的默认行为来立即显示失败和错误,而不是等到 pytest 完成所有测试。

5、pytest-tldr

一个全新的 pytest 插件,可以将输出限制为你需要的东西。pytest-tldrtldr 代表 “too long, didn’t read” —— 太长,不想读),就像 pytest-sugar 一样,除基本安装外不需要配置。不像 pytest 的默认输出那么详细,pytest-tldr 将默认输出限制为失败测试的回溯信息,并忽略了一些令人讨厌的颜色编码。添加 -v 标志会为喜欢它的人返回更详细的输出。

6、pytest-xdist

pytest-xdist 允许你通过 -n 标志并行运行多个测试:例如,pytest -n 2 将在两个 CPU 上运行你的测试。这可以显著加快你的测试速度。它还包括 --looponfail 标志,它将自动重新运行你的失败测试。

7、pytest-django

pytest-django 为 Django 应用和项目添加了 pytest 支持。具体来说,pytest-django 引入了使用 pytest fixture 测试 Django 项目的能力,而省略了导入 unittest 和复制/粘贴其他样板测试代码的需要,并且比标准的 Django 测试套件运行得更快。

8、django-test-plus

django-test-plus 并不是专门为 pytest 开发,但它现在支持 pytest。它包含自己的 TestCase 类,你的测试可以继承该类,并使你能够使用较少的按键来输出频繁的测试案例,例如检查特定的 HTTP 错误代码。

我们上面提到的库绝不是你扩展 pytest 的唯一选择。有用的 pytest 插件的前景是广阔的。查看 pytest 插件兼容性页面来自行探索。你最喜欢哪些插件?


via: https://opensource.com/article/18/6/pytest-plugins

作者:Jeff Triplett, Lacery Williams Henschel 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出