标签 python 下的文章

两种编程语言都各有优缺点,它们在某些任务方面互有胜负。

BashPython 是大多数自动化工程师最喜欢的编程语言。它们都各有优缺点,有时很难选择应该使用哪一个。所以,最诚实的答案是:这取决于任务、范围、背景和任务的复杂性。

让我们来比较一下这两种语言,以便更好地理解它们各自的优点。

Bash

  • 是一种 Linux/Unix shell 命令语言
  • 非常适合编写使用命令行界面(CLI)实用程序的 shell 脚本,利用一个命令的输出传递给另一个命令(管道),以及执行简单的任务(可以多达 100 行代码)
  • 可以按原样使用命令行命令和实用程序
  • 启动时间比 Python 快,但执行时性能差
  • Windows 中默认没有安装。你的脚本可能不会兼容多个操作系统,但是 Bash 是大多数 Linux/Unix 系统的默认 shell
  • 与其它 shell (如 csh、zsh、fish) 完全兼容。
  • 通过管道(|)传递 CLI 实用程序如 sedawkgrep 等会降低其性能
  • 缺少很多函数、对象、数据结构和多线程支持,这限制了它在复杂脚本或编程中的使用
  • 缺少良好的调试工具和实用程序

Python

  • 是一种面对对象编程语言(OOP),因此它比 Bash 更加通用
  • 几乎可以用于任何任务
  • 适用于大多数操作系统,默认情况下它在大多数 Unix/Linux 系统中都有安装
  • 与伪代码非常相似
  • 具有简单、清晰、易于学习和阅读的语法
  • 拥有大量的库、文档以及一个活跃的社区
  • 提供比 Bash 更友好的错误处理特性
  • 有比 Bash 更好的调试工具和实用程序,这使得它在开发涉及到很多行代码的复杂软件应用程序时是一种很棒的语言
  • 应用程序(或脚本)可能包含许多第三方依赖项,这些依赖项必须在执行前安装
  • 对于简单任务,需要编写比 Bash 更多的代码

我希望这些列表能够让你更好地了解该使用哪种语言以及在何时使用它。

你在日常工作中更多会使用哪种语言,Bash 还是 Python?请在评论中分享。


via: https://opensource.com/article/19/4/bash-vs-python

作者:Archit Modi (Red Hat) 选题:lujun9972 译者:MjSeven 校对:wxy

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

以下提到的这些 Python 工具在编辑图像、操作图像底层数据方面都提供了简单直接的方法。

当今的世界充满了数据,而图像数据就是其中很重要的一部分。但只有经过处理和分析,提高图像的质量,从中提取出有效地信息,才能利用到这些图像数据。

常见的图像处理操作包括显示图像,基本的图像操作,如裁剪、翻转、旋转;图像的分割、分类、特征提取;图像恢复;以及图像识别等等。Python 作为一种日益风靡的科学编程语言,是这些图像处理操作的最佳选择。同时,在 Python 生态当中也有很多可以免费使用的优秀的图像处理工具。

下文将介绍 10 个可以用于图像处理任务的 Python 库,它们在编辑图像、查看图像底层数据方面都提供了简单直接的方法。

1、scikit-image

scikit-image 是一个结合 NumPy 数组使用的开源 Python 工具,它实现了可用于研究、教育、工业应用的算法和应用程序。即使是对于刚刚接触 Python 生态圈的新手来说,它也是一个在使用上足够简单的库。同时它的代码质量也很高,因为它是由一个活跃的志愿者社区开发的,并且通过了 同行评审 peer review

资源

scikit-image 的文档非常完善,其中包含了丰富的用例。

示例

可以通过导入 skimage 使用,大部分的功能都可以在它的子模块中找到。

图像滤波 image filtering

import matplotlib.pyplot as plt
%matplotlib inline

from skimage import data,filters

image = data.coins() # ... or any other NumPy array!
edges = filters.sobel(image)
plt.imshow(edges, cmap='gray')

 title=

使用 match\_template() 方法实现 模板匹配 template matching

 title=

展示页面可以看到更多相关的例子。

2、NumPy

NumPy 提供了对数组的支持,是 Python 编程的一个核心库。图像的本质其实也是一个包含像素数据点的标准 NumPy 数组,因此可以通过一些基本的 NumPy 操作(例如切片、 掩膜 mask 花式索引 fancy indexing 等),就可以从像素级别对图像进行编辑。通过 NumPy 数组存储的图像也可以被 skimage 加载并使用 matplotlib 显示。

资源

在 NumPy 的官方文档中提供了完整的代码文档和资源列表。

示例

使用 NumPy 对图像进行 掩膜 mask 操作:

import numpy as np
from skimage import data
import matplotlib.pyplot as plt
%matplotlib inline

image = data.camera()
type(image)
numpy.ndarray #Image is a NumPy array:

mask = image < 87
image[mask]=255
plt.imshow(image, cmap='gray')

 title=

3、SciPy

像 NumPy 一样,SciPy 是 Python 的一个核心科学计算模块,也可以用于图像的基本操作和处理。尤其是 SciPy v1.1.0 中的 scipy.ndimage 子模块,它提供了在 n 维 NumPy 数组上的运行的函数。SciPy 目前还提供了 线性和非线性滤波 linear and non-linear filtering 二值形态学 binary morphology B 样条插值 B-spline interpolation 对象测量 object measurements 等方面的函数。

资源

官方文档中可以查阅到 scipy.ndimage 的完整函数列表。

示例

使用 SciPy 的高斯滤波对图像进行模糊处理:

from scipy import misc,ndimage

face = misc.face()
blurred_face = ndimage.gaussian_filter(face, sigma=3)
very_blurred = ndimage.gaussian_filter(face, sigma=5)

#Results
plt.imshow(<image to be displayed>)

 title=

4、PIL/Pillow

PIL (Python Imaging Library) 是一个免费 Python 编程库,它提供了对多种格式图像文件的打开、编辑、保存的支持。但在 2009 年之后 PIL 就停止发布新版本了。幸运的是,还有一个 PIL 的积极开发的分支 Pillow,它的安装过程比 PIL 更加简单,支持大部分主流的操作系统,并且还支持 Python 3。Pillow 包含了图像的基础处理功能,包括像素点操作、使用内置卷积内核进行滤波、颜色空间转换等等。

资源

Pillow 的官方文档提供了 Pillow 的安装说明自己代码库中每一个模块的示例。

示例

使用 Pillow 中的 ImageFilter 模块实现图像增强:

from PIL import Image,ImageFilter
#Read image
im = Image.open('image.jpg')
#Display image
im.show()

from PIL import ImageEnhance
enh = ImageEnhance.Contrast(im)
enh.enhance(1.8).show("30% more contrast")

 title=

5、OpenCV-Python

OpenCV(Open Source Computer Vision 库)是计算机视觉领域最广泛使用的库之一,OpenCV-Python 则是 OpenCV 的 Python API。OpenCV-Python 的运行速度很快,这归功于它使用 C/C++ 编写的后台代码,同时由于它使用了 Python 进行封装,因此调用和部署的难度也不大。这些优点让 OpenCV-Python 成为了计算密集型计算机视觉应用程序的一个不错的选择。

资源

入门之前最好先阅读 OpenCV2-Python-Guide 这份文档。

示例

使用 OpenCV-Python 中的 金字塔融合 Pyramid Blending 将苹果和橘子融合到一起:

 title=

6、SimpleCV

SimpleCV 是一个开源的计算机视觉框架。它支持包括 OpenCV 在内的一些高性能计算机视觉库,同时不需要去了解 位深度 bit depth 、文件格式、 色彩空间 color space 之类的概念,因此 SimpleCV 的学习曲线要比 OpenCV 平缓得多,正如它的口号所说,“将计算机视觉变得更简单”。SimpleCV 的优点还有:

  • 即使是刚刚接触计算机视觉的程序员也可以通过 SimpleCV 来实现一些简易的计算机视觉测试
  • 录像、视频文件、图像、视频流都在支持范围内

资源

官方文档简单易懂,同时也附有大量的学习用例。

示例

 title=

7、Mahotas

Mahotas 是另一个 Python 图像处理和计算机视觉库。在图像处理方面,它支持滤波和形态学相关的操作;在计算机视觉方面,它也支持 特征计算 feature computation 兴趣点检测 interest point detection 局部描述符 local descriptors 等功能。Mahotas 的接口使用了 Python 进行编写,因此适合快速开发,而算法使用 C++ 实现,并针对速度进行了优化。Mahotas 尽可能做到代码量少和依赖项少,因此它的运算速度非常快。可以参考官方文档了解更多详细信息。

资源

文档包含了安装介绍、示例以及一些 Mahotas 的入门教程。

示例

Mahotas 力求使用少量的代码来实现功能。例如这个 Finding Wally 游戏:

 title=

 title=

8、SimpleITK

ITK(Insight Segmentation and Registration Toolkit)是一个为开发者提供普适性图像分析功能的开源、跨平台工具套件,SimpleITK 则是基于 ITK 构建出来的一个简化层,旨在促进 ITK 在快速原型设计、教育、解释语言中的应用。SimpleITK 作为一个图像分析工具包,它也带有大量的组件,可以支持常规的滤波、图像分割、 图像配准 registration 功能。尽管 SimpleITK 使用 C++ 编写,但它也支持包括 Python 在内的大部分编程语言。

资源

有很多 Jupyter Notebooks 用例可以展示 SimpleITK 在教育和科研领域中的应用,通过这些用例可以看到如何使用 Python 和 R 利用 SimpleITK 来实现交互式图像分析。

示例

使用 Python + SimpleITK 实现的 CT/MR 图像配准过程:

 title=

9、pgmagick

pgmagick 是使用 Python 封装的 GraphicsMagick 库。GraphicsMagick 通常被认为是图像处理界的瑞士军刀,因为它强大而又高效的工具包支持对多达 88 种主流格式图像文件的读写操作,包括 DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM、TIFF 等等。

