0%

Python 版本

目前 Python 主要活跃的有 Python 2.x 和 Python 3.x 两个大版本,与 C++ 和 Java 这种向后兼容的语言不同,Python 的两个版本互不兼容。舍弃兼容性是一种设计上的取舍,在我看来 Python 这种尤为注重“简约”的语言,敢于大胆摒弃一些有设计缺陷的旧包袱,从而拥抱新特性的作风,未尝不是一种 Pythonic 的体现,很大程度上避免了走向像 C++ 一样越来越臃肿晦涩的道路。

Python 核心团队已于 2019 年正式宣布将在 2020 年停止对 Python2 的更新,在此期间会对 Python2 版本进行一些 bug 修复、安全增强以及移植等工作,以便使开发者顺利的从 Python2 迁移到 Python3。Python 2.7 是 2.x 系列的最后一个版本,官网上最新的 Python 2.7.18 版本发布于 2020 年 4 月 20 日。官方停止 Python2 更新的主要动机是想进行 Python3 的推广,以及同时维护两个版本给他们带来的工作负担。目前大部分 Python 开源项目已经兼容 Python3 了,所以强烈建议使用 Python3 来开发新的项目

一般较新的 Linux 发行版已经预装了 Python2 和 Python3,如果没有,也可以通过各自的包管理器进行安装和更新。Mac OS 环境下可以通过 Homebrew 工具来安装 Python,可以附加 @ + 版本号 安装指定版本。在一般情况下(不手动修改软链接),命令行中的 python 通常是 python 2.7 或其旧版本的别名,python3 才指代 Python3 版本,可以通过 --version 参数来查看安装的具体版本。由于两个版本互不兼容,在命令行运行 Python 脚本前需要先确定其所用的 Python 版本。

1
2
3
4
5
6
7
➜ brew install python  # brew install python@2.7
➜ python --version
Python 2.7.10

➜ brew install python3 # brew install python@3.8
➜ python3 --version
Python 3.8.6

有时也需要在代码中,也就是运行时确定 Python 版本,此时用到的是内置的 sys 模块:

1
2
3
4
5
6
>>> import sys
>>> print(sys.version)
3.8.6 (default, Oct 8 2020, 14:07:53)
[Clang 11.0.0 (clang-1100.0.33.17)]
>>> print(sys.version_info)
sys.version_info(major=3, minor=8, micro=6, releaselevel='final', serial=0)

可以通过在运行时判断 Python 版本从而达到较好的兼容性,这在 Python 的内置模块以及标准库中使用较多。由于 version_info 本身是个 tuple 类型,重载了比较运算符,所以可以像下面这样直接进行比较:

1
2
3
4
5
6
7
8
9
# builtins.pyi
if sys.version_info >= (3, 9):
from types import GenericAlias

# contextlib2.py
if sys.version_info[:2] >= (3, 4):
_abc_ABC = abc.ABC
else:
_abc_ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})

上面的源码来自于两个不同的文件,阅读源码可以发现一些 Python 版本变更的内容。比如自 Python 3.9 引入的 GenericAlias 类型;Python 3.4 之前继承抽象类时还得使用 ABCMeta 形式。

阅读全文 »

前言

阅读源码是每个程序员都应当具备的技能,阅读源码不仅能帮助你理解一个模块实现的细节,也能让你从优秀的源码中汲取经验,遵循更好的编码规范,编写出更 Pythonic 的代码。但不可否认的是,阅读源码需要一定的编码功底,盲目的阅读并不能取得应有的效果。在阅读源码之前我们要明白阅读的目的,如果是想了解一个模块的实现细节自不必多说,但如果是想提高自己的 Python 编码水平,那么就应该从 Python 标准库以及一些优秀的第三方开源代码下手。

在《Python编程之美:最佳实践指南》这本书中,作者 Kenneth Reitz 从简单的 HowDoI 项目,到大一点的 requests 库(他本身也是这个库的开发者),再到后面的 Web 框架 Flask,逐步递进地展示如何阅读高质量的代码。如果想阅读优秀的第三方库源码,可以从他在书中罗列出的经典项目开始。除此之外,GitHub 上也有人整理了比较详尽的目录:Python 开源库及示例代码。项目很多,但不是每个都必读。还是强调的那一点:不要盲目的阅读源码,确定有必要的时候再去阅读。

抛开这些问题不谈,本篇我想结合我自己在阅读标准库源码(主要是 typing 模块和 re 模块)时的一点理解,介绍一些阅读源码前需要掌握的先验知识,以及如何结合开发工具在 PyCharm IDE 中高效地阅读源码。让我们先从 Python 代码的类型提示开始。

