分类 软件开发 下的文章

用一个简单的 AWK 程序跟踪你的同事喝咖啡的欠款。

以下基于一个真实的故事,虽然一些名字和细节有所改变。

很久以前,在一个遥远的地方,有一间(划掉)办公室。由于各种原因,这个办公室没有购买速溶咖啡。所以那个办公室的一些人聚在一起决定建立“咖啡角”。

咖啡角的一名成员会购买一些速溶咖啡,而其他成员会付给他钱。有人喝咖啡比其他人多,所以增加了“半成员”的级别:半成员每周允许喝的咖啡限量,并可以支付其它成员支付的一半。

管理这事非常操心。而我刚读过《Unix 编程环境》这本书,想练习一下我的 AWK 编程技能,所以我自告奋勇创建了一个系统。

第 1 步:我用一个数据库来记录成员及其应支付给咖啡角的欠款。我是以 AWK 便于处理的格式记录的,其中字段用冒号分隔:

member:john:1:22
member:jane:0.5:33
member:pratyush:0.5:17
member:jing:1:27

上面的第一个字段标识了这是哪一种行(member)。第二个字段是成员的名字(即他们的电子邮件用户名,但没有 @ )。下一个字段是其成员级别(成员 = 1,或半会员 = 0.5)。最后一个字段是他们欠咖啡角的钱。正数表示他们欠咖啡角钱,负数表示咖啡角欠他们。

第 2 步:我记录了咖啡角的收入和支出:

payment:jane:33
payment:pratyush:17
bought:john:60
payback:john:50

Jane 付款 $33,Pratyush 付款 $17,John 买了价值 $60 的咖啡,而咖啡角还款给 John $50。

第 3 步:我准备写一些代码,用来处理成员和付款,并生成记录了新欠账的更新的成员文件。

#!/usr/bin/env --split-string=awk -F: -f