资源

pgmagick 的 GitHub 仓库中有相关的安装说明、依赖列表,以及详细的使用指引

示例

图像缩放:

 title=

边缘提取:

 title=

10、Pycairo

Cairo 是一个用于绘制矢量图的二维图形库,而 Pycairo 是用于 Cairo 的一组 Python 绑定。矢量图的优点在于做大小缩放的过程中不会丢失图像的清晰度。使用 Pycairo 可以在 Python 中调用 Cairo 的相关命令。

资源

Pycairo 的 GitHub 仓库提供了关于安装和使用的详细说明,以及一份简要介绍 Pycairo 的入门指南

示例

使用 Pycairo 绘制线段、基本图形、 径向渐变 radial gradients

 title=

总结

以上就是 Python 中的一些有用的图像处理库,无论你有没有听说过、有没有使用过,都值得试用一下并了解它们。


via: https://opensource.com/article/19/3/python-image-manipulation-tools

作者:Parul Pandey 选题:lujun9972 译者:HankChow 校对:wxy

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

假如说,如果你的老板给你的任务是一次又一次地访问竞争对手的网站,把对方商品的价格记录下来,而且要纯手工操作,恐怕你会想要把整个办公室都烧掉。

之所以现在网络爬虫的影响力如此巨大,就是因为网络爬虫可以被用于追踪客户的情绪和趋向、搜寻空缺的职位、监控房地产的交易,甚至是获取 UFC 的比赛结果。除此以外,还有很多意想不到的用途。

对于有这方面爱好的人来说,爬虫无疑是一个很好的工具。因此,我使用了 Scrapy 这个基于 Python 编写的开源网络爬虫框架。

鉴于我不太了解这个工具是否会对我的计算机造成伤害,我并没有将它搭建在我的主力机器上,而是搭建在了一台树莓派上面。

令人感到意外的是,Scrapy 在树莓派上面的性能并不差,或许这是 ARM 架构服务器的又一个成功例子?

我尝试 Google 了一下,但并没有得到令我满意的结果,仅仅找到了一篇相关的《Drupal 建站对比》。这篇文章的结论是,ARM 架构服务器性能比昂贵的 x86 架构服务器要更好。

从另一个角度来看,这种 web 服务可以看作是一个“被爬虫”服务,但和 Scrapy 对比起来,前者是基于 LAMP 技术栈,而后者则依赖于 Python,这就导致两者之间没有太多的可比性。

那我们该怎样做呢?只能在一些 VPS 上搭建服务来对比一下了。

什么是 ARM 架构处理器?

ARM 是目前世界上最流行的 CPU 架构。

但 ARM 架构处理器在很多人眼中的地位只是作为一个省钱又省电的选择,而不是跑在生产环境中的处理器的首选。

然而,诞生于英国剑桥的 ARM CPU,最初是用于极其昂贵的 Acorn Archimedes 计算机上的,这是当时世界上最强大的桌面计算机,甚至在很长一段时间内,它的运算速度甚至比最快的 386 还要快好几倍。

Acorn 公司和 Commodore、Atari 的理念类似,他们认为一家伟大的计算机公司就应该制造出伟大的计算机,让人感觉有点目光短浅。而比尔盖茨的想法则有所不同,他力图在更多不同种类和价格的 x86 机器上使用他的 DOS 系统。

拥有大量用户基数的平台会成为第三方开发者开发软件的平台,而软件资源丰富又会让你的计算机更受用户欢迎。

即使是苹果公司也几乎被打败。在 x86 芯片上投入大量的财力,最终,这些芯片被用于生产环境计算任务。

但 ARM 架构也并没有消失。基于 ARM 架构的芯片不仅运算速度快,同时也非常节能。因此诸如机顶盒、PDA、数码相机、MP3 播放器这些电子产品多数都会采用 ARM 架构的芯片,甚至在很多需要用电池或不配备大散热风扇的电子产品上,都可以见到 ARM 芯片的身影。

而 ARM 则脱离 Acorn 成为了一种特殊的商业模式,他们不生产实物芯片,仅仅是向芯片生产厂商出售相关的知识产权。

因此,这或多或少是 ARM 芯片被应用于如此之多的手机和平板电脑上的原因。当 Linux 被移植到这种架构的芯片上时,开源技术的大门就已经向它打开了,这才让我们今天得以在这些芯片上运行 web 爬虫程序。

服务器端的 ARM

诸如微软Cloudflare 这些大厂都在基础设施建设上花了重金,所以对于我们这些预算不高的用户来说,可以选择的余地并不多。

实际上,如果你的信用卡只够付每月数美元的 VPS 费用,一直以来只能考虑 Scaleway 这个高性价比的厂商。

但自从数个月前公有云巨头 AWS 推出了他们自研的 ARM 处理器 AWS Graviton 之后,选择似乎就丰富了一些。

我决定在其中选择一款 VPS 厂商,将它提供的 ARM 处理器和 x86 处理器作出对比。

深入了解

所以我们要对比的是什么指标呢?

Scaleway

Scaleway 自身的定位是“专为开发者设计”。我觉得这个定位很准确,对于开发和原型设计来说,Scaleway 提供的产品确实可以作为一个很好的沙盒环境。

Scaleway 提供了一个简洁的仪表盘页面,让用户可以快速地从主页进入 bash shell 界面。对于很多小企业、自由职业者或者技术顾问,如果想要运行 web 爬虫,这个产品毫无疑问是一个物美价廉的选择。

ARM 方面我们选择 ARM64-2GB 这一款服务器,每月只需要 3 欧元。它带有 4 个 Cavium ThunderX 核心,这是在 2014 年推出的第一款服务器级的 ARMv8 处理器。但现在看来它已经显得有点落后了,并逐渐被更新的 ThunderX2 取代。

x86 方面我们选择 1-S,每月的费用是 4 欧元。它拥有 2 个英特尔 Atom C3995 核心。英特尔的 Atom 系列处理器的特点是低功耗、单线程,最初是用在笔记本电脑上的,后来也被服务器所采用。

两者在处理器以外的条件都大致相同,都使用 2 GB 的内存、50 GB 的 SSD 存储以及 200 Mbit/s 的带宽。磁盘驱动器可能会有所不同,但由于我们运行的是 web 爬虫,基本都是在内存中完成操作,因此这方面的差异可以忽略不计。

为了避免我不能熟练使用包管理器的尴尬局面,两方的操作系统我都会选择使用 Debian 9。

Amazon Web Services(AWS)

当你还在注册 AWS 账号的时候,使用 Scaleway 的用户可能已经把提交信用卡信息、启动 VPS 实例、添加 sudo 用户、安装依赖包这一系列流程都完成了。AWS 的操作相对来说比较繁琐,甚至需要详细阅读手册才能知道你正在做什么。

当然这也是合理的,对于一些需求复杂或者特殊的企业用户,确实需要通过详细的配置来定制合适的使用方案。

我们所采用的 AWS Graviton 处理器是 AWS EC2( 弹性计算云 Elastic Compute Cloud )的一部分,我会以按需实例的方式来运行,这也是最贵但最简捷的方式。AWS 同时也提供竞价实例,这样可以用较低的价格运行实例,但实例的运行时间并不固定。如果实例需要长时间持续运行,还可以选择预留实例

看,AWS 就是这么复杂……

我们分别选择 a1.mediumt2.small 两种型号的实例进行对比,两者都带有 2GB 内存。这个时候问题来了,这里提到的 vCPU 又是什么?两种型号的不同之处就在于此。

对于 a1.medium 型号的实例,vCPU 是 AWS Graviton 芯片提供的单个计算核心。这个芯片由被亚马逊在 2015 收购的以色列厂商 Annapurna Labs 研发,是 AWS 独有的单线程 64 位 ARMv8 内核。它的按需价格为每小时 0.0255 美元。

而 t2.small 型号实例使用英特尔至强系列芯片,但我不确定具体是其中的哪一款。它每个核心有两个线程,但我们并不能用到整个核心,甚至整个线程。

我们能用到的只是“20% 的基准性能,可以使用 CPU 积分突破这个基准”。这可能有一定的原因,但我没有弄懂。它的按需价格是每小时 0.023 美元。

在镜像库中没有 Debian 发行版的镜像,因此我选择了 Ubuntu 18.04。

瘪四与大头蛋爬取 Moz 排行榜前 500 的网站

要测试这些 VPS 的 CPU 性能,就该使用爬虫了。一个方法是对几个网站在尽可能短的时间里发出尽可能多的请求,但这种操作不太礼貌,我的做法是只向大量网站发出少数几个请求。

为此,我编写了 beavis.py(瘪四)这个爬虫程序(致敬我最喜欢的物理学家和制片人 Mike Judge)。这个程序会将 Moz 上排行前 500 的网站都爬取 3 层的深度,并计算 “wood” 和 “ass” 这两个单词在 HTML 文件中出现的次数。(LCTT 译注:beavis(瘪四)和 butt-head(大头蛋) 都是 Mike Judge 的动画片《瘪四与大头蛋》中的角色)

但我实际爬取的网站可能不足 500 个,因为我需要遵循网站的 robot.txt 协定,另外还有些网站需要提交 javascript 请求,也不一定会计算在内。但这已经是一个足以让 CPU 保持繁忙的爬虫任务了。

Python 的全局解释器锁机制会让我的程序只能用到一个 CPU 线程。为了测试多线程的性能,我需要启动多个独立的爬虫程序进程。

因此我还编写了 butthead.py,尽管大头蛋很粗鲁,它也总是比瘪四要略胜一筹。

我将整个爬虫任务拆分为多个部分,这可能会对爬取到的链接数量有一点轻微的影响。但无论如何,每次爬取都会有所不同,我们要关注的是爬取了多少个页面,以及耗时多长。

在 ARM 服务器上安装 Scrapy

安装 Scrapy 的过程与芯片的不同架构没有太大的关系,都是安装 pip 和相关的依赖包之后,再使用 pip 来安装 Scrapy。