函数注解

PEP 3107 – Function Annotations : Python Version 3.0, Created Time 2-Dec-2006. This PEP introduces a syntax for adding arbitrary metadata annotations to Python functions.

Python 3 添加了对类型提示(Type Hints)的支持,在此之前 Python 2.x 一直缺乏一种统一的方式去对函数参数和返回值进行标注,一些工具或三方库通过 docstring、注释或者函数装饰器等其他方法尝试去弥补这种缺陷。而自从 Python 3.0 开始,Python 通过 PEP 3107 提案引入了函数注解,也就是 Function Annotations,提供了一种标准的解决方案,用于为函数声明中的参数和返回值附加元数据

函数注解的语法如下所示:

1
2
def foo(a: expression, b: expression = 5) -> expression:
...

函数声明中的各个参数可以在 : 之后添加注解表达式。如果参数有默认值,表达式后可以跟 = 指定默认值,且与常规函数声明一样,指定默认值参数要出现在无默认值参数之后。注解表达式最常使用的是类型(如 str 或 int),也可以是一个字符串(如 ‘int > 0’)。如果想注解返回值,在 ): 之间添加 -> 和一个表达式,表达式可以是任意类型,如果函数无返回值则为 None。

阅读全文 »

PEP,全称 Python Enhancement Proposals,译为 Python 增强提案。PEP 已经成为 Python 发布新特性的主要机制,它会收集社区对 Python 的改进意见,经过核心开发者的审查和认可最终形成提案向公众公示。PEP 的官网首页 也是 PEP 0 的地址,在这里官方列举了所有的 PEP 的索引,你可以按序号、标题和类型进行检索。

Python 之禅

Python 开发者喜欢用 “Pythonic” 这个单词来形容符合 Python 编码风格的代码。这种风格既不是严格的规范也不是编译器强加给开发者的规则,而是大家在使用 Python 语言协同工作的过程中逐渐形成的习惯。要记住:Python 开发者不喜欢复杂的事物,他们崇尚直观、简洁而又易读的代码。为此,Python 语言的早期贡献者 Tim Peters 提出了 PEP 20 – The Zen of Python,译为 Python 之禅,提出了共计 19 条 Python 编码的指导性原则。这已经作为一个彩蛋加入到 Python 标准库中,你可以在 Python 交互式命令行中敲入 import this 查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

这 19 条指导思想强调了代码简约可读的重要性,其中的大多数条目不仅仅适用于 Python,也适用于任何一门其他语言。

Python 风格指导

除此之外,PEP 8 – Style Guide for Python Code 也是每个 Python 程序员应当阅读的,相较于 Python 之禅它提出了更为细致的建议,目的是让 Python 程序员遵循一致的编码风格。PEP 8 中的大部分都能在 Pycharm IDE 中找到智能提示,缩进、空格与空行也可以通过代码格式化快捷键(Reformat Code)来一键规范化,在 Mac OS 中默认快捷键为 Cmd + Alt + L,Windows 中为 Ctrl + Alt + L。如果你不使用 PyCharm,也可以安装 Pylint,这是一款 Python 源码静态分析工具,可以自动检测代码是否符合 PEP 8 风格指南。

命名规范

这里,我想强调一下 Python 中的命名规范。PEP 8 提倡采用不用的命名风格来区分 Python 语言中的不同角色:

  • 文件名(模块名)使用小写字母,单词间以下划线连接,如 base_futures.py;私有模块使用单个下划线开头,如 _collections_abc.py
  • 函数、变量及属性名,使用小写字母,单词间以下划线连接,如 dict_keys
  • 受保护的属性和函数(子类可以访问),使用单个下划线开头,如 _protected_method
  • 私有的属性和函数(子类也不能访问),使用两个下划线开头,如 __private_method
  • 类与异常,以每个单词首字母大写来命名,如 BaseHandlerTypeError
  • 模块级别的常量,全部用大写字母,单词间以下划线连接,如 STDIN_FILENO
  • 类中的实例方法(instance method),首个参数命名为 self 表示对象自身;类方法(class method),首个参数命名为 cls 表示类自身。
阅读全文 »

函数是一等公民

