机器之心编译
参与:卓汇源、思源
随着机器学习的兴起,Python 逐步成为了「最受欢迎」的措辞。它大略易用、逻辑明确并拥有海量的扩展包,因此其不仅成为机器学习与数据科学的首选措辞,同时在网页、数据爬取可科学研究等方面成为不二选择。此外,很多入门级的机器学习开拓者都是跟随大流选择 Python,但到底为什么要选择 Python 便是本文的核心内容。

本教程的目的是让你相信两件事:首先,Python 是一种非常棒的编程措辞;其次,如果你是一名科学家,Python 很可能值得你去学习。本教程并非想要解释 Python 是一种万能的措辞;相反,作者明确谈论了在几种情形下,Python 并不是一种明智的选择。本教程的目的只是供应对 Python 一些核心特色的评论,并阐述作为一种通用的科学打算措辞,它比其他常用的替代方案(最著名的是 R 和 Matlab)更有上风。
本教程的别的部分假定你已经有了一些编程履历,如果你非常精通其他以数据为中央的措辞(如 R 或 Matlab),理解本教程就会非常随意马虎。本教程不能算作一份关于 Python 的先容,且文章重点在于为什么该当学习 Python 而不是若何写 Python 代码(只管其他地方有大量的精良教程)。
概述
Python 是一种广泛利用、易于学习、高等、通用的动态编程措辞。这很令人满意,以是接下来分开谈论一些特色。
Python(相对来说)易于学习
编程很难,因此从绝对意义上来说,除非你已经拥有编程履历,否则编程措辞难以学习。但是,相对而言,Python 的高等属性(见下一节)、语法可读性和语义直白性使得它比其他措辞更随意马虎学习。例如,这是一个大略 Python 函数的定义(故意未注释),它将一串英语单词转换为(crummy)Pig Latin:
def pig_latin(text):
''' Takes in a sequence of words and converts it to (imperfect) pig latin. '''
word_list = text.split(' ')
output_list = []
for word in word_list:
word = word.lower()
if word.isalpha():
first_char = word[0]
if first_char in 'aeiou':
word = word + 'ay'
else:
word = word[1:] + first_char + 'yay'
output_list.append(word)
pygged = ' '.join(output_list)
return pygged
以上函数事实上无法天生完备有效的 Pig Latin(假设存在「有效 Pig Latin」),但这没有关系。有些情形下它是可行的:
test1 = pig_latin(\公众let us see if this works\"大众)
print(test1)
抛开 Pig Latin 不说,这里的重点只是,出于几个缘故原由,代码是很随意马虎阅读的。首先,代码是在高等抽象中编写的(下面将详细先容),因此每行代码都会映射到一个相称直不雅观的操作。这些操作可以是「取这个单词的第一个字符」,而不是映射到一个没那么直不雅观的低级操作,例如「为一个字符预留一个字节的内存,稍后我会传入一个字符」。其次,掌握构造(如,for—loops,if—then 条件等)利用诸如「in」,「and」和「not」的大略单词,其语义相对靠近其自然英语含义。第三,Python 对缩进的严格掌握强加了一种使代码可读的规范,同时防止了某些常见的缺点。第四,Python 社区非常强调遵照样式规定和编写「Python 式的」代码,这意味着比较利用其他措辞的程序员而言,Python 程序员更方向于利用同等的命名规定、行的长度、编程习气和其他许多类似特色,它们共同使别人的代码更易阅读(只管这可以说是社区的一个特色而不是措辞本身)。
Python 是一种高等措辞
与其他许多措辞比较,Python 是一种相对「高等」的措辞:它不须要(并且在许多情形下,不许可)用户担心太多底层细节,而这是其他许多措辞须要去处理的。例如,假设我们想创建一个名为「my_box_of_things」的变量当作我们所用东西的容器。我们事先不知道我们想在盒子中保留多少工具,同时我们希望在添加或删除工具时,工具数量可以自动增减。以是这个盒子须要霸占一个可变的空间:在某个韶光点,它可能包含 8 个工具(或「元素」),而在另一个韶光点,它可能包含 257 个工具。在像 C 这样的底层措辞中,这个大略的哀求就已经给我们的程序带来了一些繁芜性,由于我们须要提前声明盒子须要霸占多少空间,然后每次我们想要增加盒子须要的空间时,我么须要明确创建一个霸占更多空间的全新的盒子,然后将所有东西拷贝到个中。
比较之下,在 Python 中,只管在底层这些过程或多或少会发生(效率较低),但我们在利用高等措辞编写时并不须要担心这一部分。从我们的角度来看,我们可以创建自己的盒子并根据喜好添加或删除工具:
# Create a box (really, a 'list') with 5 things# Create
my_box_of_things = ['Davenport', 'kettle drum', 'swallow-tail coat', 'table cloth', 'patent leather shoes']
print(my_box_of_things)
['Davenport', 'kettle drum', 'swallow-tail coat', 'table cloth', 'patent leather shoes']
# Add a few more things
my_box_of_things += ['bathing suit', 'bowling ball', 'clarinet', 'ring']
# Maybe add one last thing
my_box_of_things.append('radio that only needs a fuse')
# Let's see what we have...
print(my_box_of_things)
更一样平常来说,Python(以及根据定义的其他所有高等措辞)方向于隐蔽须要在底层措辞中明确表达的各种去世记硬背的声明。这使得我们可以编写非常紧凑、清晰的代码(只管它常日以降落性能为代价,由于内部不再可访问,因此优化变得更加困难)。
例如,考虑从文件中读取纯文本这样看似大略的行为。对付与文件系统直接打仗而伤痕累累的开拓者来说,从观点上看彷佛只须要两个大略的操作就可以完成:首先打开一个文件,然后从个中读取。实际过程远不止这些,并且比 Python 更底层的措辞常日逼迫(或至少是鼓励)我们去承认这一点。例如,这是在 Java 中从文件中读取内容的规范(只管肯定不是最简洁的)方法:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFile {
public static void main(String[] args) throws IOException{
String fileContents = readEntireFile(\"大众./foo.txt\"大众);
}
private static String readEntireFile(String filename) throws IOException {
FileReader in = new FileReader(filename);
StringBuilder contents = new StringBuilder();
char[] buffer = new char[4096];
int read = 0;
do {
contents.append(buffer, 0, read);
read = in.read(buffer);
} while (read >= 0);
return contents.toString();
}
}
你可以看到我们不得不做一些令人苦恼的事,例如导入文件读取器、为文件中的内容创建一个缓存,以块的形式读取文件块并将它们分配到缓存中等等。比较之下,在 Python 中,读取文件中的全部内容只须要如下代码:
# Read the contents of \"大众hello_world.txt\公众
text = open(\公众hello_world.txt\"大众).read()
当然,这种简洁性并不是 Python 独占的;还有其他许多高等措辞同样隐蔽了大略要求所暗含的大部分令人讨厌的内部过程(如,Ruby,R,Haskell 等)。但是,相对来说比较少有其他措辞能与接下来磋商的 Python 特色相媲美。
Python 是一种通用措辞
根据设计,Python 是一种通用的措辞。也便是说,它旨在许可程序员在任何领域编写险些所有类型的运用,而不是专注于一类特定的问题。在这方面,Python 可以与(相对)特定领域的措辞进行比拟,如 R 或 PHP。这些措辞原则上可用于很多环境,但仍针对特定用例进行了明确优化(在这两个示例中,分别用于统计和网络后端开拓)。
Python 常日被亲切地成为「所有事物的第二个最好的措辞」,它很好地捕捉到了这样的感情,只管在很多情形下 Python 并不是用于特定问题的最佳措辞,但它常日具有足够的灵巧性和良好的支持性,使得人们仍旧可以相对有效地办理问题。事实上,Python 可以有效地运用于许多不同的运用中,这使得学习 Python 成为一件相称有代价的事。由于作为一个软件开拓职员,能够利用单一措辞实现所有事情,而不是必须根据所实行的项目在不同措辞和环境间进行切换,是一件非常棒的事。
标准库
通过浏览标准库中可用的浩瀚模块列表,即 Python 阐明器自带的工具集(没有安装第三方软件包),这可能是最随意马虎理解 Python 通用性的办法。若考虑以下几个示例:
os: 系统操尴尬刁难象re:正则表达collections:有用的数据构造multiprocessing:大略的并行化工具pickle:大略的序列化json:读和写 JSONargparse:命令行参数解析functools:函数化编程工具datetime:日期和韶光函数cProfile:剖析代码的基本工具这张列表乍一看并不令人印象深刻,但对付 Python 开拓者来说,利用它们是一个相对常见的经历。很多时候用谷歌搜索一个看似主要乃至有点深奥的问题,我们很可能找到隐蔽在标准库模块内的内置办理方案。
JSON,大略的方法
例如,假设你想从 web.JSON 中读取一些 JSON 数据,如下所示:
data_string = '''
[
{
\"大众_id\"大众: \"大众59ad8f86450c9ec2a4760fae\"大众,
\"大众name\"大众: \公众Dyer Kirby\"大众,
\"大众registered\"大众: \"大众2016-11-28T03:41:29 +08:00\"大众,
\"大众latitude\公众: -67.170365,
\"大众longitude\"大众: 130.932548,
\"大众favoriteFruit\"大众: \"大众durian\"大众
},
{
\"大众_id\"大众: \公众59ad8f8670df8b164021818d\"大众,
\"大众name\公众: \"大众Kelly Dean\"大众,
\"大众registered\公众: \"大众2016-12-01T09:39:35 +08:00\"大众,
\公众latitude\公众: -82.227537,
\"大众longitude\"大众: -175.053135,
\公众favoriteFruit\公众: \"大众durian\"大众
}
]
'''
我们可以花一些韶光自己编写 json 解析器,或试着去找一个有效读取 json 的第三方包。但我们很可能是在摧残浪费蹂躏韶光,由于 Python 内置的 json 模块已经能完备知足我们的须要:
import json
data = json.loads(data_string)
print(data)
'''
[{'_id': '59ad8f86450c9ec2a4760fae', 'name': 'Dyer Kirby', 'registered': '2016-11-28T03:41:29 +08:00', 'latitude': -67.170365, 'longitude': 130.932548, 'favoriteFruit': 'durian'}, {'_id': '59ad8f8670df8b164021818d', 'name': 'Kelly Dean', 'registered': '2016-12-01T09:39:35 +08:00', 'latitude': -82.227537, 'longitude': -175.053135, 'favoriteFruit': 'durian'}]
请把稳,在我们能于 json 模块内利用 loads 函数前,我们必须导入 json 模块。这种必须将险些所有功能模块明确地导入命名空间的模式在 Python 中相称主要,且基本命名空间中可用的内置函数列表非常有限。许多用过 R 或 Matlab 的开拓者会在刚打仗时感到恼火,由于这两个包的全局命名空间包含数百乃至上千的内置函数。但是,一旦你习气于输入一些额外字符,它就会使代码更易于读取和管理,同时命名冲突的风险(R 措辞中常常涌现)被大大降落。
精良的外部支持
当然,Python 供应大量内置工具来实行大量操作并不虞味着总须要去利用这些工具。可以说比 Python 丰富的标准库更大的卖点是弘大的 Python 开拓者社区。多年来,Python 一贯是天下上最盛行的动态编程措辞,开拓者社区也贡献了浩瀚高质量的安装包。
如下 Python 软件包在不同领域内供应了被广泛利用的办理方案(这个列表在你阅读本文的时候可能已经由时了!
):
Python 的一个优点是有出色的软件包管理生态系统。虽然在 Python 中安装包常日比在 R 或 Matlab 中更难,这紧张是由于 Python 包每每具有高度的模块化和/或更多依赖于系统库。但原则上至少大多数 Python 的包可以利用 pip 包管理器通过命令提示符安装。更繁芜的安装程序和包管理器,如 Anaconda 也大大减少了配置新 Python 环境时产生的痛楚。
Python 是一种(相对)快速的措辞
这可能令人有点惊异:从表面上看,Python 是一种快速措辞的说法看起来很屈曲。由于在标准测试时,和 C 或 Java 这样的编译措辞比较,Python 常日会卡顿。毫无疑问,如果速率至关主要(例如,你正在编写 3D 图形引擎或运行大规模的流体动力学仿照实验),Python 可能不会成为你最优选择的措辞,乃至不会是第二好的措辞。但在实际中,许多科学家事情流程中的限定成分不是运行韶光而是开拓韶光。一个花费一个小时运行但只须要 5 分钟编写的脚本常日比一个花费 5 秒钟运行但是须要一个星期编写和调试的脚本更合意。此外,正如我们将不才面看到的,纵然我们所用的代码都用 Python 编写,一些优化操作常日可以使其运行速率险些与基于 C 的办理方案一样快。实际上,对大多数科学家家来说,基于 Python 的办理方案不足快的情形并不是很多,而且随着工具的改进,这种情形的数量正在急剧减少。
不要重复做功
软件开拓的一样平常原则是该当尽可能避免做重复事情。当然,有时候是没法避免的,并且在很多情形下,为问题编写自己的办理方案或创建一个全新的工具是故意义的。但一样平常来说,你自己编写的 Python 代码越少,性能就越好。有以下几个缘故原由:
Python 是一种成熟的措辞,以是许多现有的包有大量的用户根本并且经由大量优化。例如,对 Python 中大多数核心科学库(numpy,scipy,pandas 等)来说都是如此。大多数 Python 包实际上是用 C 措辞编写的,而不是用 Python 编写的。对付大多数标准库,当你调用一个 Python 函数时,实际上很大可能你是在运行具有 Python 接口的 C 代码。这意味着无论你办理问题的算法有多精妙,如果你完备用 Python 编写,而内置的办理方案是用 C 措辞编写的,那你的性能可能不如内置的方案。例如,以下是运行内置的 sum 函数(用 C 编写):# Create a list of random floats
import random
my_list = [random.random() for i in range(10000)]
# Python's built-in sum() function is pretty fast
%timeit sum(my_list)
47.7 µs ± 4.5 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
从算法上来说,你没有太多办法来加速任意数值列表的加和打算。以是你可能会想这是什么鬼,你也容许以用 Python 自己写加和函数,大概这样可以封装内置 sum 函数的开销,以防它进行任何内部验证。嗯……并非如此。
def ill_write_my_own_sum_thank_you_very_much(l):
s = 0
for elem in my_list:
s += elem
return s
%timeit ill_write_my_own_sum_thank_you_very_much(my_list)
331 µs ± 50.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
至少在这个例子中,运行你自己大略的代码很可能不是一个好的办理方案。但这不虞味着你必须利用内置 sum 函数作为 Python 中的性能上限!
由于 Python 没有针对涉及大型输入的数值运算进行优化,因此内置方法在加和大型列表时是表现次优。在这种情形下我们该当做的是提问:「是否有其他一些 Python 库可用于对潜在的大型输入进行数值剖析?」正如你可能想的那样,答案是肯定的:NumPy 包是 Python 的科学生态系统中的紧张身分,Python 中的绝大多数科学打算包都以某种办法构建在 NumPy 上,它包含各种能帮助我们的打算函数。
在这种情形下,新的办理方案是非常大略的:如果我们将纯 Python 列表转化为 NumPy 数组,我们就可以立即调用 NumPy 的 sum 方法,我们可能期望它该当比核心的 Python 实现更快(技能上讲,我们可以传入一个 Python 列表到 numpy.sum 中,它会隐式地将其转换为数组,但如果我们打算复用该 NumPy 数组,最好明确地转化它)。
import numpy as np
my_arr = np.array(my_list)
%timeit np.sum(my_arr)
7.92 µs ± 1.15 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
因此大略地切换到 NumPy 可加快一个数量级的列表加和速率,而不须要自己去实现任何东西。
须要更快的速率?
当然,有时候纵然利用所有基于 C 的扩展包和高度优化的实现,你现有的 Python 代码也无法快速减少韶光。在这种情形下,你的下意识反应可能是放弃并转化到一个「真正」的措辞。并且常日,这是一种完备合理的本能。但是在你开始利用 C 或 Java 移植代码前,你须要考虑一些不那么费力的方法。
利用 Python 编写 C 代码
首先,你可以考试测验编写 Cython 代码。Cython 是 Python 的一个超集(superset),它许可你将(某些)C 代码直接嵌入到 Python 代码中。Cython 不以编译的办法运行,相反你的 Python 文件(或个中特定的某部分)将在运行前被编译为 C 代码。实际的结果是你可以连续编写看起来险些完备和 Python 一样的代码,但仍旧可以从 C 代码的合理引入中得到性能提升。特殊是大略地供应 C 类型的声明常日可以显著提高性能。
以下是我们大略加和代码的 Cython 版本:
# Jupyter extension that allows us to run Cython cell magics
%load_ext Cython
The Cython extension is already loaded. To reload it, use:
%reload_ext Cython
%%%%cythoncython
defdef ill_write_my_own_cython_sum_thank_you_very_muchill_write (list arr):
cdef int N = len(arr)
cdef float x = arr[0]
cdef int i
for i in range(1 ,N):
x += arr[i]
return x
%timeit ill_write_my_own_cython_sum_thank_you_very_much(my_list)
227 µs ± 48.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
关于 Cython 版本有几点须要把稳一下。首先,在你第一次实行定义该方法的单元时,须要很少的(但值得把稳的)韶光来编译。那是由于,与纯粹的 Python 不同,代码在实行时不是逐行解译的;相反,Cython 式的函数必须先编译成 C 代码才能调用。
其次,虽然 Cython 式的加和函数比我们上面写的大略的 Python 加和函数要快,但仍旧比内置求和方法和 NumPy 实现慢得多。然而,这个结果更有力地解释了我们特定的实现过程和问题的实质,而不是 Cython 的一样平常好处;在许多情形下,一个有效的 Cython 实现可以轻易地将运行韶光提升一到两个数量级。
利用 NUMBA 进行清理
Cython 并不是提升 Python 内部性能的唯一方法。从开拓的角度来看,另一种更大略的方法是依赖于即时编译,个中一段 Python 代码在第一次调用时被编译成优化的 C 代码。近年来,在 Python 即时编译器上取得了很大进展。大概最成熟的实现可以在 numba 包中找到,它供应了一个大略的 jit 润色器,可以轻易地结合其他任何方法。
我们之前的示例并没有强调 JITs 可以产生多大的影响,以是我们转向一个轻微繁芜点的问题。这里我们定义一个被称为 multiply_randomly 的新函数,它将一个一维浮点数数组作为输入,并将数组中的每个元素与其他任意一个随机选择的元素相乘。然后它返回所有随机相乘的元素和。
让我们从定义一个大略的实现开始,我们乃至都不采取向量化来代替随机相乘操作。相反,我们大略地遍历数组中的每个元素,从中随机挑选一个其他元素,将两个元素相乘并将结果分配给一个特定的索引。如果我们用基准问题测试这个函数,我们会创造它运行得相称慢。
import numpy as np
def multiply_randomly_naive(l):
n = l.shape[0]
result = np.zeros(shape=n)
for i in range(n):
ind = np.random.randint(0, n)
result[i] = l[i] l[ind]
return np.sum(result)
%timeit multiply_randomly_naive(my_arr)
25.7 ms ± 4.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
在我们即时编译之前,我们该当首先自问是否上述函数可以用更加符合 NumPy 形式的方法编写。NumPy 针对基于数组的操作进行了优化,因此该当不惜统统代价地避免利用循环操作,由于它们会非常慢。幸运的是,我们的代码非常随意马虎向量化(并且易于阅读):
def multiply_randomly_vectorized(l):
n = len(l)
inds = np.random.randint(0, n, size=n)
result = l l[inds]
return np.sum(result)
%timeit multiply_randomly_vectorized(my_arr)
234 µs ± 50.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
在作者的机器上,向量化版本的运行速率比循环版本的代码快大约 100 倍。循环和数组操作之间的这种性能差异对付 NumPy 来说是非常范例的,因此我们要在算法上思考你所做的事的主要性。
假设我们不是花韶光重构我们朴素的、缓慢的实现,而是大略地在我们的函数上加一个润色器去见告 numba 库我们要在第一次调用它时将函数编译为 C。字面上,下面的函数 multiply_randomly_naive_jit 与上面定义的函数 multiply_randomly_naive 之间的唯一差异是 @jit 润色器。当然,4 个小字符是没法造成那么大的差异的。对吧?
import numpy as np
from numba import jit
@jit
def multiply_randomly_naive_jit(l):
n = l.shape[0]
result = np.zeros(shape=n)
for i in range(n):
ind = np.random.randint(0, n)
result[i] = l[i] l[ind]
return np.sum(result)
%timeit multiply_randomly_naive_jit(my_arr)
135 µs ± 22.4 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
令人惊异的是,JIT 编译版本的朴素函数事实上比向量化的版本跑得更快。
有趣的是,将 @jit 润色器运用于函数的向量化版本(将其作为联系留给读者)并不能供应更多帮助。在 numba JIT 编译器用于我们的代码之后,Python 实现的两个版本都以同样的速率运行。因此,至少在这个例子中,即时编译不仅可以绝不费力地为我们供应类似 C 的速率,而且可以避免以 Python 式地去优化代码。
这可能是一个相称有力的结论,由于(a)现在 numba 的 JIT 编译器只覆盖了 NumPy 特色的一部分,(b)不能担保编译的代码一定比解译的代码运行地更快(只管这常日是一个有效的假设)。这个例子真正的目的是提醒你,在你流传宣传它慢到无法去实现你想要做的事之前,实在你在 Python 中有许多可用的选择。值得把稳的是,如 C 集成和即时编译,这些性能特色都不是 Python 独占的。Matlab 最近的版本自动利用即时编译,同时 R 支持 JIT 编译(通过外部库)和 C ++ 集成(Rcpp)。
Python 是天生面向工具的
纵然你正在做的只是编写一些简短的脚本去解析文本或挖掘一些数据,Python 的许多好处也很随意马虎领会到。在你开始编写相对大型的代码片段前,Python 的最佳功能之一可能并不明显:Python 具有设计非常优雅的基于工具的数据模型。事实上,如果你查看底层,你会创造 Python 中的统统都是工具。乃至函数也是工具。当你调用一个函数的时候,你事实上正在调用 Python 中每个工具都运行的 __call__ 方法:
def double(x):
return x2
# Lists all object attributes
dir(double)
['__annotations__',
'__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__get__',
'__getattribute__',
'__globals__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__kwdefaults__',
'__le__',
'__lt__',
'__module__',
'__name__',
'__ne__',
'__new__',
'__qualname__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']
事实上,由于 Python 中的统统都是工具,Python 中的所有内容遵照相同的核心逻辑,实现相同的基本 API,并以类似的办法进行扩展。工具模型也恰好非常灵巧:可以很随意马虎地定义新的工具去实现故意思的事,同时仍旧表现得相对可预测。大概并不奇怪,Python 也是编写特定领域措辞(DSLs)的一个绝佳选择,由于它许可用户在很大程度上重载和重新定义现有的功能。
魔术方法
Python 工具模型的核心部分是它利用「魔术」方法。这些在工具上实现的分外方法可以变动 Python 工具的行为——常日以主要的办法。魔术方法(Magic methods)常日以双下划线开始和结束,一样平常来说,除非你知道自己在做什么,否则不要轻易修改它们。但一旦你真的开始改了,你就可以做些相称了不起的事。
举个大略的例子,我们来定义一个新的 Brain 工具。首先,Barin 不会进行任何操作,它只会待在那儿礼貌地发呆。
class Brain(object):
def __init__(self, owner, age, status):
self.owner = owner
self.age = age
self.status = status
def __getattr__(self, attr):
if attr.startswith('get_'):
attr_name = attr.split('_')[1]
if hasattr(self, attr_name):
return lambda: getattr(self, attr_name)
raise AttributeError
在 Python 中,__init__ 方法是工具的初始化方法——当我们考试测验创建一个新的 Brain 实例时,它会被调用。常日你须要在编写新类时自己实现__init__,以是如果你之前看过 Python 代码,那__init__ 可能看起来就比较熟习了,本文就不再赘述。
比较之下,大多数用户很少明确地实现__getattr__方法。但它掌握着 Python 工具行为的一个非常主要的部分。详细来说,当用户试图通过点语法(如 brain.owner)访问类属性,同时这个属性实际上并不存在时,__getattr__方法将会被调用。此方法的默认操作仅是引发一个缺点:
# Create a new Brain instance
brain = Brain(owner=\"大众Sue\"大众, age=\公众62\"大众, status=\"大众hanging out in a jar\公众)
print(brain.owner)
---------------------------------------------------------------------------
sue
print(brain.gender)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-136-52813a6b3567> in <module>()
----> 1 print(brain.gender)
<ipython-input-133-afe64c3e086d> in __getattr__(self, attr)
12 if hasattr(self, attr_name):
13 return lambda: getattr(self, attr_name)
---> 14 raise AttributeError
AttributeError:
主要的是,我们不用忍受这种行为。假设我们想创建一个替代接口用于通过以「get」开头的 getter 方法从 Brain 类的内部检索数据(这是许多其他措辞中的常见做法),我们当然可以通过名字(如 get_owner、get_age 等)显式地实现 getter 方法。但假设我们很
在上面的代码片段中,我们的 __getattr__ 实现首先检讨了传入属性的名称。如果名称以 get_ 开头,我们将检讨工具内是否存在期望属性的名称。如果确实存在,则返回该工具。否则,我们会引发缺点的默认操作。这让我们可以做一些看似猖獗的事,比如:
print(brain.get_owner())
其他不可思议的方法许可你动态地掌握工具行为的其他各种方面,而这在其他许多措辞中你没法做到。事实上,由于 Python 中的统统都是工具,乃至数学运算符实际上也是对工具的秘密方法调用。例如,当你用 Python 编写表达式 4 + 5 时,你实际上是在整数工具 4 上调用 __add__,其参数为 5。如果我们乐意(并且我们该当小心谨慎地行使这项权利!
),我们能做的是创建新的特定领域的「迷你措辞」,为通用运算符注入全新的语义。
举个大略的例子,我们来实现一个表示单一 Nifti 容积的新类。我们将依赖继续来实现大部分事情;只需从 nibabel 包中继续 NiftierImage 类。我们要做的便是定义 __and__ 和 __or__ 方法,它们分别映射到 & 和 | 运算符。看看在实行以下几个单元前你是否搞懂了这段代码的浸染(可能你须要安装一些包,如 nibabel 和 nilearn)。
from nibabel import Nifti1Image
from nilearn.image import new_img_like
from nilearn.plotting import plot_stat_map
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
class LazyMask(Nifti1Image):
''' A wrapper for the Nifti1Image class that overloads the & and | operators
to do logical conjunction and disjunction on the image data. '''
def __and__(self, other):
if self.shape != other.shape:
raise ValueError(\"大众Mismatch in image dimensions: %s vs. %s\"大众 % (self.shape, other.shape))
data = np.logical_and(self.get_data(), other.get_data())
return new_img_like(self, data, self.affine)
def __or__(self, other):
if self.shape != other.shape:
raise ValueError(\"大众Mismatch in image dimensions: %s vs. %s\公众 % (self.shape, other.shape))
data = np.logical_or(self.get_data(), other.get_data())
return new_img_like(self, data, self.affine)
img1 = LazyMask.load('image1.nii.gz')
img2 = LazyMask.load('image2.nii.gz')
result = img1 & img2
fig, axes = plt.subplots(3, 1, figsize=(15, 6))
p = plot_stat_map(img1, cut_coords=12, display_mode='z', title='Image 1', axes=axes[0], vmax=3)
plot_stat_map(img2, cut_coords=p.cut_coords, display_mode='z', title='Image 2', axes=axes[1], vmax=3)
p = plot_stat_map(result, cut_coords=p.cut_coords, display_mode='z', title='Result', axes=axes[2], vmax=3)
Python 社区
我在这里提到的 Python 的末了一个特色便是它精良的社区。当然,每种紧张的编程措辞都有一个大型的社区致力于该措辞的开拓、运用和推广;关键是社区内的人是谁。一样平常来说,环绕编程措辞的社区更能反响用户的兴趣和专业根本。对付像 R 和 Matlab 这样相对特定领域的措辞来说,这意味着为措辞贡献新工具的人中很大一部分不是软件开拓职员,更可能是统计学家、工程师和科学家等等。当然,统计学家和工程师没什么不好。例如,与其他措辞比较,统计学家较多的 R 生态系统的上风之一便是 R 具有一系列统计软件包。
然而,由统计或科学背景用户所主导的社区存在缺陷,即这些用户常日未受过软件开拓方面的演习。因此,他们编写的代码质量每每比较低(从软件的角度看)。专业的软件工程师普遍采取的最佳实践和习气在这种未经培训的社区中并不出众。例如,CRAN 供应的许多 R 包短缺类似自动化测试的东西——除了最小的 Python 软件包之外,这险些是闻所未闻的。其余在风格上,R 和 Matlab 程序员编写的代码每每在人与人之间的同等性方面要低一些。结果是,在其他条件相同的情形下,用 Python 编写软件每每比用 R 编写的代码具备更高的稳健性。虽然 Python 的这种上风无疑与措辞本身的内在特色无关(一个人可以利用任何措辞(包括 R、Matlab 等)编写出极高质量的代码),但仍旧存在这样的情形,强调共同老例和最佳实践规范的开拓职员社区每每会使大家编写出更清晰、更规范、更高质量的代码。
结论
Python 太棒了。