据我观察,在使用 ARM 的机器上使用 pip 安装 Scrapy 确实耗时要长一点,我估计是由于需要从源码编译为二进制文件。

在 Scrapy 安装结束后,就可以通过 shell 来查看它的工作状态了。

在 Scaleway 的 ARM 机器上,Scrapy 安装完成后会无法正常运行,这似乎和 service_identity 模块有关。这个现象也会在树莓派上出现,但在 AWS Graviton 上不会出现。

对于这个问题,可以用这个命令来解决:

sudo pip3 install service_identity --force --upgrade

接下来就可以开始对比了。

单线程爬虫

Scrapy 的官方文档建议将爬虫程序的 CPU 使用率控制在 80% 到 90% 之间,在真实操作中并不容易,尤其是对于我自己写的代码。根据我的观察,实际的 CPU 使用率变动情况是一开始非常繁忙,随后稍微下降,接着又再次升高。

在爬取任务的最后,也就是大部分目标网站都已经被爬取了的这个阶段,会持续数分钟的时间。这让人有点失望,因为在这个阶段当中,任务的运行时长只和网站的大小有比较直接的关系,并不能以之衡量 CPU 的性能。

所以这并不是一次严谨的基准测试,只是我通过自己写的爬虫程序来观察实际的现象。

下面我们来看看最终的结果。首先是 Scaleway 的机器:

机器种类耗时爬取页面数每小时爬取页面数每百万页面费用(欧元)
Scaleway ARM64-2GB108m 59.27s38,20521,032.6230.28527
Scaleway 1-S97m 44.067s39,47624,324.6480.33011

我使用了 top 工具来查看爬虫程序运行期间的 CPU 使用率。在任务刚开始的时候,两者的 CPU 使用率都达到了 100%,但 ThunderX 大部分时间都达到了 CPU 的极限,无法看出来 Atom 的性能会比 ThunderX 超出多少。

通过 top 工具,我还观察了它们的内存使用情况。随着爬取任务的进行,ARM 机器的内存使用率最终达到了 14.7%,而 x86 则最终是 15%。

从运行日志还可以看出来,当 CPU 使用率到达极限时,会有大量的超时页面产生,最终导致页面丢失。这也是合理出现的现象,因为 CPU 过于繁忙会无法完整地记录所有爬取到的页面。

如果仅仅是为了对比爬虫的速度,页面丢失并不是什么大问题。但在实际中,业务成果和爬虫数据的质量是息息相关的,因此必须为 CPU 留出一些用量,以防出现这种现象。

再来看看 AWS 这边:

机器种类耗时爬取页面数每小时爬取页面数每百万页面费用(美元)
a1.medium100m 39.900s41,29424,612.7251.03605
t2.small78m 53.171s41,20031,336.2860.73397

为了方便比较,对于在 AWS 上跑的爬虫,我记录的指标和 Scaleway 上一致,但似乎没有达到预期的效果。这里我没有使用 top,而是使用了 AWS 提供的控制台来监控 CPU 的使用情况,从监控结果来看,我的爬虫程序并没有完全用到这两款服务器所提供的所有性能。

a1.medium 型号的机器尤为如此,在任务开始阶段,它的 CPU 使用率达到了峰值 45%,但随后一直在 20% 到 30% 之间。

让我有点感到意外的是,这个程序在 ARM 处理器上的运行速度相当慢,但却远未达到 Graviton CPU 能力的极限,而在 Intel Atom 处理器上则可以在某些时候达到 CPU 能力的极限。它们运行的代码是完全相同的,处理器的不同架构可能导致了对代码的不同处理方式。

个中原因无论是由于处理器本身的特性,还是二进制文件的编译,又或者是两者皆有,对我来说都是一个黑盒般的存在。我认为,既然在 AWS 机器上没有达到 CPU 处理能力的极限,那么只有在 Scaleway 机器上跑出来的性能数据是可以作为参考的。

t2.small 型号的机器性能让人费解。CPU 利用率大概 20%,最高才达到 35%,是因为手册中说的“20% 的基准性能,可以使用 CPU 积分突破这个基准”吗?但在控制台中可以看到 CPU 积分并没有被消耗。

为了确认这一点,我安装了 stress 这个软件,然后运行了一段时间,这个时候发现居然可以把 CPU 使用率提高到 100% 了。

显然,我需要调整一下它们的配置文件。我将 CONCURRENT_REQUESTS 参数设置为 5000,将 REACTOR_THREADPOOL_MAXSIZE 参数设置为 120,将爬虫任务的负载调得更大。

机器种类耗时爬取页面数每小时爬取页面数每万页面费用(美元)
a1.medium46m 13.619s40,28352,285.0470.48771
t2.small41m7.619s36,24152,871.8570.43501
t2.small(无 CPU 积分)73m 8.133s34,29828,137.88910.81740

a1.medium 型号机器的 CPU 使用率在爬虫任务开始后 5 分钟飙升到了 100%,随后下降到 80% 并持续了 20 分钟,然后再次攀升到 96%,直到任务接近结束时再次下降。这大概就是我想要的效果了。

而 t2.small 型号机器在爬虫任务的前期就达到了 50%,并一直保持在这个水平直到任务接近结束。如果每个核心都有两个线程,那么 50% 的 CPU 使用率确实是单个线程可以达到的极限了。

现在我们看到它们的性能都差不多了。但至强处理器的线程持续跑满了 CPU,Graviton 处理器则只是有一段时间如此。可以认为 Graviton 略胜一筹。

然而,如果 CPU 积分耗尽了呢?这种情况下的对比可能更为公平。为了测试这种情况,我使用 stress 把所有的 CPU 积分用完,然后再次启动了爬虫任务。

在没有 CPU 积分的情况下,CPU 使用率在 27% 就到达极限不再上升了,同时又出现了丢失页面的现象。这么看来,它的性能比负载较低的时候更差。

多线程爬虫

将爬虫任务分散到不同的进程中,可以有效利用机器所提供的多个核心。

一开始,我将爬虫任务分布在 10 个不同的进程中并同时启动,结果发现比每个核心仅使用 1 个进程的时候还要慢。

经过尝试,我得到了一个比较好的方案。把爬虫任务分布在 10 个进程中,但每个核心只启动 1 个进程,在每个进程接近结束的时候,再从剩余的进程中选出 1 个进程启动起来。

如果还需要优化,还可以让运行时间越长的爬虫进程在启动顺序中排得越靠前,我也在尝试实现这个方法。

想要预估某个域名的页面量,一定程度上可以参考这个域名主页的链接数量。我用另一个程序来对这个数量进行了统计,然后按照降序排序。经过这样的预处理之后,只会额外增加 1 分钟左右的时间。

结果,爬虫运行的总耗时超过了两个小时!毕竟把链接最多的域名都堆在同一个进程中也存在一定的弊端。

针对这个问题,也可以通过调整各个进程爬取的域名数量来进行优化,又或者在排序之后再作一定的修改。不过这种优化可能有点复杂了。

因此,我还是用回了最初的方法,它的效果还是相当不错的:

机器种类耗时爬取页面数每小时爬取页面数每万页面费用(欧元)
Scaleway ARM64-2GB62m 10.078s36,15834,897.07190.17193
Scaleway 1-S60m 56.902s36,72536,153.55290.22128

毕竟,使用多个核心能够大大加快爬虫的速度。

我认为,如果让一个经验丰富的程序员来优化的话,一定能够更好地利用所有的计算核心。但对于开箱即用的 Scrapy 来说,想要提高性能,使用更快的线程似乎比使用更多核心要简单得多。

从数量来看,Atom 处理器在更短的时间内爬取到了更多的页面。但如果从性价比角度来看,ThunderX 又是稍稍领先的。不过总的来说差距不大。

爬取结果分析

在爬取了 38205 个页面之后,我们可以统计到在这些页面中 “ass” 出现了 24170435 次,而 “wood” 出现了 54368 次。

“wood” 的出现次数不少,但和 “ass” 比起来简直微不足道。

结论

从上面的数据来看,对于性能而言,CPU 的架构并没有它们的问世时间重要,2018 年生产的 AWS Graviton 是单线程情况下性能最佳的。

你当然可以说按核心来比,Xeon 仍然赢了。但是,你不但需要计算美元的变化,甚至还要计算线程数。

另外在性能方面 2017 年生产的 Atom 轻松击败了 2014 年生产的 ThunderX,而 ThunderX 则在性价比方面占优。当然,如果你使用 AWS 的机器的话,还是使用 Graviton 吧。

总之,ARM 架构的硬件是可以用来运行爬虫程序的,而且在性能和费用方面也相当有竞争力。

而这种差异是否足以让你将整个技术架构迁移到 ARM 上?这就是另一回事了。当然,如果你已经是 AWS 用户,并且你的代码有很强的可移植性,那么不妨尝试一下 a1 型号的实例。

希望 ARM 设备在不久的将来能够在公有云上大放异彩。

源代码

这是我第一次使用 Python 和 Scrapy 来做一个项目,所以我的代码写得可能不是很好,例如代码中使用全局变量就有点力不从心。

不过我仍然会在下面开源我的代码。

要运行这些代码,需要预先安装 Scrapy,并且需要 Moz 上排名前 500 的网站的 csv 文件。如果要运行 butthead.py,还需要安装 psutil 这个库。

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.crawler import CrawlerProcess

ass = 0
wood = 0
totalpages = 0

def getdomains():

  moz500file = open('top500.domains.05.18.csv')

  domains = []
  moz500csv = moz500file.readlines()

  del moz500csv[0]

  for csvline in moz500csv:
    leftquote = csvline.find('"')    
    rightquote = leftquote + csvline[leftquote + 1:].find('"')
    domains.append(csvline[leftquote + 1:rightquote])

  return domains

def getstartpages(domains):
  
  startpages = []
  
  for domain in domains:
    startpages.append('http://' + domain)
  
  return startpages
  