虽然《流畅的Python》作者一再强调 Python 不是一门函数式编程语言,但它的的确确具备了一些函数式编程的特性。其中的一个重要特性是:Python 将函数作为一等公民。这与 JavaScript、Scala 等语言一样,意味着在这类语言中:函数与其他数据类型处于同等地位,函数可以定义在函数内部,也可以作为函数的参数和返回值。基于这个特性,我们可以很容易的定义高阶函数。来看一个 JavaScript 的例子:

1
2
3
4
5
const add = function(x) {
return function(y) {
return x + y
}
}

这个函数将一个函数作为了返回值,很明显它是一个高阶函数,那么问题来了:这样定义有什么作用或者是好处呢?事实上,这段代码是 JavaScript 中的一个优雅的函数式编程库 Ramda 对于加法实现的基本思路(还需要可变参数以及参数个数判断)。最终我们可以这样去使用它:

1
2
3
4
5
const R = require('ramda')
R.add(1, 2) // -> 3
const increment = R.add(1) // 返回一个函数
increment(2) // -> 3
R.add(1)(2) // -> 3

既可以像代码第二行一次性传入两个参数,也可以像代码第三、四行分两个阶段传入,这与代码第五行效果一致。我们将这种特性称为函数柯里化(Currying),这样做的好处一是可以代码重用,就像特意将 R.add(1) 取名为 increment 一样,它可以单独地作为一个递增函数;二是可以实现惰性求值,只有当函数收集到了所有所需参数,才进行真正的计算并返回结果,这一点在许多流处理框架中有广泛使用。

Python 中的函数之所以可以作为一等公民,究其原因,是因为 Python 中的一切皆是对象,即 Everything in Python is an object。使用 def 关键字定义的任何函数,都是 function 类的一个实例。

1
2
3
4
5
>>> def func():
... pass
...
>>> type(func)
<class 'function'>

既然函数是对象,那就可以持有属性,这也是为什么 Python 中函数可以持有外部定义的变量(也就是闭包问题)的根本原因。这一点与 Java 和 C++ 这类语言是有本质区别的。以 Java8 为例,虽然 Java8 提供了一些语法糖让我们得以编写所谓的“高阶函数”,但 Java 中的函数(方法)依然不能脱离类或者对象而存在:

1
2
3
4
Arrays.asList(1, 2, 3, 4, 5)
.stream()
.filter(i -> i >= 3)
.forEach(System.out::println);
阅读全文 »

前言

与传统的批处理(Batch processing)相比,流处理(Streaming processing)处理的是实时的持续数据流,也被称为无界数据集(unbounded datasets),亦即能够持续增长的、不可预测的无限数据集。而批处理处理的是有界数据集(bounded datasets),有界数据集是有限的不变的,存在开始和结束,也被称之为历史数据集(historic datasets)。

通常,为了应对高速流动的无界数据流,流处理对于处理效率要求更高,内存占用要求更低,与之相对的,相较批处理对于错误的容忍度要更高。

本文介绍了 Apache Storm、Apache Flink 和 Spark Streaming 三种常用流处理框架,主要包含它们各自的拓扑结构和运行时架构,最后还对流处理框架的演进和流批一体化的趋势做了简要介绍。

Apache Storm

Apache Storm 是一个分布式实时计算框架,主要使用 Clojure 和 Java 语言编写,目前最新版本 2.2.0。在 Storm 中,数据流被抽象为 tuples,由数据和 ID 标识符组成。Storm 的拓扑结构(Topology)是一个有向无环图,由输入节点Spouts处理节点Bolts代表数据流的边三部分组成,如下图所示:

7f120eb6c1e92b4775e84d196100e3f5.png

Spouts 是整个拓扑结构的入口点,负责将输入数据流转换为 tuples,送至 Bolts 进行处理;Bolts 负责处理输入流并转换为输出流,它维护了处理逻辑,能够对 tuples 执行过滤、映射、聚合等函数式操作,还能与数据库进行交互。

Storm 的运行时架构与 Hadoop 类似,也是经典的主从模式(master-slave)。Storm 中的主节点(master node)运行一个叫做 Nimbus 的程序,由其负责资源分配和任务调度,用户定义的 Topology 会被提交到 Nimbus 上;从节点(worker node)运行 Supervisor 程序,负责执行 Nimbus 分配的任务,其上可以运行一个或多个工作进程(worker process),每个工作进程执行 Topology 的一个子集。Nimbus 不能直接与 Supervisor 进行交互,两者需要通过 ZooKeeper 进行协作,ZooKeeper 保存调度信息、心跳信息、集群状态和配置信息。

aa1882c89b70544081866ade75563064.png

阅读全文 »