Python 已经成为机器学习和数据科学的主要编程语言,同时 Python 2 和 Python 3 共存与 Python 的生态体系内。不过,在 2019 年底,NumPy 将停止支持 Python 2.7,2018 年后的新版本只支持 Python 3。
为了让数据科学家们快速上手 Python 3,该库介绍了一些 Python 3 的新功能,供数据工作者参考。
本文首发于 Github,原文链接请见文末,AI 研习社编译如下:
pathlib 是 Python 3 的一个默认模块,它能帮你避免大量的 os.path.joins:
from pathlib import Path
dataset = 'wiki_images'
datasets_root = Path('/path/to/datasets/')
train_path = datasets_root / dataset / 'train'
test_path = datasets_root / dataset / 'test'
for image_path in train_path.iterdir():
with image_path.open() as f: # note, open is a method of Path object
# do something with an image
以前,开发者喜欢用字符串来连接(虽然简洁,但是很明显这是很不好的),而现在 pathlib 的代码则是简洁、安全并且有高可读性的。
p.exists()
p.is_dir()
p.parts
p.with_name('sibling.png') # only change the name, but keep the folder
p.with_suffix('.jpg') # only change the extension, but keep the folder and the name
p.chmod(mode)
p.rmdir()
pathlib 能帮你节省很多时间,详情请查看以下文档和参考资料:
https://docs.python.org/3/library/pathlib.html
https://pymotw.com/3/pathlib/
下图是 pycharm 中的类型提示案例:
Python 不再是仅用于编写一些小脚本的编程语言,如今的数据处理流程包括了不同的步骤并涉及到了不同的框架。
程序复杂度日益增长,类型提示功能的引入能够很好地缓解这样的状况,能够让机器帮助验证代码。
下面是个简单的例子,这些代码可以处理不同类型的数据(这就是我们喜欢的 Python 数据栈):
def repeat_each_entry(data):
""" Each entry in the data is doubled
<blah blah nobody reads the documentation till the end>
"""
index = numpy.repeat(numpy.arange(len(data)), 2)
return data[index]
这代码可以用于 numpy.array、astropy.Table、astropy.Column、bcolz、cupy、mxnet.ndarray 等等。
此代码可以用于 pandas.Series,不过下面这种方式是错的:
repeat_each_entry(pandas.Series(data=[0, 1, 2], index=[3, 4, 5])) # returns Series with Nones inside
默认情况下,函数注释不会影响你的代码,只是用于说明代码的意图。
不过,你可以在运行时使用类似 ... 这样的工具强制输入检测,这能有助于你的 debug(在很多情况下,输入提示不起作用)。
@enforce.runtime_validation
def foo(text: str) -> None:
print(text)
foo('Hi') # ok
foo(5) # fails
@enforce.runtime_validation
def any2(x: List[bool]) -> bool:
return any(x)
any ([False, False, True, False]) # True
any2([False, False, True, False]) # True
any (['False']) # True
any2(['False']) # fails
any ([False, None, "", 0]) # False
any2([False, None, "", 0]) # fails
如同之前所说,注释不影响代码的执行,只是提供一些元信息,可以让你随意地使用它。
例如,测量单位的处理对于科研领域来讲是个令人头痛的事情,astropy 包可以提供一个简单的修饰器来控制输入量的单位,并将输出转化为所需的单位。
# Python 3
from astropy import units as u
@u.quantity_input()
def frequency(speed: u.meter / u.s, wavelength: u.m) -> u.terahertz:
return speed / wavelength
frequency(speed=300_000 * u.km / u.s, wavelength=555 * u.nm)
# output: 540.5405405405404 THz, frequency of green visible light
如果你在用 python 处理科学数据表格,你应该关注下 astropy。
你也可以定义特定应用程序的修饰器,以相同的方式执行输入和输出的控制与转换。
我们来实现一个最简单的 ML 模型 —— L2 正则化线性回归(又称岭回归):
# l2-regularized linear regression: || AX - b ||^2 + alpha * ||x||^2 -> min
# Python 2
X = np.linalg.inv(np.dot(A.T, A) + alpha * np.eye(A.shape[1])).dot(A.T.dot(b))
# Python 3
X = np.linalg.inv(A.T @ A + alpha * np.eye(A.shape[1])) @ (A.T @ b)
带有 @ 的代码在不同的深度学习框架之间更具有可读性并且更容易翻译:
同样的单层感知代码 X @ W + b[None, :] 可以在 numpy、cupy、pytorch、tensorflow 中运行。
在 Python 2 里,递归文件的 globbing 并不容易,即使有 glob2 (https://github.com/miracle2k/python-glob2)模块克服了这点。从 3.5 版本开始,Python 支持递归 flag:
import glob
# Python 2
found_images = \
glob.glob('/path/*.jpg') \
+ glob.glob('/path/*/*.jpg') \
+ glob.glob('/path/*/*/*.jpg') \
+ glob.glob('/path/*/*/*/*.jpg') \
+ glob.glob('/path/*/*/*/*/*.jpg')
# Python 3
found_images = glob.glob('/path/**/*.jpg', recursive=True)
一个更好的选择是在 Python 3 中使用 pathlib:
# Python 3
found_images = pathlib.Path('/path/').glob('**/*.jpg')
是的,虽然代码里有些恼人的括号,但是这也有些优点:
print >>sys.stderr, "critical error" # Python 2
print("critical error", file=sys.stderr) # Python 3
# Python 3
print(*array, sep='\t')
print(batch, epoch, loss, accuracy, time, sep='\t')
# Python 3
_print = print # store the original print function
def print(*args, **kargs):
pass # do something useful, e.g. store output to some file
在 jupyter 中最好将每个输出记录到一个单独的文件中(方便在你断线后跟踪),所以你现在可以覆盖(override)print 了,
在下面你可以看到一个临时覆盖 print 行为的 Context Manager:
@contextlib.contextmanager
def replace_print():
import builtins
_print = print # saving old print function
# or use some other function here
builtins.print = lambda *args, **kwargs: _print('new printing', *args, **kwargs)
yield
builtins.print = _print
with replace_print():
<code here will invoke other print function>
# Python 3
result = process(x) if is_valid(x) else print('invalid item: ', x)
PEP-515(https://www.python.org/dev/peps/pep-0515/) 在数字文字中引入了下划线。在 Python3 里,下划线可用于整数、浮点数、和复数的可视化分隔。
# grouping decimal numbers by thousands
one_million = 1_000_000
# grouping hexadecimal addresses by words
addr = 0xCAFE_F00D
# grouping bits into nibbles in a binary literal
flags = 0b_0011_1111_0100_1110
# same, for string conversions
flags = int('0b_1111_0000', 2)
默认的格式化系统提供了数据实验中不需要的灵活性,由此产生的代码对于任何变化来讲要么太脆弱要么太冗长。
通常数据科学家用一种固定格式输出记录信息:
# Python 2
print('{batch:3} {epoch:3} / {total_epochs:3} accuracy: {acc_mean:0.4f}±{acc_std:0.4f} time: {avg_time:3.2f}'.format(
batch=batch, epoch=epoch, total_epochs=total_epochs,
acc_mean=numpy.mean(accuracies), acc_std=numpy.std(accuracies),
avg_time=time / len(data_batch)
))
# Python 2 (too error-prone during fast modifications, please avoid):
print('{:3} {:3} / {:3} accuracy: {:0.4f}±{:0.4f} time: {:3.2f}'.format(
batch, epoch, total_epochs, numpy.mean(accuracies), numpy.std(accuracies),
time / len(data_batch)
))
输出:
120 12 / 300 accuracy: 0.8180±0.4649 time: 56.60
在 Python 3.6 中,引入了f-strings aka 格式化的字符串文字:
# Python 3.6+
print(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}')
对于数据科学家来讲,这是一个非常方便的改变。
data = pandas.read_csv('timing.csv')
velocity = data['distance'] / data['time']
在 Python2 中,结果的类型取决于'time' 和 'distance' 是否储存为整数,在 Python3 中,结果在两种情况下都正确,因为结果是浮点型。
# All these comparisons are illegal in Python 3
3 < '3'
2 < None
(3, 4) < (3, None)
(4, 5) < [4, 5]
# False in both Python 2 and Python 3
(4, 5) == [4, 5]
s = '您好'
print(len(s))
print(s[:2])
输出:
x = u'со'
x += 'co' # ok
x += 'со' # fail
输出结果在 Python2 失败,Python3 正常。
在 Python3 中,str 是 unicode 字符串,用于非英文文本的 NLP 更加方便。
在默认情况下,CPython 3.6+ 中的 dicts 的行为类似 OrderedDict。这保留了 dict 理解的顺序(以及一些其他操作,比如在 json 序列化和反序列化中的一些操作)。
import json
x = {str(i):i for i in range(5)}
json.loads(json.dumps(x))
# Python 2
{u'1': 1, u'0': 0, u'3': 3, u'2': 2, u'4': 4}
# Python 3
{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
这同样适用于 **kwargs(在 Python 3.6+ 里),在参数之间保持了同样的顺序。
当涉及到数据管道时,顺序至关重要,而以前我们必须用更加麻烦的方式来编写:
from torch import nn
# Python 2
model = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(1,20,5)),
('relu1', nn.ReLU()),
('conv2', nn.Conv2d(20,64,5)),
('relu2', nn.ReLU())
]))
# Python 3.6+, how it *can* be done, not supported right now in pytorch
model = nn.Sequential(
conv1=nn.Conv2d(1,20,5),
relu1=nn.ReLU(),
conv2=nn.Conv2d(20,64,5),
relu2=nn.ReLU())
)
# handy when amount of additional stored info may vary between experiments, but the same code can be used in all cases
model_paramteres, optimizer_parameters, *other_params = load(checkpoint_name)
# picking two last values from a sequence
*prev, next_to_last, last = values_history
# This also works with any iterables, so if you have a function that yields e.g. qualities,
# below is a simple way to take only last two values from a list
*prev, next_to_last, last = iter_train(args)
# Python 2
import cPickle as pickle
import numpy
print len(pickle.dumps(numpy.random.normal(size=[1000, 1000])))
# result: 23691675
# Python 3
import pickle
import numpy
len(pickle.dumps(numpy.random.normal(size=[1000, 1000])))
# result: 8000162
更少的空间,更快的速度。实际上,类似的压缩可以通过 protocol=2 参数来实现,但是用户通常忽略掉了这个选项。
labels = <initial_value>
predictions = [model.predict(data) for data, labels in dataset]
# labels are overwritten in Python 2
# labels are not affected by comprehension in Python 3
Python2 的 super(...) 经常出错
# Python 2
class MySubClass(MySuperClass):
def __init__(self, name, **options):
super(MySubClass, self).__init__(name='subclass', **options)
# Python 3
class MySubClass(MySuperClass):
def __init__(self, name, **options):
super().__init__(name='subclass', **options)
更多 super 的解析请移步:
https://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods
在 Java,C#等语言编程里,最令人愉快的事情是 IDE 可以提出非常好的建议,因为在执行程序之前每个标识符的类型是已知的。
在 Python 里很难实现,但是注释会帮你:
下面是你如何合并两个 dicts:
x = dict(a=1, b=2)
y = dict(b=3, d=4)
# Python 3.5+
z = {**x, **y}
# z = {'a': 1, 'b': 3, 'd': 4}, note that value for `b` is taken from the latter dict.
比较 Python2 中的差异,请查看:
https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression
同样的方法也适用于列表、元组和集合(a、b、c 是任意 iterables):
[*a, *b, *c] # list, concatenating
(*a, *b, *c) # tuple, concatenating
{*a, *b, *c} # set, union
函数也支持* args和** kwargs:
Python 3.5+
do_something(**{**default_settings, **custom_settings})
# Also possible, this code also checks there is no intersection between keys of dictionaries
do_something(**first_args, **second_args)
思考下这个代码片段:
model = sklearn.svm.SVC(2, 'poly', 2, 4, 0.5)
很明显,这个作者好没有形成 Python 的编码风格(他很有可能刚从 C++ 或者 rust 跳转到 Python 开发上)。不幸的是,这不是编码风格的问题,因为你改变 SVC 中参数的顺序将打破这段代码。特别是,sklearn 会不时对众多算法参数重排序/重命名来提供一致的 API,每次这样的重构都会破坏代码。
在 Python3 里,库作者可能需要用 * 来显示命名参数。
class SVC(BaseSVC):
def __init__(self, *, C=1.0, kernel='rbf', degree=3, gamma='auto', coef0=0.0, ... )
Python2 和 Python3 共存了将近十年,现在我们应该转移到 Python3上,研究和产品开发代码转移到 Python3-Only 的代码库之后会更简洁、更安全、更易读。
Via:https://github.com/arogozhnikov/python3_with_pleasure