class AssWoodItem(scrapy.Item):
  ass = scrapy.Field()
  wood = scrapy.Field()
  url = scrapy.Field()
  
class AssWoodPipeline(object):
  def __init__(self):
    self.asswoodstats = []

  def process_item(self, item, spider):
    self.asswoodstats.append((item.get('url'), item.get('ass'), item.get('wood')))
    
  def close_spider(self, spider):
    asstally, woodtally = 0, 0
    
    for asswoodcount in self.asswoodstats:
      asstally += asswoodcount[1]
      woodtally += asswoodcount[2]
      
    global ass, wood, totalpages
    ass = asstally
    wood = woodtally
    totalpages = len(self.asswoodstats)

class BeavisSpider(CrawlSpider):
  name = "Beavis"
  allowed_domains = getdomains()
  start_urls = getstartpages(allowed_domains)
  #start_urls = [ 'http://medium.com' ]
  custom_settings = {
    'DEPTH_LIMIT': 3,
    'DOWNLOAD_DELAY': 3,
    'CONCURRENT_REQUESTS': 1500,
    'REACTOR_THREADPOOL_MAXSIZE': 60,
    'ITEM_PIPELINES': { '__main__.AssWoodPipeline': 10 },
    'LOG_LEVEL': 'INFO',
    'RETRY_ENABLED': False,
    'DOWNLOAD_TIMEOUT': 30,
    'COOKIES_ENABLED': False,
    'AJAXCRAWL_ENABLED': True
  }
    
  rules = ( Rule(LinkExtractor(), callback='parse_asswood'), )
  
  def parse_asswood(self, response):
    if isinstance(response, scrapy.http.TextResponse):
      item = AssWoodItem()
      item['ass'] = response.text.casefold().count('ass')
      item['wood'] = response.text.casefold().count('wood')
      item['url'] = response.url
      yield item


if __name__ == '__main__':

  process = CrawlerProcess({
      'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
  })

  process.crawl(BeavisSpider)
  process.start()

  print('Uhh, that was, like, ' + str(totalpages) + ' pages crawled.')
  print('Uh huhuhuhuh. It said ass ' + str(ass) + ' times.')
  print('Uh huhuhuhuh. It said wood ' + str(wood) + ' times.')

beavis.py

import scrapy, time, psutil
from scrapy.spiders import CrawlSpider, Rule, Spider
from scrapy.linkextractors import LinkExtractor
from scrapy.crawler import CrawlerProcess
from multiprocessing import Process, Queue, cpu_count

ass = 0
wood = 0
totalpages = 0
linkcounttuples =[]

def getdomains():

  moz500file = open('top500.domains.05.18.csv')

  domains = []
  moz500csv = moz500file.readlines()

  del moz500csv[0]

  for csvline in moz500csv:
    leftquote = csvline.find('"')    
    rightquote = leftquote + csvline[leftquote + 1:].find('"')
    domains.append(csvline[leftquote + 1:rightquote])

  return domains

def getstartpages(domains):
  
  startpages = []
  
  for domain in domains:
    startpages.append('http://' + domain)
  
  return startpages
  
class AssWoodItem(scrapy.Item):
  ass = scrapy.Field()
  wood = scrapy.Field()
  url = scrapy.Field()
  
class AssWoodPipeline(object):
  def __init__(self):
    self.asswoodstats = []

  def process_item(self, item, spider):
    self.asswoodstats.append((item.get('url'), item.get('ass'), item.get('wood')))
    
  def close_spider(self, spider):
    asstally, woodtally = 0, 0
    
    for asswoodcount in self.asswoodstats:
      asstally += asswoodcount[1]
      woodtally += asswoodcount[2]
      
    global ass, wood, totalpages
    ass = asstally
    wood = woodtally
    totalpages = len(self.asswoodstats)
          

class ButtheadSpider(CrawlSpider):
  name = "Butthead"
  custom_settings = {
    'DEPTH_LIMIT': 3,
    'DOWNLOAD_DELAY': 3,
    'CONCURRENT_REQUESTS': 250,
    'REACTOR_THREADPOOL_MAXSIZE': 30,
    'ITEM_PIPELINES': { '__main__.AssWoodPipeline': 10 },
    'LOG_LEVEL': 'INFO',
    'RETRY_ENABLED': False,
    'DOWNLOAD_TIMEOUT': 30,
    'COOKIES_ENABLED': False,
    'AJAXCRAWL_ENABLED': True
  }
    
  rules = ( Rule(LinkExtractor(), callback='parse_asswood'), )
  
  
  def parse_asswood(self, response):
    if isinstance(response, scrapy.http.TextResponse):
      item = AssWoodItem()
      item['ass'] = response.text.casefold().count('ass')
      item['wood'] = response.text.casefold().count('wood')
      item['url'] = response.url
      yield item

def startButthead(domainslist, urlslist, asswoodqueue):
  crawlprocess = CrawlerProcess({
      'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
  })

  crawlprocess.crawl(ButtheadSpider, allowed_domains = domainslist, start_urls = urlslist)
  crawlprocess.start()
  asswoodqueue.put( (ass, wood, totalpages) )
  

if __name__ == '__main__':
  asswoodqueue = Queue()
  domains=getdomains()
  startpages=getstartpages(domains)
  processlist =[]
  cores = cpu_count()
  
  for i in range(10):
    domainsublist = domains[i * 50:(i + 1) * 50]
    pagesublist = startpages[i * 50:(i + 1) * 50]
    p = Process(target = startButthead, args = (domainsublist, pagesublist, asswoodqueue))
    processlist.append(p)
  
  for i in range(cores):
    processlist[i].start()
    
  time.sleep(180)
  
  i = cores
  
  while i != 10:
    time.sleep(60)
    if psutil.cpu_percent() < 66.7:
      processlist[i].start()
      i += 1
  
  for i in range(10):
    processlist[i].join()
  
  for i in range(10):
    asswoodtuple = asswoodqueue.get()
    ass += asswoodtuple[0]
    wood += asswoodtuple[1]
    totalpages += asswoodtuple[2]

  print('Uhh, that was, like, ' + str(totalpages) + ' pages crawled.')
  print('Uh huhuhuhuh. It said ass ' + str(ass) + ' times.')
  print('Uh huhuhuhuh. It said wood ' + str(wood) + ' times.')  

butthead.py


via: https://blog.dxmtechsupport.com.au/speed-test-x86-vs-arm-for-web-crawling-in-python/

作者:James Mawson 选题:lujun9972 译者:HankChow 校对:wxy

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

了解一些技巧助你减少代码查错时间。

在周五的下午三点钟(为什么是这个时间?因为事情总会在周五下午三点钟发生),你收到一条通知,客户发现你的软件出现一个错误。在有了初步的怀疑后,你联系运维,查看你的软件日志以了解发生了什么,因为你记得收到过日志已经搬家了的通知。

结果这些日志被转移到了你获取不到的地方,但它们正在导入到一个网页应用中——所以到时候你可以用这个漂亮的应用来检索日志,但是,这个应用现在还没完成。这个应用预计会在几天内完成。我知道,你觉得这完全不切实际。然而并不是,日志或者日志消息似乎经常在错误的时间消失不见。在我们开始查错前,一个忠告:经常检查你的日志以确保它们还在你认为它们应该在的地方,并记录你认为它们应该记的东西。当你不注意的时候,这些东西往往会发生令人惊讶的变化。

好的,你找到了日志或者尝试了呼叫运维人员,而客户确实发现了一个错误。甚至你可能认为你已经知道错误在哪儿。

你立即打开你认为可能有问题的文件并开始查错。

1、先不要碰你的代码

阅读代码,你甚至可能会想到该阅读哪些部分。但是在开始搞乱你的代码前,请重现导致错误的调用并把它变成一个测试。这将是一个集成测试,因为你可能还有其他疑问,目前你还不能准确地知道问题在哪儿。

确保这个测试结果是失败的。这很重要,因为有时你的测试不能重现失败的调用,尤其是你使用了可以混淆测试的 web 或者其他框架。很多东西可能被存储在变量中,但遗憾的是,只通过观察测试,你在测试里调用的东西并不总是明显可见的。当我尝试着重现这个失败的调用时,我并不是说我要创建一个可以通过的测试,但是,好吧,我确实是创建了一个测试,但我不认为这特别不寻常。

从自己的错误中吸取教训。

2、编写错误的测试

现在,你有了一个失败的测试,或者可能是一个带有错误的测试,那么是时候解决问题了。但是在你开干之前,让我们先检查下调用栈,因为这样可以更轻松地解决问题。

调用栈包括你已经启动但尚未完成地所有任务。因此,比如你正在烤蛋糕并准备往面糊里加面粉,那你的调用栈将是:

  • 做蛋糕
  • 打面糊
  • 加面粉

你已经开始做蛋糕,开始打面糊,而你现在正在加面粉。往锅底抹油不在这个列表中,因为你已经完成了,而做糖霜不在这个列表上因为你还没开始做。

如果你对调用栈不清楚,我强烈建议你使用 Python Tutor,它能帮你在执行代码时观察调用栈。

现在,如果你的 Python 程序出现了错误, Python 解释器会帮你打印出当前调用栈。这意味着无论那一时刻程序在做什么,很明显错误发生在调用栈的底部。

3、始终先检查调用栈底部

在栈底你不仅能看到发生了哪个错误,而且通常可以在调用栈的最后一行发现问题。如果栈底对你没有帮助,而你的代码还没有经过代码分析,那么使用代码分析是非常有用的。我推荐 pylint 或者 flake8。通常情况下,它会指出我一直忽略的错误的地方。

如果错误看起来很迷惑,你下一步行动可能是用 Google 搜索它。如果你搜索的内容不包含你的代码的相关信息,如变量名、文件等,那你将获得更好的搜索结果。如果你使用的是 Python 3(你应该使用它),那么搜索内容包含 Python 3 是有帮助的,否则 Python 2 的解决方案往往会占据大多数。

