为数据科学家提供的关于Python 3特性的简介
Python 已成为机器学习以及其他紧密结合数据的科学领域的主流语言;它提供了各种深度学习的框架以及一系列完善的数据处理和可视化工具。
然而,Python 的生态圈中 Python 2 和 Python 3 是共存状态,并且数据科学家之中是依然有使用 Python 2 的。2019年年底(Python的)科学组件将会停止支持 Python 2 。 至于numpy,2018年之后任何推出的新特性将会只支持Python 3 。
为了让这一过渡更轻松一点,我整理了一些 Python 3 你可能觉得有用的特性。
pathlib提供了更好的路径处理
pathlib 是Python 3 一个默认的组件,有助于避免大量使用os.path.join:
1from pathlib import Path
2
3dataset = 'wiki_images'
4datasets_root = Path('/path/to/datasets/')
5
6train_path = datasets_root / dataset / 'train'
7test_path = datasets_root / dataset / 'test'
8
9for image_path in train_path.iterdir():
10 with image_path.open() as f:
11
以前,人们倾向于使用字符串连接(虽然简洁,但明显不好);现在,代码中用pathlib是安全的,简洁的,并且更有可读性。
此外,pathlib.Path有大量的方法和属性,每一位 Python 早期的初学者不得不谷歌了解:
1p.exists()
2p.is_dir()
3p.parts
4p.with_name('sibling.png')
5p.with_suffix('.jpg')
6p.chmod(mode)
7p.rmdir()
pathlib 应当会节省大量时间,请参看文档以及指南了解更多。
类型提示现在已是这语言的一部分
pycharm环境类型提示的例子:
Python 不再是一种小型的脚本语言,数据管道现如今包含数个级别,而每一级又涉及到不同的框架(甚至有时是千差万别的逻辑)。
类型提示的引入是为了在程序的持续增加的复杂性方面提供帮助,这样机器可以辅助代码验证。以前不同的模块使用自定义的方式指定文档字符中的类型(提示:pycharm可以将旧的字符串转换成新的类型提示)。
作为一个简单的例子,下面的代码可以适用于数据的不同类型(这也是关于数据栈我们喜欢的一点)。
1def repeat_each_entry(data):
2 """ Each entry in the data is doubled
3
4 """
5 index = numpy.repeat(numpy.arange(len(data)), 2)
6 return data[index]
这段代码可适用于例如 numpy.array (包括多维数组), astropy.Table 以及 astropy.Column, bcolz, cupy, mxnet.ndarray 和其他的组件。
这段代码虽然也适用于pandas.Series,但是是错误的使用方式:
1repeat_each_entry(pandas.Series(data=[0, 1, 2], index=[3, 4, 5]))
这曾经是两行代码。想象一下一个复杂系统不可预知的行为,仅仅是因为一个功能可能会失败。在大型的系统中,明确地指出方法期望的类型是非常有帮助的。如果一个方法通过了意外参数,则会给出警告。
1def repeat_each_entry(data: Union[numpy.ndarray, bcolz.carray]):
如果你有一个重要的代码仓库,比如MyPy的提示工具有可能成为你持续集成管道的一部分。Daniel Pyrathon主持的"Putting Type Hints to Work"研讨会,给出了一个很好的简介。
边注:不幸的是,提示信息还不够强大为多维数组/张量提供精细的提示。但是也许我们会有,并且这将是DS的一个强大功能。
类型提示 → 在运行时检查类型
默认情况下,方法声明不会影响你运行中的代码,而只是帮助你指出代码的意图。
然而,你可以利用工具,比如enforce,在代码运行时执行类型检查,这对你在debug代码时是很有帮助的(类型提示不起作用的情况也很多)。
1@enforce.runtime_validation
2def foo(text: str) -> None:
3 print(text)
4
5foo('Hi'
)
6foo(5)
7
8
9@enforce.runtime_validation
10def any2(x: List[bool]) -> bool:
11 return any(x)
12
13any ([False, False, True, False])
14any2([False, False, True, False])
15
16any (['False'])
17any2(['False'])
18
19any ([False, None, "", 0])
20any2([False, None, "", 0])
方法声明的其他用途
正如之前提到的,声明不会影响代码执行,而只是提供一些元信息,此外你也可以随意使用。
比如,测量单位是科学领域常见的痛点,astropy包提供了一个简单的装饰器用来控制输入数量的单位及转换输出部分所需的单位。
1
2from astropy import units as
u
3@u.quantity_input()
4def frequency(speed: u.meter / u.s, wavelength: u.m) -> u.terahertz:
5 return speed / wavelength
6
7frequency(speed=300_000 * u.km / u.s, wavelength=555 * u.nm)
8
如果你正在用Python处理表格式的科学数据(没必要是天文数字),那么你应该试试astropy。
你也可以自定义专用的装饰器,以相同的方式执行输入和输出的控制/转换。
矩阵乘号 @
让我们来实现一个最简单的 ML(机器学习) 模型 — 具有 l2 正则化的线性回归(又名岭回归):
1
2
3
4X = np.linalg.inv(np.dot(A.T, A) + alpha * np.eye(A.shape[1])).dot(A.T.dot(b))
5
6X = np.linalg.inv(A.T @ A + alpha * np.eye(A.shape[1])) @ (A.T @ b)
使用@的代码在深度学习框架之间变得更有可读性和可转换性:对于单层感知器,相同的代码X @ W + b[None, :] 可运行与numpy、 cupy、 pytorch、 tensorflow(以及其他基于张量运行的框架)。
通配符 `**`
即使glob2的自定义模块克服了这一点,但是在Python 2中递归的文件夹通配依旧不容易。自Python3.5以来便支持了递归标志:
1import glob
2
3
4found_images = \
5 glob.glob('/path/*.jpg') \
6 + glob.glob('/path/*/*.jpg') \
7 + glob.glob('/path/*/*/*.jpg') \
8 + glob.glob('/path/*/*/*/*.jpg'
) \
9 + glob.glob('/path/*/*/*/*/*.jpg')
10
11
12found_images = glob.glob('/path/**/*.jpg', recursive=True)
一个更好的选项就是在Python 3中使用pathlib(减少了一个导入!):
1
2found_images = pathlib.Path('/path/').glob('**/*.jpg')
Print 现在成了一个方法
是的,代码现在有了这些烦人的括号,但也是有一些好处的:
1 print >>sys.stderr, "critical error"
2 print("critical error", file=sys.stderr)
不使用str.join打印制表符对齐表:
1
2print(*array, sep='\t')
3print(batch, epoch, loss, accuracy, time, sep='\t')
结束/重定向打印输出:
1
2_print = print
3def print(*args, **kargs):
4 pass
在jupyter中,最好将每个输出记录到一个单独的文件中(以便跟踪断开连接后发生的情况),以便你现在可以重写 print 。
下面你可以看到暂时覆盖打印行为的上下文管理器:
1@contextlib.contextmanager
2def replace_print():
3 import builtins
4 _print = print
5
6 builtins.print = lambda *args, **kwargs: _print('new printing', *args, **kwargs)
7 yield
8 builtins.print = _print
9
10with replace_print():
11 print function>
这并不是推荐的方法,现在却可能是一次小小的黑客攻击。
print 可以参与理解列表和其他语言结构:
1
2result = process(x) if is_valid(x) else print('invalid item: ', x)
数字中的下划线 (千位分隔符)
PEP-515在数字中引入了下划线。在Python 3 中,下划线可以用于在整数,浮点数,以及一些复杂的数字中以可视的方式对数字分组。
1
2one_million = 1_000_000
3
4
5addr = 0xCAFE_F00D
6
7
8flags = 0
b_0011_1111_0100_1110
9
10
11flags = int('0b_1111_0000', 2)
用于简单可靠格式化的 f-strings
默认的格式化系统提供了数据实验中不必要的灵活性。由此产生的代码对于任何更改都显得过于冗长或者脆弱。
通常数据科学家会以固定的格式反复输出一些记录信息。如下代码就是常见的一段:
1
2print('{batch:3} {epoch:3} / {total_epochs:3} accuracy: {acc_mean:0.4f}±{acc_std:0.4f} time: {avg_time:3.2f}'.format(
3 batch=batch, epoch=epoch, total_epochs=total_epochs,
4 acc_mean=numpy.mean(accuracies), acc_std=numpy.std(accuracies),
5 avg_time=time / len(data_batch)
6))
7
8
9print('{:3} {:3} / {:3} accuracy: {:0.4f}±{:0.4f} time: {:3.2f}'.format(
10 batch, epoch, total_epochs, numpy.mean(accuracies), numpy.std(accuracies),
11 time / len(data_batch)
12))
简单输出:
1120 12 / 300 accuracy: 0.8180±0.4649 time: 56.60
f-string 又名格式化的字符串,在Python 3.6 中引入:
1
2print(f'{batch:3} {epoch:3} /
{total_epochs:3} accuracy: {numpy.mean(accuracies):0.4f}±{numpy.std(accuracies):0.4f} time: {time / len(data_batch):3.2f}')
“真正的除法”与“整数除法”之间的明显区别
对于数据科学来说,这绝对是一个便利的改变。
1data = pandas.read_csv('timing.csv')
2velocity = data['distance'] / data['time']
Python 2 中的计算结果取决于“时间”和“距离”(例如,分别以米和秒计量)是否存储为整数,而在Python 3 中,结果在两种情况下都是正确的,因为除法的计算结果是浮点型了。
另一种情况是整数除法,它现在是一种精确的运算了:
1n_gifts = money // gift_price
注意,这都适用于内置类型及数据包提供的自定义类型(如numpy 或者 pandas)。
严谨的排序
1
23 '3'
32 None
4(3, 4) 3, None)
5(4, 5) 4, 5]
6
7
8(4, 5) == [4, 5]
防止偶尔对不同类型的实例进行排序
1sorted([2, '1', 3])
有助于发现在处理原始数据时的一些问题
边注:合理检查None的情况(Python两个版本中都有)
1if a is not None:
2 pass
3
4if a:
5 pass
用于NLP的Unicode
*译者注:NLP,自然语言处理 (Natural Language Processing) *
1s = '您好'
2print(len(s))
3print(s[:2])
输出:
Python 2: 6\n��
Python 3: 2\n您好.
1x = u'со'
2x += 'co'
3x += 'со'
Python 2 失败了,Python 3 如预期运行(因为我在字符串中使用了俄语的文字)。
在Python 3 中,str是unicode字符串,对于非英文文本的NLP处理更为方便。
这还有一些其他有趣的事情,比如:
1'a' u'a'
2'a' u'a'
1from collections import Counter
2Counter('Möbelstück')
Python 2: Counter({'\xc3': 2, 'b': 1, 'e': 1, 'c': 1, 'k': 1, 'M': 1, 'l': 1, 's': 1, 't': 1, '\xb6': 1, '\xbc': 1})
Python 3: Counter({'M': 1, 'ö': 1, 'b': 1, 'e': 1, 'l': 1, 's': 1, 't': 1, 'ü': 1, 'c': 1, 'k': 1})
虽然你可以用Python 2正确地处理所有这些情况,但Python 3显得更加友好。
保留字典和** kwargs的顺序
在CPython 3.6+中,字典的默认行为与OrderedDict类似(并且这在Python 3.7+ 中也得到了保证))。这在字典释义时提供了顺序(以及其他操作执行时,比如json序列化/反序列化)。
1import json
2x = {str(i):i for i in range(5)}
3json.loads(json.dumps(x))
4
5{u'1': 1, u'0': 0, u'3': 3, u'2': 2, u'4': 4}
6
7{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
同样适用于** kwargs(Python 3.6+),它们保持与它们在参数中出现的顺序相同。在数据管道方面,顺序至关重要,以前我们必须以繁琐的方式来编写:
1from torch import nn
2
3
4model = nn.Sequential(OrderedDict([
5 ('conv1', nn.Conv2d(1,20,5)),
6 ('relu1', nn.ReLU()),
7 ('conv2', nn.Conv2d(20,64,5
)),
8 ('relu2', nn.ReLU())
9 ]))
10
11
12model = nn.Sequential(
13 conv1=nn.Conv2d(1,20,5),
14 relu1=nn.ReLU(),
15 conv2=nn.Conv2d(20,64,5),
16 relu2=nn.ReLU())
17)
你注意到了吗?命名的唯一性也会自动检查。
可迭代对象的(Iterable)解压
1
2model_paramteres, optimizer_parameters, *other_params = load(checkpoint_name)
3
4
5*prev, next_to_last, last = values_history
6
7
8
9*prev, next_to_last, last = iter_train(args)
默认的pickle引擎为数组提供更好的压缩
1
2import cPickle as pickle
3import numpy
4print len(pickle.dumps(numpy.random.normal(size=[1000, 1000])))
5
6
7
8import pickle
9import numpy
10len(pickle.dumps(numpy.random.normal(size=[1000, 1000])))
11
1/3的空间,以及更加快的速度。事实上,使用protocol = 2参数可以实现类似的压缩(速度则大相径庭),但用户通常会忽略此选项(或者根本不知道它)。
更安全的压缩
1labels =
2predictions = [model.predict(data) for data, labels in dataset]
3
4
5
超简单的super()函数
Python 2 中的super(...)曾是代码中最常见的错误源头。
1
2class MySubClass(MySuperClass):
3 def __init__(self, name, **options):
4 super(MySubClass, self).__init__(name='subclass', **options)
5
6
7class MySubClass(MySuperClass):
8
def __init__(self, name, **options):
9 super().__init__(name='subclass', **options)
stackoverflow上有更多关于super和方法解决的信息。
有着变量注释的更好的IDE建议
关于Java,C#等语言编程最令人享受的事情是IDE可以提出非常好的建议,因为每个标识符的类型在执行程序之前是已知的。
Python中这很难实现,但注释是会帮助你的
这是PyCharm带有变量声明建议的一个例子。即使在你使用的功能未被注释过的情况依旧是有效的(例如,向后的兼容性)。
更多的解包(unpacking)
现在展示如何合并两个字典:
1x = dict(a=1, b=2)
2y = dict(b=3, d=4)
3
4z = {**x, **y}
5
请参照在StackOverflow上的这一过程,与Python 2进行比较。
同样的方法对于列表,元组,以及集合(a, b, c 是可任意迭代的):
1[*a, *b, *c]
2(*a, *b, *c)
3{*a, *b, *c}
函数对于参数*args和**kwargs同样支持
1Python 3.5+
2do_something(**{**default_settings, **custom_settings})
3
4# Also possible, this code also checks there is no intersection between keys of dictionaries
5do_something(**first_args, **second_args)
具有关键字参数的面向未来的API
让我们看一下这个代码片段:
1model = sklearn.svm.SVC(2
, 'poly', 2, 4, 0.5)
很明显,代码的作者还未理解Python的编码风格(很有可能是从cpp或者rust转到Python的)。
不幸的是,这不仅仅是品味的问题,因为在SVC中改变参数顺序(添加/删除)都会破坏代码。 特别是,sklearn会不时地对许多算法参数进行重新排序/重命名以提供一致的API。 每个这样的重构都可能导致代码损坏。
在Python 3中,类库作者可能会通过使用*来要求明确命名的参数:
1class SVC(BaseSVC):
2 def __init__(self, *, C=1.0, kernel='rbf', degree=3, gamma='auto', coef0=0.0, ... )
用户现在必须指定参数名称为sklearn.svm.SVC(C=2, kernel='poly', degree=2, gamma=4, coef0=0.5)
这种机制提供了API完美结合的可靠性和灵活性
次要: `math`模块中的常量
1
2math.inf
3math.nan
4
5max_quality = -math.inf
6
7for model in trained_models:
8 max_quality = max(max_quality, compute_quality(model, data))
次要: 单一的整数类型
Python 2提供了两种基础的整数类型,int(64位有符号的整数)以及对于长整型计算的long(在C++之后就变得非常混乱)。
Python 3有着单一的类型int,其同时融合了长整型的计算。
如下为如何检查该值是整数:
1isinstance(x, numbers.Integral) # Python 2, the canonical way
2isinstance(x, (long, int)) # Python 2
3
isinstance(x, int) # Python 3, easier to remember
其他事项
Enum(枚举类)理论上是有用的,但是
协程(coroutines)听起来也非常适用于数据管道(参见David Beazley的幻灯片),但是我从来没见过代码引用它们。
Python 3 有着稳定的ABI
ABI(Application Binary Interface): 应用程序二进制接口 描述了应用程序和操作系统之间,一个应用和它的库之间,或者应用的组成部分之间的低接口。
Python 3支持unicode标识(甚至ω=Δφ/Δt也可以),但是你最好使用好的旧ASCII名称。
一些类库例如 jupyterhub(云端的jupyter),django和最新的ipython仅支持Python 3,因此对于您来说听起来无用的功能,对于您可能想要使用的库却很有用。
特定于数据科学的代码迁移问题(以及如何解决这些问题)
1 map(lambda x, (y, z): x, z, dict.items())
但是,它仍然完全适用于不同的(列表)解析:
python {x:z for x, (y, z) in d.items()}
一般来说,Python 2和Python 3之间的解析也是有着更好的“可翻译性”。
将结果转换为列表几乎可以解决所有问题。
使用python教授机器学习和数据科学的主要问题
课程讲解者应该花时间在第一讲中解释什么是迭代器,
为什么它不能像字符串一样被分割/连接/相乘/重复两次(以及如何处理它)。
我认为大多数课程讲解者曾经都乐于避开这些细节,但现在几乎不可能(再避开了)。
结论
虽然Python 2 和 Python 3 已经共存了十年有余,但是我们应该要过渡到Python 3 了。
在转向使用唯一的 Python 3 代码库之后,研究和生产的代码将会变得更剪短,更有可读性,以及明显是更加安全的。
目前大部分类库都会支持两个Python版本,我已等不及要使用新的语言特性了,也同样期待依赖包舍弃对 Python 2 支持这一光明时刻的到来。
以后的(版本)迁移会更加顺利:我们再也不会做这种不向后兼容的变化了。
相关链接
Key differences between Python 2.7 and Python 3.x
Python FAQ: How do I port to Python 3?
10 awesome features of Python that you can't use because you refuse to upgrade to Python 3
Trust me, python 3.3 is better than 2.7 (video)
Python 3 for scientists
版权声明
This text was published by Alex Rogozhnikov under CC BY-SA 3.0 License (excluding images).
Translated to Chinese by Hunter-liu (@lq920320).
学习Python和网络爬虫,关注公众号:datanami

近期文章:
给Python新手的一道面试题:如何正确读写文件
10行Python代码实现目标检测
wtfPython—Python中一些奇妙的代码
不使用异步IO爬虫,就是OUT
python爬虫-- Scrapy入门
Python爬虫库-Beautiful Soup的使用
wtfPython—Python中一些奇妙的代码
秒懂系列 | 史上最简单的Python Django入门教程