释伴行(#!)需要做一些调整,我使用 env 命令来允许从释伴行传递多个参数:具体来说,AWK 的 -F 命令行参数会告诉它字段分隔符是什么。

AWK 程序就是一个规则序列(也可以包含函数定义,但是对于这个咖啡角应用来说不需要)

第一条规则读取该成员文件。当我运行该命令时,我总是首先给它的是成员文件,然后是付款文件。它使用 AWK 关联数组来在 members 数组中记录成员级别,以及在 debt 数组中记录当前欠账。

$1 == "member" {
   members[$2]=$3
   debt[$2]=$4
   total_members += $3
}

第二条规则在记录付款(payment)时减少欠账。

$1 == "payment" {
   debt[$2] -= $3
}

还款(payback)则相反:它增加欠账。这可以优雅地支持意外地给了某人太多钱的情况。

$1 == "payback" {
   debt[$2] += $3
}

最复杂的部分出现在有人购买(bought)速溶咖啡供咖啡角使用时。它被视为付款(payment),并且该人的债务减少了适当的金额。接下来,它计算每个会员的费用。它根据成员的级别对所有成员进行迭代并增加欠款

$1 == "bought" {
   debt[$2] -= $3
   per_member = $3/total_members
   for (x in members) {
       debt[x] += per_member * members[x]
   }
}

END 模式很特殊:当 AWK 没有更多的数据要处理时,它会一次性执行。此时,它会使用更新的欠款数生成新的成员文件。

END {
   for (x in members) {
       printf "%s:%s:%s\n", x, members[x], debt[x]
   }
}

再配合一个遍历成员文件,并向人们发送提醒电子邮件以支付他们的会费(积极清账)的脚本,这个系统管理咖啡角相当一段时间。


via: https://opensource.com/article/19/2/drinking-coffee-awk

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

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

Pony,一种“Rust 遇上 Erlang”的语言,让开发快捷、安全、高效、高并发的程序更简单。

Wallaroo Labs,我是工程副总裁,我们正在构建一个用 Pony 编程语言编写的 高性能分布式流处理器。大多数人没有听说过 Pony,但它一直是 Wallaroo 的最佳选择,它也可能成为你的下一个项目的最佳选择。

“一门编程语言只是另一种工具。与语法无关,与表达性无关,与范式或模型无关,仅与解决难题有关。” —Sylvan Clebsch,Pony 的创建者

我是 Pony 项目的贡献者,但在这里我要谈谈为什么 Pony 对于像 Wallaroo 这样的应用是个好选择,并分享我使用 Pony 的方式。如果你对我们为什么使用 Pony 来编写 Wallaroo 甚感兴趣,我们有一篇关于它的 博文

Pony 是什么?

你可以把 Pony 想象成某种“Rust 遇上 Erlang”的东西。Pony 有着最引人注目的特性,它们是:

  • 类型安全
  • 存储安全
  • 异常安全
  • 无数据竞争
  • 无死锁

此外,它可以被编译为高效的本地代码,它是在开放的情况下开发的,在两句版 BSD 许可证下发布。

以上说的功能不少,但在这里我将重点关注那些对我们公司来说采用 Pony 至关重要的功能。

为什么使用 Pony?

使用大多数我们现有的工具编写快速、安全、高效、高并发的程序并非易事。“快速、高效、高并发”是可实现的目标,但加入“安全”之后,就困难了许多。对于 Wallaroo,我们希望同时实现四个目标,而 Pony 让实现它们更加简单。

高并发

Pony 让并发变得简单。部分是通过提供一个固执的并发方式实现的。在 Pony 语言中,所有的并发都是通过 Actor 模型 进行的。

Actor 模型以在 Erlang 和 Akka 中的实现最为著名。Actor 模型出现于上世纪 70 年代,细节因实现方式而异。不变的是,所有计算都由通过异步消息进行通信的 actor 来执行。

你可以用这种方式来看待 Actor 模型:面向对象中的对象是状态 + 同步方法,而 actor 是状态 + 异步方法。

当一个 actor 收到一个消息时,它执行相应的方法。该方法可以在只有该 actor 可访问的状态下运行。Actor 模型允许我们以并发安全的方式使用可变状态。每个 actor 都是单线程的。一个 actor 中的两个方法绝不会并发运行。这意味着,在给定的 actor 中,数据更新不会引起数据竞争或通常与线程和可变状态相关的其他问题。

快速高效

Pony actor 通过一个高效的工作窃取调度程序来调度。每个可用的 CPU 都有一个单独 Pony 调度程序。这种每个核心一个线程的并发模型是 Pony 尝试与 CPU 协同工作以尽可能高效运行的一部分。Pony 运行时尝试尽可能利用 CPU 缓存。代码越少干扰缓存,运行得越好。Pony 意在帮你的代码与 CPU 缓存友好相处。

Pony 的运行时还会有每个 actor 的堆,因此在垃圾收集期间,没有 “停止一切” 的垃圾收集步骤。这意味着你的程序总是至少能做一点工作。因此 Pony 程序最终具有非常一致的性能和可预测的延迟。

安全

Pony 类型系统引入了一个新概念:引用能力,它使得数据安全成为类型系统的一部分。Pony 语言中每种变量的类型都包含了有关如何在 actor 之间分享数据的信息。Pony 编译器用这些信息来确认,在编译时,你的代码是无数据竞争和无死锁的。

如果这听起来有点像 Rust,那是因为本来就是这样的。Pony 的引用功能和 Rust 的借用检查器都提供数据安全性;它们只是以不同的方式来接近这个目标,并有不同的权衡。

Pony 适合你吗?

决定是否要在一个非业余爱好的项目上使用一门新的编程语言是困难的。与其他方法想比,你必须权衡工具的适当性和不成熟度。那么,Pony 和你搭不搭呢?

如果你有一个困难的并发问题需要解决,那么 Pony 可能是一个好选择。解决并发应用问题是 Pony 之所以存在的理由。如果你能用一个单线程的 Python 脚本就完成所需操作,那你大概不需要它。如果你有一个困难的并发问题,你应该考虑 Pony 及其强大的无数据竞争、并发感知类型系统。

你将获得一个这样的编译器,它将阻止你引入许多与并发相关的错误,并在运行时为你提供出色的性能特征。

开始使用 Pony

如果你准备好开始使用 Pony,你需要先在 Pony 的网站上访问 学习部分。在这里你会找到安装 Pony 编译器的步骤和学习这门语言的资源。

如果你愿意为你正在使用的这个语言做出贡献,我们会在 GitHub 上为你提供一些 初学者友好的问题

同时,我迫不及待地想在 我们的 IRC 频道Pony 邮件列表 上与你交谈。

要了解更多有关 Pony 的消息,请参阅 Sean Allen 2018 年 7 月 16 日至 19 日在俄勒冈州波特兰举行的 第 20 届 OSCON 会议 上的演讲: Pony,我如何学会停止担心并拥抱未经证实的技术


via: https://opensource.com/article/18/5/pony

作者:Sean T Allen 选题:lujun9972 译者:beamrolling 校对:wxy

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

在你的游戏开发过程中有了 PyGame Zero,和枯燥的模板说再见吧。

Python 是一个很好的入门级编程语言。并且,游戏是一个很好的入门项目:它们是可视化的,自驱动的,并且可以很愉快的与朋友和家人分享。虽然,绝大多数的 Python 写就的库,比如 PyGame ,会让初学者因为忘记微小的细节很容易导致什么都没渲染而感到困扰。

在理解所有部分的作用之前,他们会将其中的许多部分都视为“无意识的模板文件”——需要复制和粘贴到程序中才能使其工作的神奇段落。

PyGame Zero 试图通过在 PyGame 上放置一个抽象层来弥合这一差距,因此它字面上并不需要模板。

我们在说的“字面”,就是在指字面。

这是一个合格的 PyGame Zero 文件:

# This comment is here for clarity reasons

我们可以将它放在一个 game.py 文件里,并运行:

$ pgzrun game.py

这将会展示一个窗口,并运行一个可以通过关闭窗口或按下 CTRL-C 中断的游戏循环。

遗憾的是,这将是一场无聊的游戏。什么都没发生。

为了让它更有趣一点,我们可以画一个不同的背景:

def draw():
    screen.fill((255, 0, 0))

这将会把背景色从黑色换为红色。但是这仍是一个很无聊的游戏,什么都没发生。我们可以让它变的更有意思一点:

colors = [0, 0, 0]

def draw():
    screen.fill(tuple(colors))

def update():
    colors[0] = (colors[0] + 1) % 256

这将会让窗口从黑色开始,逐渐变亮,直到变为亮红色,再返回黑色,一遍一遍循环。

update 函数更新了参数的值,而 draw 基于这些参数渲染这个游戏。

即使是这样,这里也没有任何方式给玩家与这个游戏的交互的方式。让我们试试其他一些事情:

colors = [0, 0, 0]

def draw():
    screen.fill(tuple(colors))

def update():
    colors[0] = (colors[0] + 1) % 256

def on_key_down(key, mod, unicode):
    colors[1] = (colors[1] + 1) % 256

现在,按下按键来提升亮度。

这些包括游戏循环的三个重要部分:响应用户输入,更新参数和重新渲染屏幕。

PyGame Zero 提供了更多功能,包括绘制精灵图和播放声音片段的功能。

试一试,看看你能想出什么类型的游戏!


via: https://opensource.com/article/19/1/pygame-zero

作者:Moshe Zadka 选题:lujun9972 译者:bestony 校对: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中国 荣誉推出

(以下内容是我的硕士论文的摘录,几乎是整个 2.1 章节,向具有 CS 背景的人快速介绍 Go)

Go 是一门用于并发编程的命令式编程语言,它主要由创造者 Google 进行开发,最初主要由 Robert Griesemer、Rob Pike 和 Ken Thompson 开发。这门语言的设计起始于 2007 年,并在 2009 年推出最初版本;而第一个稳定版本是 2012 年发布的 1.0 版本。 1

Go 有 C 风格的语法(没有预处理器)、垃圾回收机制,而且类似它在贝尔实验室里被开发出来的前辈们:Newsqueak(Rob Pike)、Alef(Phil Winterbottom)和 Inferno(Pike、Ritchie 等人),使用所谓的 Go 协程 goroutines 信道 channels (一种基于 Hoare 的“通信顺序进程”理论的协程)提供内建的并发支持。 2

Go 程序以包的形式组织。包本质是一个包含 Go 文件的文件夹。包内的所有文件共享相同的命名空间,而包内的符号有两种可见性:以大写字母开头的符号对于其他包是可见,而其他符号则是该包私有的:

func PublicFunction() {
    fmt.Println("Hello world")
}

func privateFunction() {
    fmt.Println("Hello package")
}

类型

Go 有一个相当简单的类型系统:没有子类型(但有类型转换),没有泛型,没有多态函数,只有一些基本的类型:

  1. 基本类型:intint64int8uintfloat32float64
  2. struct
  3. interface:一组方法的集合
  4. map[K, V]:一个从键类型到值类型的映射
  5. [number]Type:一些 Type 类型的元素组成的数组
  6. []Type:某种类型的切片(具有长度和功能的数组的指针)
  7. chan Type:一个线程安全的队列
  8. 指针 *T 指向其他类型
  9. 函数
  10. 具名类型:可能具有关联方法的其他类型的别名(LCTT 译注:这里的别名并非指 Go 1.9 中的新特性“类型别名”):
  type T struct { foo int }
  type T *T
  type T OtherNamedType

具名类型完全不同于它们的底层类型,所以你不能让它们互相赋值,但一些操作符,例如 +,能够处理同一底层数值类型的具名类型对象们(所以你可以在上面的示例中把两个 T 加起来)。

映射、切片和信道是类似于引用的类型——它们实际上是包含指针的结构。包括数组(具有固定长度并可被拷贝)在内的其他类型则是值传递(拷贝)。

类型转换

类型转换类似于 C 或其他语言中的类型转换。它们写成这样子:

TypeName(value)

常量

Go 有“无类型”字面量和常量。

1 // 无类型整数字面量
const foo = 1 // 无类型整数常量
const foo int = 1 // int 类型常量

无类型值可以分为以下几类:UntypedBoolUntypedIntUntypedRuneUntypedFloatUntypedComplexUntypedString 以及 UntypedNil(Go 称它们为基础类型,其他基础种类可用于具体类型,如 uint8)。一个无类型值可以赋值给一个从基础类型中派生的具名类型;例如:

type someType int

const untyped = 2 // UntypedInt
const bar someType = untyped // OK: untyped 可以被赋值给 someType
const typed int = 2 // int
const bar2 someType = typed // error: int 不能被赋值给 someType

接口和对象

正如上面所说的,接口是一组方法的集合。Go 本身不是一种面向对象的语言,但它支持将方法关联到具名类型上:当声明一个函数时,可以提供一个接收者。接收者是函数的一个额外参数,可以在函数之前传递并参与函数查找,就像这样:

type SomeType struct { ... }
type SomeType struct { ... }

func (s *SomeType) MyMethod() {
}

func main() {
    var s SomeType
    s.MyMethod()
}

如果对象实现了所有方法,那么它就实现了接口;例如,*SomeType(注意指针)实现了下面的接口 MyMethoder,因此 *SomeType 类型的值就能作为 MyMethoder 类型的值使用。最基本的接口类型是 interface{},它是一个带空方法集的接口 —— 任何对象都满足该接口。

type MyMethoder interface {
    MyMethod()
}

合法的接收者类型是有些限制的;例如,具名类型可以是指针类型(例如,type MyIntPointer *int),但这种类型不是合法的接收者类型。

控制流

Go 提供了三个主要的控制了语句:ifswitchfor。这些语句同其他 C 风格语言内的语句非常类似,但有一些不同:

  • 条件语句没有括号,所以条件语句是 if a == b {} 而不是 if (a == b) {}。大括号是必须的。
  • 所有的语句都可以有初始化,比如这个 if result, err := someFunction(); err == nil { // use result }
  • switch 语句在分支里可以使用任何表达式
  • switch 语句可以处理空的表达式(等于 true
  • 默认情况下,Go 不会从一个分支进入下一个分支(不需要 break 语句),在程序块的末尾使用 fallthrough 则会进入下一个分支。
  • 循环语句 for 不仅能循环值域:for key, val := range map { do something }

Go 协程

关键词 go 会产生一个新的 Go 协程 goroutine ,这是一个可以并行执行的函数。它可以用于任何函数调用,甚至一个匿名函数:

func main() {
    ...
    go func() {
        ...
    }()

    go some_function(some_argument)
}

信道

Go 协程通常和信道channels结合,用来提供一种通信顺序进程的扩展。信道是一个并发安全的队列,而且可以选择是否缓冲数据:

var unbuffered = make(chan int) // 直到数据被读取时完成数据块发送
var buffered = make(chan int, 5) // 最多有 5 个未读取的数据块

运算符 <- 用于和单个信道进行通信。

valueReadFromChannel := <- channel
otherChannel <- valueToSend

语句 select 允许多个信道进行通信:

select {
    case incoming := <- inboundChannel:
    // 一条新消息
    case outgoingChannel <- outgoing:
    // 可以发送消息
}

defer 声明

Go 提供语句 defer 允许函数退出时调用执行预定的函数。它可以用于进行资源释放操作,例如:

func myFunc(someFile io.ReadCloser) {
    defer someFile.close()
    /* 文件相关操作 */
}

当然,它允许使用匿名函数作为被调函数,而且编写被调函数时可以像平常一样使用任何变量。

错误处理

Go 没有提供异常类或者结构化的错误处理。然而,它通过第二个及后续的返回值来返回错误从而处理错误:

func Read(p []byte) (n int, err error)

// 内建类型:
type error interface {
    Error() string
}

必须在代码中检查错误或者赋值给 _

n0, _ := Read(Buffer) // 忽略错误
n, err := Read(buffer)
if err != nil {
    return err
}

有两个函数可以快速跳出和恢复调用栈:panic()recover()。当 panic() 被调用时,调用栈开始弹出,同时每个 defer 函数都会正常运行。当一个 defer 函数调用 recover()时,调用栈停止弹出,同时返回函数 panic() 给出的值。如果我们让调用栈正常弹出而不是由于调用 panic() 函数,recover() 将只返回 nil。在下面的例子中,defer 函数将捕获 panic() 抛出的任何 error 类型的值并储存在错误返回值中。第三方库中有时会使用这个方法增强递归代码的可读性,如解析器,同时保持公有函数仍使用普通错误返回值。

func Function() (err error) {
    defer func() {
        s := recover()
        switch s := s.(type) {  // type switch
            case error:
                err = s         // s has type error now
            default:
                panic(s)
        }
    }
}

数组和切片

正如前边说的,数组是值类型,而切片是指向数组的指针。切片可以由现有的数组切片产生,也可以使用 make() 创建切片,这会创建一个匿名数组以保存元素。

slice1 := make([]int, 2, 5) // 分配 5 个元素,其中 2 个初始化为0
slice2 := array[:] // 整个数组的切片
slice3 := array[1:] // 除了首元素的切片

除了上述例子,还有更多可行的切片运算组合,但需要明了直观。

使用 append() 函数,切片可以作为一个变长数组使用。

slice = append(slice, value1, value2)
slice = append(slice, arrayOrSlice...)

切片也可以用于函数的变长参数。

映射

映射 maps 是简单的键值对储存容器,并支持索引和分配。但它们不是线程安全的。

someValue := someMap[someKey]
someValue, ok := someMap[someKey] // 如果键值不在 someMap 中,变量 ok 会赋值为 `false`
someMap[someKey] = someValue

via: https://blog.jak-linux.org/2018/12/24/introduction-to-go/

作者:Julian Andres Klode 选题:lujun9972 译者:LazyWolfLin 校对:wxy

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


  1. Frequently Asked Questions (FAQ) - The Go Programming Language https://golang.org/doc/faq#history [return]
  2. HOARE, Charles Antony Richard. Communicating sequential processes. Communications of the ACM, 1978, 21. Jg., Nr. 8, S. 666-677. [return]

十月初,由 Opensource.com 赞助的首届 Open Jam 吸引了来自世界各地的团队的 45 个参赛项目。这些队伍只用了三天时间就用开源软件制作出一个游戏来参与角逐,有三支队伍取得了胜利

我们在我们的大学为每一位愿意参与的人举办了我们自己的 Open Jam 活动。我们预留了周末的计算机实验室,并教大家使用开源软件来创建游戏和游戏素材:游戏引擎:Godot、音乐:LMMS、2D 素材:GIMP、3D 素材:Blender。我们的活动产出了三个游戏:LoathsomeLost ArtistPaint Rider(我做的)。

根据我在游戏开发和游戏开发方面的经验,这里有 10 条关于游戏引擎、编码和快速游戏开发的经验教训。

1、限定规模

很容易想要去做一个规模宏大的冒险游戏或者可以比拟你最喜欢的游戏的东西。如果你有一些经验,追求超乎游戏 Jam 活动的东西可能很酷,但不要高估自己拥有的时间。我欣赏游戏 Jam 活动的一点是它强制你快速将一个游戏从概念阶段变成最终产品,因为你的时间非常有限。这也就是限定规模如此重要的原因。

这个 Open Jam 的主题是“留下痕迹”,题目一出来,我和朋友就开始讨论什么样的游戏适合该主题。一个想法就是做玩家能在敌人身上留下伤痕的 3D 拳击游戏。我几乎没有做 3D 游戏的经验,我想做好的话,在我甚至还没发掘出可玩性之前,就得花太多时间在学习如何让痕迹合理和打击有效。

2、尽早可玩

这是我对游戏 Jam 活动最中肯的建议。试着做出核心机制,快速写出代码,这样你就可以测试并决定它是否值得做成一个完整的游戏。不应该只剩几个小时截止了,你的游戏才可玩。像 Open Jam 这样的三天的活动,不应该花费几个小时以上来做一个可以运行的演示。

3、保持简单

你想加入的每个特性都会延长整个开发时间。因为你不能迅速使之运行,所以无从得知提交一个新特性是否会消耗大量时间。街机风格的高分作品往往会在游戏 Jam 活动中表现良好,它们天生就很简单。一旦核心部分完成,你可以开始加入特性并润色,无需担心最后游戏是否功能正常。

4、从其他游戏获取灵感

可能你想做出完全原创的作品,但有个可以基于它开发的原型极其有用。这将节省重复劳动的时间,因为你已经知道什么有趣。告诉自己实践的经验越多,越容易做出包含自己想法的大型游戏,所以你也能从再创作其他人的作品中得到很好地练习。

考虑到 Open Jam 的“留下痕迹”主题,我觉得创作一个玩的时候可以留下颜料痕迹的游戏会很有趣,这样也可以看到你留下的标记。我记得有款老式动画游戏 Line Rider 2 Beta (后来叫 Paint Rider),而且知道玩的时候按住 Control 键可以画出痕迹的彩蛋。我简化了这个概念,甚至只需要一个按键来垂直移动。(更像老式飞机游戏)。进入到 Jam 活动大概一两个小时后,我就有了基本模型,可以用一个按钮上下移动和留下小黑圈的痕迹。

5、不要忽视可得性

确保尽可能多的人能玩你的游戏。某个提交到 Open Jam 的游戏是虚拟现实游戏。尽管那很酷,但几乎没有人可以玩,因为拥有 VR 设备的人不多。所幸它的开发者并不期望取得好名次,只是想练手。但如果你想和人们分享你的游戏(或者赢得游戏 Jam 活动),注意可得性是很重要的。

Godot (和其他大多数游戏引擎)允许你在所有主流平台发布游戏。提交游戏时,特别是在 Itch.io,有个浏览器版本就可以支持大多数人玩。但尽你所能去发布在更多的平台和操作系统上。我甚至试着在移动端发布 Paint Rider,但技术有限。

6、不要做得太难

如果游戏需要花费过多精力去学或者玩,你将失去一部分玩家。要保持简单和限定规模,这在游戏计划阶段非常重要。再次重申,想出一个需要花上十天半个月开发的宏大的游戏创意很容易;难的是做出好玩、简单的游戏。

给我的妈妈介绍了 Paint Rider 之后,她很快开始玩起来,我认为不需要跟她说明更多。

7、不用太整洁

如果你习惯于花时间在设计模式上和确保代码可复用、可适应,试着放松一点。如果你花太多时间考虑设计,当你最后到了可以玩游戏的时候,你可能发现游戏不是很有趣,那时候就来不及修改了。

这过程也适用于简化更严格的游戏:快速码出验证概念性展示模型,直到找出值得做成完整游戏的,然后你可以潜心建立完美的代码来支持它。为游戏 Jame 活动创作的游戏就像是个快速开发一个可验证的模型一样。

8、但也不要太随意

另一方面, 意大利面式代码 容易失控,即使游戏开发没有大量代码。还好大多是游戏引擎都考虑到了设计模式。就拿 Godot 的信号 功能来说,节点可以发送数据信息给它们“连上了”的节点 —— 这是你的设计自动成型的观察者模式。只要你知道如何利用这种游戏引擎的特性的优势,就可以快速写代码,你的代码也不会特别难读。

9、取得反馈

向人们展示你正在做的。让他们试一试并看看他们说些啥。看看他们如何玩你的游戏,找找他们有没有发现你预料之外的事。如果游戏 Jam 活动有 Discord 频道或者类似的,把你的游戏放上去,人们会反馈给你想法。Paint Rider 的一个确定的功能是画布循环,所以你可以看到之前留下来的画。在有人问我为什么这个游戏没有之前,我甚至没有考虑那个机制。

团队协作的话,确保有可以传递周围反馈的人参与这个开发。

而且不要忘了用相同的方式帮助其他人;如果你在玩其他人游戏的时候发现了有助于你游戏的东西,这就是双赢。

10、哪里找资源

做出所有你自己的资源真的会拖你后腿。Open Jam 期间,当我忙于组装新特性和修漏洞时,我注意到 Loathsome 的开发者花了大量时间在绘制主要角色上。你可以简化游戏的艺术风格创作并且用一些视听效果尚可的东西,但这里还有其他选择。试着寻找 Creative Commons 许可的或免费音乐站点(比如 Anttis Instrumentals)的资源。或者,可行的话,组建一个有专门艺术家、作家或者音乐家的团队。

其他你可能觉得有用的软件有 Krita,这是一款适合数字绘画的开源 2D 图像生成软件,特别是如果你有一块绘图板的话;还有 sfxr,这是一款游戏音效生成软件,很多参数可以调,但正如它的开发者所说:“它的基本用法包括了按下随机按钮。”(Paint Rider 的所有音效都是用 Sfxr 做的。)你也可以试试 Calinou 的众多但有序的开源游戏开发软件列表。

你参加 Open Jam 或者其他游戏 Jam 并有别的建议吗?对我未提及的有问题吗?有的话,请在评论中分享。


via: https://opensource.com/article/17/12/10-keys-rapid-open-source-game-development

作者:Ryan Estes 译者:XYenChi 校对:wxy

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