很久以前,开发者需要在没有搜索引擎的帮助下解决问题。那是一段黑暗时光。充分利用你可以使用的所有工具。

不幸的是,有时候问题发生在更早阶段,但只有在调用栈底部执行的地方才显现出来。就像当蛋糕没有膨胀时,忘记加发酵粉的事才被发现。

那就该检查整个调用栈。问题更可能在你的代码而不是 Python 标准库或者第三方包,所以先检查调用栈内你的代码。另外,在你的代码中放置断点通常会更容易检查代码。在调用栈的代码中放置断点,然后看看周围是否如你预期。

“但是,玛丽,”我听到你说,“如果我有一个调用栈,那这些都是有帮助的,但我只有一个失败的测试。我该从哪里开始?”

pdb,一个 Python 调试器。

找到你代码里会被这个调用命中的地方。你应该能够找到至少一个这样的地方。在那里打上一个 pdb 的断点。

一句题外话

为什么不使用 print 语句呢?我曾经依赖于 print 语句。有时候,它们仍然很方便。但当我开始处理复杂的代码库,尤其是有网络调用的代码库,print 语句就变得太慢了。我最终在各种地方都加上了 print 语句,但我没法追踪它们的位置和原因,而且变得更复杂了。但是主要使用 pdb 还有一个更重要的原因。假设你添加一条 print 语句去发现错误问题,而且 print 语句必须早于错误出现的地方。但是,看看你放 print 语句的函数,你不知道你的代码是怎么执行到那个位置的。查看代码是寻找调用路径的好方法,但看你以前写的代码是恐怖的。是的,我会用 grep 处理我的代码库以寻找调用函数的地方,但这会变得乏味,而且搜索一个通用函数时并不能缩小搜索范围。pdb 就变得非常有用。

你遵循我的建议,打上 pdb 断点并运行你的测试。然而测试再次失败,但是没有任何一个断点被命中。留着你的断点,并运行测试套件中一个同这个失败的测试非常相似的测试。如果你有个不错的测试套件,你应该能够找到一个这样的测试。它会命中了你认为你的失败测试应该命中的代码。运行这个测试,然后当它运行到你的断点,按下 w 并检查调用栈。如果你不知道如何查看因为其他调用而变得混乱的调用栈,那么在调用栈的中间找到属于你的代码,并在堆栈中该代码的上一行放置一个断点。再试一次新的测试。如果仍然没命中断点,那么继续,向上追踪调用栈并找出你的调用在哪里脱轨了。如果你一直没有命中断点,最后到了追踪的顶部,那么恭喜你,你发现了问题:你的应用程序名称拼写错了。

没有经验,小白,一点都没有经验。

4、修改代码

如果你仍觉得迷惑,在你稍微改变了一些的地方尝试新的测试。你能让新的测试跑起来么?有什么是不同的呢?有什么是相同的呢?尝试改变一下别的东西。当你有了你的测试,以及可能也还有其它的测试,那就可以开始安全地修改代码了,确定是否可以缩小问题范围。记得从一个新提交开始解决问题,以便于可以轻松地撤销无效地更改。(这就是版本控制,如果你没有使用过版本控制,这将会改变你的生活。好吧,可能它只是让编码更容易。查阅“版本控制可视指南”,以了解更多。)

5、休息一下

尽管如此,当它不再感觉起来像一个有趣的挑战或者游戏而开始变得令人沮丧时,你最好的举措是脱离这个问题。休息一下。我强烈建议你去散步并尝试考虑别的事情。

6、把一切写下来

当你回来了,如果你没有突然受到启发,那就把你关于这个问题所知的每一个点信息写下来。这应该包括:

  • 真正造成问题的调用
  • 真正发生了什么,包括任何错误信息或者相关的日志信息
  • 你真正期望发生什么
  • 到目前为止,为了找出问题,你做了什么工作;以及解决问题中你发现的任何线索。

有时这里有很多信息,但相信我,从零碎中挖掘信息是很烦人。所以尽量简洁,但是要完整。

7、寻求帮助

我经常发现写下所有信息能够启迪我想到还没尝试过的东西。当然,有时候我在点击求助邮件(或表单)的提交按钮后立刻意识到问题是是什么。无论如何,当你在写下所有东西仍一无所获时,那就试试向他人发邮件求助。首先是你的同事或者其他参与你的项目的人,然后是该项目的邮件列表。不要害怕向人求助。大多数人都是友善和乐于助人的,我发现在 Python 社区里尤其如此。

Maria McKinley 已在 PyCascades 2019 演讲 代码查错,2 月 23-24,于西雅图。


via: https://opensource.com/article/19/2/steps-hunting-code-python-bugs

作者:Maria Mckinley 选题:lujun9972 译者:LazyWolfLin 校对:wxy

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

身兼教师、开发者、作家数职的 Peter Farrell 来讲述为什么使用 Python 来讲数学课会比传统方法更加好。

数学课一直都是很讨厌的一件事情,尤其对于在传统教学方法上吃过苦头的人(例如我)来说。传统教学方法强调的是死记硬背和理论知识,这种形式与学生们的现实世界似乎相去甚远。

Peter Farrell 作为一位 Python 开发者和数学教师,发现学生在数学课程中遇到了困难,于是决定尝试使用 Python 来帮助介绍数学概念。

Peter 的灵感来源于 Logo 语言之父 Seymour Papert,他的 Logo 语言现在还存在于 Python 的 Turtle 模块中。Logo 语言中的海龟形象让 Peter 喜欢上了 Python,并且进一步将 Python 应用到数学教学中。

Peter 在他的新书《 Python 数学奇遇记 Math Adventures with Python 》中分享了他的方法:“图文并茂地指导如何用代码探索数学”。因此我最近对他进行了一次采访,向他了解更多这方面的情况。

Don Watkins(LCTT 译注:本文作者): 你的教学背景是什么?

Peter Farrell: 我曾经当过八年的数学老师,之后又做了十年的数学私教。我还在当老师的时候,就阅读过 Papert 的 《 头脑风暴 Mindstorms 》并从中受到了启发,将 Logo 语言和海龟引入到了我所有的数学课上。

DW: 你为什么开始使用 Python 呢?

PF: 在我当家教的时候,需要教学一门枯燥刻板的数学课程,这是一个痛苦的过程。后来我引入了 Logo 语言和海龟,我的学生刚好是一个编程爱好者,他非常喜欢这样的方式。在接触到函数和实际的编程之后,他还提议改用 Python。尽管当时我还不了解 Python,但看起来好像和 Logo 语言差别不大,我就同意了。后来我甚至坚持在 Python 上一条道走到黑了!

我还曾经寻找过 3D 图形方面的软件包,用来模拟太阳系行星的运动轨迹,让学生们理解行星是如何在牛顿的万有引力定律作用下运动的。很多图形软件包都需要用到 C 语言编程或者其它一些很复杂的内容,后来我发现了一个叫做 VisualPython 的软件包,它非常方便使用。于是在那之后的几年里,我就一直在用 Vpython 这个软件包。

所以,我是在和学生一起学习数学的时候被介绍使用 Python 的。在那段时间里,他是我的编程老师,而我则是他的数学老师。

DW: 是什么让你对数学感兴趣?

PF: 我是通过传统的方法学习数学的,那时候都是用手写、用纸记、在黑板上计算。我擅长代数和几何,在大学的时候也接触过 Basic 和 Fortran 编程,但那个时候也没有从中获取到灵感。直到后来在从编程中收到了启发,编程可以让你将数学课上一些晦涩难懂的内容变得简单直观,也能让你轻松地绘图、调整、探索,进而发现更多乐趣。

DW: 是什么启发了你使用 Python 教学?

PF: 还是在我当家教的时候,我惊奇地发现可以通过循环来计算对同一个函数输入不同参数的结果。如果用人手计算,可能要花半个小时的时间,但计算机瞬间就完成了。在这样的基础上,我们只要将一些计算的过程抽象成一个函数,再对其进行一些适当的扩展,就可以让计算机来计算了。

DW: 你的教学方法如何帮助学生,特别是在数学上感觉吃力的学生?如何将 Python 编程和数学结合起来

PF: 很多学生,尤其是高中生,都认为通过手工计算和画图来解决问题的方式在当今已经没有必要了,我并不反对这样的观点。例如,使用 Excel 来处理数据确实应该算是办公室工作的基本技能。学习任何一种编程语言,对公司来说都是一项非常有价值的技能。因此,使用计算机计算确实是有实际意义的。

而使用代码来为数学课创造艺术,则是一项革命性的改变。例如,仅仅是把某个形状显示到屏幕上,就需要使用到数学,因为位置需要用 x-y 坐标去表示,而尺寸、颜色等等都是数字。如果想要移动或者更改某些内容,会需要用到变量。更特殊地,如果需要改变位置,就需要更有效的向量来实现。这样的最终结果是,类似向量、矩阵这些难以捉摸的空洞概念会转变成实打实有意义的数学工具。

那些看起来在数学上吃力的学生,或许只是不太容易接受“书本上的数学”。因为“书本上的数学”过分强调了死记硬背和循规蹈矩,而有些忽视了创造力和实际应用能力。有些学生其实擅长数学,但并不适应学校的教学方式。我的方法会让父母们看到他们的孩子通过代码画出了很多有趣的图形,然后说:“我从来不知道正弦和余弦还能这样用!”

DW: 你的教学方法是如何在学校里促进 STEM 教育的呢?

PF: 我喜欢将这几个学科统称为 STEM( 科学、技术、工程、数学 Science, Technology, Engineering and Mathematics ) 或 STEAM( 科学、技术、工程、艺术、数学 Science, Technology, Engineering, Art and Mathematics )。但作为数学工作者,我很不希望其中的 M 被忽视。我经常看到很多很小的孩子在 STEM 实验室里参与一些有趣的项目,这表明他们已经在接受科学、技术和工程方面的教育。与此同时,我发现数学方面的材料和项目却很少。因此,我和机电一体化领域的优秀教师 Ken Hawthorn 正在着手解决这个问题。

希望我的书能够帮助鼓励学生们在技术上有所创新,无论在形式上是切实的还是虚拟的。同时书中还有很多漂亮的图形,希望能够激励大家去体验编程的过程,并且应用到实际中来。我使用的软件(Python Processing)是免费的,在树莓派等系统上都可以轻松安装。因为我认为,个人或者学校的成本问题不应该成为学生进入 STEM 世界的门槛。

DW: 你有什么想要跟其他的数学老师分享?

PF: 如果数学教学机构决定要向学生教导数字推理、逻辑、分析、建模、几何、数据解释这些内容,那么它们应该承认,可以通过编程来实现这些目标。正如我上面所说的,我的教学方法是在尝试使传统枯燥的方法变得直观,我认为任何一位老师都可以做到这一点。他们只需要知道其中的本质做法,就可以使用代码来完成大量重复的工作了。

我的教学方法依赖于一些免费的图形软件,因此只需要知道在哪里找到这些软件包,以及如何使用这些软件包,就可以开始引导学生使用 21 世纪的技术来解决实际问题,将整个过程和结果可视化,并找到更多可以以此实现的模式。


via: https://opensource.com/article/19/1/hacking-math

作者:Don Watkins 选题:lujun9972 译者:HankChow 校对:wxy

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

在比较 Python 框架的系列文章的第三部分中,我们来了解 Tornado,它是为处理异步进程而构建的。

在这个由四部分组成的系列文章的前两篇中,我们介绍了 PyramidFlask Web 框架。我们已经构建了两次相同的应用程序,看到了一个完整的 DIY 框架和包含了更多功能的框架之间的异同。

现在让我们来看看另一个稍微不同的选择:Tornado 框架。Tornado 在很大程度上与 Flask 一样简单,但有一个主要区别:Tornado 是专门为处理异步进程而构建的。在我们本系列所构建的应用程序中,这种特殊的酱料(LCTT 译注:这里意思是 Tornado 的异步功能)在我们构建的 app 中并不是非常有用,但我们将看到在哪里可以使用它,以及它在更一般的情况下是如何工作的。

让我们继续前两篇文章中模式,首先从处理设置和配置开始。

Tornado 启动和配置

如果你一直关注这个系列,那么第一步应该对你来说习以为常。

$ mkdir tornado_todo
$ cd tornado_todo
$ pipenv install --python 3.6
$ pipenv shell
(tornado-someHash) $ pipenv install tornado

创建一个 setup.py 文件来安装我们的应用程序相关的东西:

(tornado-someHash) $ touch setup.py
# setup.py
from setuptools import setup, find_packages

requires = [
    'tornado',
    'tornado-sqlalchemy',
    'psycopg2',
]

setup(
    name='tornado_todo',
    version='0.0',
    description='A To-Do List built with Tornado',
    author='<Your name>',
    author_email='<Your email>',
    keywords='web tornado',
    packages=find_packages(),
    install_requires=requires,
    entry_points={
        'console_scripts': [
            'serve_app = todo:main',
        ],
    },
)

因为 Tornado 不需要任何外部配置,所以我们可以直接编写 Python 代码来让程序运行。让我们创建 todo 目录,并用需要的前几个文件填充它。

todo/
    __init__.py
    models.py
    views.py

就像 Flask 和 Pyramid 一样,Tornado 也有一些基本配置,放在 __init__.py 中。从 tornado.web 中,我们将导入 Application 对象,它将处理路由和视图的连接,包括数据库(当我们谈到那里时再说)以及运行 Tornado 应用程序所需的其它额外设置。

# __init__.py
from tornado.web import Application

def main():
    """Construct and serve the tornado application."""
    app = Application()

像 Flask 一样,Tornado 主要是一个 DIY 框架。当构建我们的 app 时,我们必须设置该应用实例。因为 Tornado 用它自己的 HTTP 服务器来提供该应用,我们必须设置如何提供该应用。首先,在 tornado.options.define 中定义要监听的端口。然后我们实例化 Tornado 的 HTTPServer,将该 Application 对象的实例作为参数传递给它。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.options import define, options
from tornado.web import Application

define('port', default=8888, help='port to listen on')

def main():
    """Construct and serve the tornado application."""
    app = Application()
    http_server = HTTPServer(app)
    http_server.listen(options.port)

当我们使用 define 函数时,我们最终会在 options 对象上创建属性。第一个参数位置的任何内容都将是属性的名称,分配给 default 关键字参数的内容将是该属性的值。

例如,如果我们将属性命名为 potato 而不是 port,我们可以通过 options.potato 访问它的值。

HTTPServer 上调用 listen 并不会启动服务器。我们必须再做一步,找一个可以监听请求并返回响应的工作应用程序,我们需要一个输入输出循环。幸运的是,Tornado 以 tornado.ioloop.IOLoop 的形式提供了开箱即用的功能。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application

define('port', default=8888, help='port to listen on')

def main():
    """Construct and serve the tornado application."""
    app = Application()
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

我喜欢某种形式的 print 语句,来告诉我什么时候应用程序正在提供服务,这是我的习惯。如果你愿意,可以不使用 print

我们以 IOLoop.current().start() 开始我们的 I/O 循环。让我们进一步讨论输入,输出和异步性。

Python 中的异步和 I/O 循环的基础知识

请允许我提前说明,我绝对,肯定,一定并且放心地说不是异步编程方面的专家。就像我写的所有内容一样,接下来的内容源于我对这个概念的理解的局限性。因为我是人,可能有很深很深的缺陷。

异步程序的主要问题是:

* 数据如何进来?
* 数据如何出去?
* 什么时候可以在不占用我全部注意力情况下运行某个过程?

由于全局解释器锁(GIL),Python 被设计为一种单线程)语言。对于 Python 程序必须执行的每个任务,其线程执行的全部注意力都集中在该任务的持续时间内。我们的 HTTP 服务器是用 Python 编写的,因此,当接收到数据(如 HTTP 请求)时,服务器的唯一关心的是传入的数据。这意味着,在大多数情况下,无论是程序需要运行还是处理数据,程序都将完全消耗服务器的执行线程,阻止接收其它可能的数据,直到服务器完成它需要做的事情。

在许多情况下,这不是太成问题。典型的 Web 请求,响应周期只需要几分之一秒。除此之外,构建 HTTP 服务器的套接字可以维护待处理的传入请求的积压。因此,如果请求在该套接字处理其它内容时进入,则它很可能只是在处理之前稍微排队等待一会。对于低到中等流量的站点,几分之一秒的时间并不是什么大问题,你可以使用多个部署的实例以及 NGINX 等负载均衡器来为更大的请求负载分配流量。

但是,如果你的平均响应时间超过一秒钟,该怎么办?如果你使用来自传入请求的数据来启动一些长时间的过程(如机器学习算法或某些海量数据库查询),该怎么办?现在,你的单线程 Web 服务器开始累积一个无法寻址的积压请求,其中一些请求会因为超时而被丢弃。这不是一种选择,特别是如果你希望你的服务在一段时间内是可靠的。

异步 Python 程序登场。重要的是要记住因为它是用 Python 编写的,所以程序仍然是一个单线程进程。除非特别标记,否则在异步程序中仍然会阻塞执行。

但是,当异步程序结构正确时,只要你指定某个函数应该具有这样的能力,你的异步 Python 程序就可以“搁置”长时间运行的任务。然后,当搁置的任务完成并准备好恢复时,异步控制器会收到报告,只要在需要时管理它们的执行,而不会完全阻塞对新输入的处理。

这有点夸张,所以让我们用一个人类的例子来证明。

带回家吧

我经常发现自己在家里试图完成很多家务,但没有多少时间来做它们。在某一天,积压的家务可能看起来像:

* 做饭(20 分钟准备,40 分钟烹饪)
* 洗碗(60 分钟)
* 洗涤并擦干衣物(30 分钟洗涤,每次干燥 90 分钟)
* 真空清洗地板(30 分钟)

如果我是一个传统的同步程序,我会亲自完成每项任务。在我考虑处理任何其他事情之前,每项任务都需要我全神贯注地完成。因为如果没有我的全力关注,什么事情都完成不了。所以我的执行顺序可能如下:

1. 完全专注于准备和烹饪食物,包括等待食物烹饪(60 分钟)
2. 将脏盘子移到水槽中(65 分钟过去了)
3. 清洗所有盘子(125 分钟过去了)
4. 开始完全专注于洗衣服,包括等待洗衣机洗完,然后将衣物转移到烘干机,再等烘干机完成( 250 分钟过去了)
5. 对地板进行真空吸尘(280 分钟了)

从头到尾完成所有事情花费了 4 小时 40 分钟。

我应该像异步程序一样聪明地工作,而不是努力工作。我的家里到处都是可以为我工作的机器,而不用我一直努力工作。同时,现在我可以将注意力转移真正需要的东西上。

我的执行顺序可能看起来像:

1. 将衣物放入洗衣机并启动它(5 分钟)
2. 在洗衣机运行时,准备食物(25 分钟过去了)
3. 准备好食物后,开始烹饪食物(30 分钟过去了)
4. 在烹饪食物时,将衣物从洗衣机移到烘干机机中开始烘干(35 分钟过去了)
5. 当烘干机运行中,且食物仍在烹饪时,对地板进行真空吸尘(65 分钟过去了)
6. 吸尘后,将食物从炉子中取出并装盘子入洗碗机(70 分钟过去了)
7. 运行洗碗机(130 分钟完成)

现在花费的时间下降到 2 小时 10 分钟。即使我允许在作业之间切换花费更多时间(总共 10-20 分钟)。如果我等待着按顺序执行每项任务,我花费的时间仍然只有一半左右。这就是将程序构造为异步的强大功能。

那么 I/O 循环在哪里?

一个异步 Python 程序的工作方式是从某个外部源(输入)获取数据,如果某个进程需要,则将该数据转移到某个外部工作者(输出)进行处理。当外部进程完成时,Python 主程序会收到提醒,然后程序获取外部处理(输入)的结果,并继续这样其乐融融的方式。

当数据不在 Python 主程序手中时,主程序就会被释放来处理其它任何事情。包括等待全新的输入(如 HTTP 请求)和处理长时间运行的进程的结果(如机器学习算法的结果,长时间运行的数据库查询)。主程序虽仍然是单线程的,但成了事件驱动的,它对程序处理的特定事件会触发动作。监听这些事件并指示应如何处理它们的主要是 I/O 循环在工作。

我知道,我们走了很长的路才得到这个重要的解释,但我希望在这里传达的是,它不是魔术,也不是某种复杂的并行处理或多线程工作。全局解释器锁仍然存在,主程序中任何长时间运行的进程仍然会阻塞其它任何事情的进行,该程序仍然是单线程的。然而,通过将繁琐的工作外部化,我们可以将线程的注意力集中在它需要注意的地方。

这有点像我上面的异步任务。当我的注意力完全集中在准备食物上时,它就是我所能做的一切。然而,当我能让炉子帮我做饭,洗碗机帮我洗碗,洗衣机和烘干机帮我洗衣服时,我的注意力就会被释放出来,去做其它事情。当我被提醒,我的一个长时间运行的任务已经完成并准备再次处理时,如果我的注意力是空闲的,我可以获取该任务的结果,并对其做下一步需要做的任何事情。

Tornado 路由和视图

尽管经历了在 Python 中讨论异步的所有麻烦,我们还是决定暂不使用它。先来编写一个基本的 Tornado 视图。

与我们在 Flask 和 Pyramid 实现中看到的基于函数的视图不同,Tornado 的视图都是基于类的。这意味着我们将不在使用单独的、独立的函数来规定如何处理请求。相反,传入的 HTTP 请求将被捕获并将其分配为我们定义的类的一个属性。然后,它的方法将处理相应的请求类型。

让我们从一个基本的视图开始,即在屏幕上打印 “Hello, World”。我们为 Tornado 应用程序构造的每个基于类的视图都必须继承 tornado.web 中的 RequestHandler 对象。这将设置我们需要(但不想写)的所有底层逻辑来接收请求,同时构造正确格式的 HTTP 响应。

from tornado.web import RequestHandler

class HelloWorld(RequestHandler):
    """Print 'Hello, world!' as the response body."""

    def get(self):
        """Handle a GET request for saying Hello World!."""
        self.write("Hello, world!")

因为我们要处理 GET 请求,所以我们声明(实际上是重写)了 get 方法。我们提供文本或 JSON 可序列化对象,用 self.write 写入响应体。之后,我们让 RequestHandler 来做在发送响应之前必须完成的其它工作。

就目前而言,此视图与 Tornado 应用程序本身并没有实际连接。我们必须回到 __init__.py,并稍微更新 main 函数。以下是新的内容:

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import HelloWorld

define('port', default=8888, help='port to listen on')

def main():
    """Construct and serve the tornado application."""
    app = Application([
        ('/', HelloWorld)
    ])
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

我们做了什么

我们将 views.py 文件中的 HelloWorld 视图导入到脚本 __init__.py 的顶部。然后我们添加了一个路由-视图对应的列表,作为 Application 实例化的第一个参数。每当我们想要在应用程序中声明一个路由时,它必须绑定到一个视图。如果需要,可以对多个路由使用相同的视图,但每个路由必须有一个视图。

我们可以通过在 setup.py 中启用的 serve_app 命令来运行应用程序,从而确保这一切都能正常工作。查看 http://localhost:8888/ 并看到它显示 “Hello, world!”。

当然,在这个领域中我们还能做更多,也将做更多,但现在让我们来讨论模型吧。

连接数据库

如果我们想要保留数据,就需要连接数据库。与 Flask 一样,我们将使用一个特定于框架的 SQLAchemy 变体,名为 tornado-sqlalchemy

为什么要使用它而不是 SQLAlchemy 呢?好吧,其实 tornado-sqlalchemy 具有简单 SQLAlchemy 的所有优点,因此我们仍然可以使用通用的 Base 声明模型,并使用我们习以为常的所有列数据类型和关系。除了我们已经惯常了解到的,tornado-sqlalchemy 还为其数据库查询功能提供了一种可访问的异步模式,专门用于与 Tornado 现有的 I/O 循环一起工作。

我们通过将 tornado-sqlalchemypsycopg2 添加到 setup.py 到所需包的列表并重新安装包来创建环境。在 models.py 中,我们声明了模型。这一步看起来与我们在 Flask 和 Pyramid 中已经看到的完全一样,所以我将跳过全部声明,只列出了 Task 模型的必要部分。

# 这不是完整的 models.py, 但是足够看到不同点
from tornado_sqlalchemy import declarative_base

Base = declarative_base

class Task(Base):
    # 等等,因为剩下的几乎所有的东西都一样 ...

我们仍然需要将 tornado-sqlalchemy 连接到实际应用程序。在 __init__.py 中,我们将定义数据库并将其集成到应用程序中。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import HelloWorld

# add these
import os
from tornado_sqlalchemy import make_session_factory

define('port', default=8888, help='port to listen on')
factory = make_session_factory(os.environ.get('DATABASE_URL', ''))

def main():
    """Construct and serve the tornado application."""
    app = Application([
        ('/', HelloWorld)
    ],
        session_factory=factory
    )
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

就像我们在 Pyramid 中传递的会话工厂一样,我们可以使用 make_session_factory 来接收数据库 URL 并生成一个对象,这个对象的唯一目的是为视图提供到数据库的连接。然后我们将新创建的 factory 传递给 Application 对象,并使用 session_factory 关键字参数将它绑定到应用程序中。

最后,初始化和管理数据库与 Flask 和 Pyramid 相同(即,单独的 DB 管理脚本,与 Base 对象一起工作等)。它看起来很相似,所以在这里我就不介绍了。

回顾视图

Hello,World 总是适合学习基础知识,但我们需要一些真实的,特定应用程序的视图。

让我们从 info 视图开始。

# views.py
import json
from tornado.web import RequestHandler

class InfoView(RequestHandler):
    """只允许 GET 请求"""
    SUPPORTED_METHODS = ["GET"]

    def set_default_headers(self):
        """设置默认响应头为 json 格式的"""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def get(self):
        """列出这个 API 的路由"""
        routes = {
            '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>'
        }
        self.write(json.dumps(routes))

有什么改变吗?让我们从上往下看。

我们添加了 SUPPORTED_METHODS 类属性,它是一个可迭代对象,代表这个视图所接受的请求方法,其他任何方法都将返回一个 405 状态码。当我们创建 HelloWorld 视图时,我们没有指定它,主要是当时有点懒。如果没有这个类属性,此视图将响应任何试图绑定到该视图的路由的请求。

我们声明了 set_default_headers 方法,它设置 HTTP 响应的默认头。我们在这里声明它,以确保我们返回的任何响应都有一个 "Content-Type""application/json" 类型。

我们将 json.dumps(some_object) 添加到 self.write 的参数中,因为它可以很容易地构建响应主体的内容。

现在已经完成了,我们可以继续将它连接到 __init__.py 中的主路由。

# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import InfoView

# 添加这些
import os
from tornado_sqlalchemy import make_session_factory

define('port', default=8888, help='port to listen on')
factory = make_session_factory(os.environ.get('DATABASE_URL', ''))

def main():
    """Construct and serve the tornado application."""
    app = Application([
        ('/', InfoView)
    ],
        session_factory=factory
    )
    http_server = HTTPServer(app)
    http_server.listen(options.port)
    print('Listening on http://localhost:%i' % options.port)
    IOLoop.current().start()

我们知道,还需要编写更多的视图和路由。每个都会根据需要放入 Application 路由列表中,每个视图还需要一个 set_default_headers 方法。在此基础上,我们还将创建 send_response 方法,它的作用是将响应与我们想要给响应设置的任何自定义状态码打包在一起。由于每个视图都需要这两个方法,因此我们可以创建一个包含它们的基类,这样每个视图都可以继承基类。这样,我们只需要编写一次。

# views.py
import json
from tornado.web import RequestHandler

class BaseView(RequestHandler):
    """Base view for this application."""

    def set_default_headers(self):
        """Set the default response header to be JSON."""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def send_response(self, data, status=200):
        """Construct and send a JSON response with appropriate status code."""
        self.set_status(status)
        self.write(json.dumps(data))

对于我们即将编写的 TaskListView 这样的视图,我们还需要一个到数据库的连接。我们需要 tornado_sqlalchemy 中的 SessionMixin 在每个视图类中添加一个数据库会话。我们可以将它放在 BaseView 中,这样,默认情况下,从它继承的每个视图都可以访问数据库会话。

# views.py
import json
from tornado_sqlalchemy import SessionMixin
from tornado.web import RequestHandler

class BaseView(RequestHandler, SessionMixin):
    """Base view for this application."""

    def set_default_headers(self):
        """Set the default response header to be JSON."""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def send_response(self, data, status=200):
        """Construct and send a JSON response with appropriate status code."""
        self.set_status(status)
        self.write(json.dumps(data))

只要我们修改 BaseView 对象,在将数据发布到这个 API 时,我们就应该定位到这里。

当 Tornado(从 v.4.5 开始)使用来自客户端的数据并将其组织起来到应用程序中使用时,它会将所有传入数据视为字节串。但是,这里的所有代码都假设使用 Python 3,因此我们希望使用的唯一字符串是 Unicode 字符串。我们可以为这个 BaseView 类添加另一个方法,它的工作是将输入数据转换为 Unicode,然后再在视图的其他地方使用。

如果我们想要在正确的视图方法中使用它之前转换这些数据,我们可以重写视图类的原生 prepare 方法。它的工作是在视图方法运行前运行。如果我们重写 prepare 方法,我们可以设置一些逻辑来运行,每当收到请求时,这些逻辑就会执行字节串到 Unicode 的转换。

# views.py
import json
from tornado_sqlalchemy import SessionMixin
from tornado.web import RequestHandler

class BaseView(RequestHandler, SessionMixin):
    """Base view for this application."""

    def prepare(self):
        self.form_data = {
            key: [val.decode('utf8') for val in val_list]
            for key, val_list in self.request.arguments.items()
        }

    def set_default_headers(self):
        """Set the default response header to be JSON."""
        self.set_header("Content-Type", 'application/json; charset="utf-8"')

    def send_response(self, data, status=200):
        """Construct and send a JSON response with appropriate status code."""
        self.set_status(status)
        self.write(json.dumps(data))

如果有任何数据进入,它将在 self.request.arguments 字典中找到。我们可以通过键访问该数据库,并将其内容(始终是列表)转换为 Unicode。因为这是基于类的视图而不是基于函数的,所以我们可以将修改后的数据存储为一个实例属性,以便以后使用。我在这里称它为 form_data,但它也可以被称为 potato。关键是我们可以存储提交给应用程序的数据。

异步视图方法

现在我们已经构建了 BaseaView,我们可以构建 TaskListView 了,它会继承 BaseaView

正如你可以从章节标题中看到的那样,以下是所有关于异步性的讨论。TaskListView 将处理返回任务列表的 GET 请求和用户给定一些表单数据来创建新任务的 POST 请求。让我们首先来看看处理 GET 请求的代码。

# all the previous imports
import datetime
from tornado.gen import coroutine
from tornado_sqlalchemy import as_future
from todo.models import Profile, Task

# the BaseView is above here
class TaskListView(BaseView):
    """View for reading and adding new tasks."""
    SUPPORTED_METHODS = ("GET", "POST",)

    @coroutine
    def get(self, username):
        """Get all tasks for an existing user."""
        with self.make_session() as session:
            profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)
            if profile:
                tasks = [task.to_dict() for task in profile.tasks]
                self.send_response({
                    'username': profile.username,
                    'tasks': tasks
                })

这里的第一个主要部分是 @coroutine 装饰器,它从 tornado.gen 导入。任何具有与调用堆栈的正常流程不同步的 Python 可调用部分实际上是“协程”,即一个可以与其它协程一起运行的协程。在我的家务劳动的例子中,几乎所有的家务活都是一个共同的例行协程。有些阻止了例行协程(例如,给地板吸尘),但这种例行协程只会阻碍我开始或关心其它任何事情的能力。它没有阻止已经启动的任何其他协程继续进行。

Tornado 提供了许多方法来构建一个利用协程的应用程序,包括允许我们设置函数调用锁,同步异步协程的条件,以及手动修改控制 I/O 循环的事件系统。

这里使用 @coroutine 装饰器的唯一条件是允许 get 方法将 SQL 查询作为后台进程,并在查询完成后恢复,同时不阻止 Tornado I/O 循环去处理其他传入的数据源。这就是关于此实现的所有“异步”:带外数据库查询。显然,如果我们想要展示异步 Web 应用程序的魔力和神奇,那么一个任务列表就不是好的展示方式。

但是,这就是我们正在构建的,所以让我们来看看方法如何利用 @coroutine 装饰器。SessionMixin 混合到 BaseView 声明中,为我们的视图类添加了两个方便的,支持数据库的属性:sessionmake_session。它们的名字相似,实现的目标也相当相似。

self.session 属性是一个关注数据库的会话。在请求-响应周期结束时,在视图将响应发送回客户端之前,任何对数据库的更改都被提交,并关闭会话。

self.make_session 是一个上下文管理器和生成器,可以动态构建和返回一个全新的会话对象。第一个 self.session 对象仍然存在。无论如何,反正 make_session 会创建一个新的。make_session 生成器还为其自身提供了一个功能,用于在其上下文(即缩进级别)结束时提交和关闭它创建的会话。

如果你查看源代码,则赋值给 self.session 的对象类型与 self.make_session 生成的对象类型之间没有区别,不同之处在于它们是如何被管理的。

使用 make_session 上下文管理器,生成的会话仅属于上下文,在该上下文中开始和结束。你可以使用 make_session 上下文管理器在同一个视图中打开,修改,提交以及关闭多个数据库会话。

self.session 要简单得多,当你进入视图方法时会话已经打开,在响应被发送回客户端之前会话就已提交。

虽然读取文档片段PyPI 示例都说明了上下文管理器的使用,但是没有说明 self.session 对象或由 self.make_session 生成的 session 本质上是不是异步的。当我们启动查询时,我们开始考虑内置于 tornado-sqlalchemy 中的异步行为。

tornado-sqlalchemy 包为我们提供了 as_future 函数。它的工作是装饰 tornado-sqlalchemy 会话构造的查询并 yield 其返回值。如果视图方法用 @coroutine 装饰,那么使用 yield as_future(query) 模式将使封装的查询成为一个异步后台进程。I/O 循环会接管等待查询的返回值和 as_future 创建的 future 对象的解析。

要访问 as_future(query) 的结果,你必须从它 yield。否则,你只能获得一个未解析的生成器对象,并且无法对查询执行任何操作。

这个视图方法中的其他所有内容都与之前课堂上的类似,与我们在 Flask 和 Pyramid 中看到的内容类似。

post 方法看起来非常相似。为了保持一致性,让我们看一下 post 方法以及它如何处理用 BaseView 构造的 self.form_data

@coroutine
def post(self, username):
    """Create a new task."""
    with self.make_session() as session:
        profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)
        if profile:
            due_date = self.form_data['due_date'][0]
            task = Task(
                name=self.form_data['name'][0],
                note=self.form_data['note'][0],
                creation_date=datetime.now(),
                due_date=datetime.strptime(due_date, '%d/%m/%Y %H:%M:%S') if due_date else None,
                completed=self.form_data['completed'][0],
                profile_id=profile.id,
                profile=profile
            )
            session.add(task)
            self.send_response({'msg': 'posted'}, status=201)

正如我所说,这是我们所期望的:

* 与我们在 get 方法中看到的查询模式相同 * 构造一个新的 Task 对象的实例,用 form_data 的数据填充 * 添加新的 Task 对象(但不提交,因为它由上下文管理器处理!)到数据库会话 * 将响应发送给客户端

这样我们就有了 Tornado web 应用程序的基础。其他内容(例如,数据库管理和更多完整应用程序的视图)实际上与我们在 Flask 和 Pyramid 应用程序中看到的相同。

关于使用合适的工具完成合适的工作的一点想法

在我们继续浏览这些 Web 框架时,我们开始看到它们都可以有效地处理相同的问题。对于像这样的待办事项列表,任何框架都可以完成这项任务。但是,有些 Web 框架比其它框架更适合某些工作,这具体取决于对你来说什么“更合适”和你的需求。

虽然 Tornado 显然和 Pyramid 或 Flask 一样可以处理相同工作,但将它用于这样的应用程序实际上是一种浪费,这就像开车从家走一个街区(LCTT 译注:这里意思应该是从家开始走一个街区只需步行即可)。是的,它可以完成“旅行”的工作,但短途旅行不是你选择汽车而不是自行车或者使用双脚的原因。

根据文档,Tornado 被称为 “Python Web 框架和异步网络库”。在 Python Web 框架生态系统中很少有人喜欢它。如果你尝试完成的工作需要(或将从中获益)以任何方式、形状或形式的异步性,使用 Tornado。如果你的应用程序需要处理多个长期连接,同时又不想牺牲太多性能,选择 Tornado。如果你的应用程序是多个应用程序,并且需要线程感知以准确处理数据,使用 Tornado。这是它最有效的地方。

用你的汽车做“汽车的事情”,使用其他交通工具做其他事情。

向前看,进行一些深度检查

谈到使用合适的工具来完成合适的工作,在选择框架时,请记住应用程序的范围和规模,包括现在和未来。到目前为止,我们只研究了适用于中小型 Web 应用程序的框架。本系列的下一篇也是最后一篇将介绍最受欢迎的 Python 框架之一 Django,它适用于可能会变得更大的大型应用程序。同样,尽管它在技术上能够并且将会处理待办事项列表问题,但请记住,这不是它的真正用途。我们仍然会通过它来展示如何使用它来构建应用程序,但我们必须牢记框架的意图以及它是如何反映在架构中的:

  • Flask: 适用于小型,简单的项目。它可以使我们轻松地构建视图并将它们快速连接到路由,它可以简单地封装在一个文件中。
  • Pyramid: 适用于可能增长的项目。它包含一些配置来启动和运行。应用程序组件的独立领域可以很容易地划分并构建到任意深度,而不会忽略中央应用程序。
  • Tornado: 适用于受益于精确和有意识的 I/O 控制的项目。它允许协程,并轻松公开可以控制如何接收请求或发送响应以及何时发生这些操作的方法。
  • Django:(我们将会看到)意味着可能会变得更大的东西。它有着非常庞大的生态系统,包括大量插件和模块。它非常有主见的配置和管理,以保持所有不同部分在同一条线上。

无论你是从本系列的第一篇文章开始阅读,还是稍后才加入的,都要感谢阅读!请随意留下问题或意见。下次再见时,我手里会拿着 Django。

感谢 Python BDFL

我必须把功劳归于它应得的地方,非常感谢 Guido van Rossum,不仅仅是因为他创造了我最喜欢的编程语言。

PyCascades 2018 期间,我很幸运的不仅做了基于这个文章系列的演讲,而且还被邀请参加了演讲者的晚宴。整个晚上我都坐在 Guido 旁边,不停地问他问题。其中一个问题是,在 Python 中异步到底是如何工作的,但他没有一点大惊小怪,而是花时间向我解释,让我开始理解这个概念。他后来推特给我发了一条消息:是用于学习异步 Python 的广阔资源。我随后在三个月内阅读了三次,然后写了这篇文章。你真是一个非常棒的人,Guido!


via: https://opensource.com/article/18/6/tornado-framework

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

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