人生苦短,为何我要用Python?

原文连接java

摘要: 随着机器学习的兴起,Python 逐步成为了「最受欢迎」的语言。它简单易用、逻辑明确并拥有海量的扩展包,所以其不只成为机器学习与数据科学的首选语言,同时在网页、数据爬取可科学研究等方面成为不二选择。此外,不少入门级的机器学习开发者都是跟随大流选择 Python,但到底为何要选择 Python 就是本文的核心内容。python

随着机器学习的兴起,Python 逐步成为了「最受欢迎」的语言。它简单易用、逻辑明确并拥有海量的扩展包,所以其不只成为机器学习与数据科学的首选语言,同时在网页、数据爬取可科学研究等方面成为不二选择。此外,不少入门级的机器学习开发者都是跟随大流选择 Python,但到底为何要选择 Python 就是本文的核心内容。git

本教程的目的是让你相信两件事:首先,Python 是一种很是棒的编程语言;其次,若是你是一名科学家,Python 极可能值得你去学习。本教程并不是想要说明 Python 是一种万能的语言;相反,做者明确讨论了在几种状况下,Python 并非一种明智的选择。本教程的目的只是提供对 Python 一些核心特征的评论,并阐述做为一种通用的科学计算语言,它比其余经常使用的替代方案(最著名的是 R 和 Matlab)更有优点。程序员

本教程的其他部分假定你已经有了一些编程经验,若是你很是精通其余以数据为中心的语言(如 R 或 Matlab),理解本教程就会很是容易。本教程不能算做一份关于 Python 的介绍,且文章重点在于为何应该学习 Python 而不是怎样写 Python 代码(尽管其余地方有大量的优秀教程)。web

概述算法

Python 是一种普遍使用、易于学习、高级、通用的动态编程语言。这很使人满意,因此接下来分开讨论一些特征。编程

Python(相对来讲)易于学习json

编程很难,所以从绝对意义上来讲,除非你已经拥有编程经验,不然编程语言难以学习。可是,相对而言,Python 的高级属性(见下一节)、语法可读性和语义直白性使得它比其余语言更容易学习。例如,这是一个简单 Python 函数的定义(故意未注释),它将一串英语单词转换为(crummy)Pig Latin:flask

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:读和写 JSON
argparse:命令行参数解析
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 软件包在不一样领域内提供了被普遍使用的解决方案(这个列表在你阅读本文的时候可能已通过时了!):

Web 和 API 开发:flask,Django,Falcon,hug
爬取数据和解析文本/标记: requests,beautifulsoup,scrapy
天然语言处理(NLP):nltk,gensim,textblob
数值计算和数据分析:numpy,scipy,pandas,xarray
机器学习:scikit-learn,Theano,Tensorflow,keras
图像处理:pillow,scikit-image,OpenCV
做图:matplotlib,seaborn,ggplot,Bokeh
等等

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 x*2

# 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 方法。但假设咱们很懒,而且不想为每一个属性编写一个显式的 getter。此外,咱们可能想要为已经建立的 Brains 类添加新的属性(如,brain.foo = 4),在这种状况下,咱们不须要提早为那些未知属性建立 getter 方法(请注意,在现实世界中,这些是为何咱们接下来要这么作的可怕理由;固然这里彻底是为了举例说明)。咱们能够作的是,当用户请求任意属性时,经过指示 Brain 类的操做去改变它的行为。

在上面的代码片断中,咱们的 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)

 

image

Python 社区

我在这里提到的 Python 的最后一个特征就是它优秀的社区。固然,每种主要的编程语言都有一个大型的社区致力于该语言的开发、应用和推广;关键是社区内的人是谁。通常来讲,围绕编程语言的社区更能反映用户的兴趣和专业基础。对于像 R 和 Matlab 这样相对特定领域的语言来讲,这意味着为语言贡献新工具的人中很大一部分不是软件开发人员,更多是统计学家、工程师和科学家等等。固然,统计学家和工程师没什么很差。例如,与其余语言相比,统计学家较多的 R 生态系统的优点之一就是 R 具备一系列统计软件包。

然而,由统计或科学背景用户所主导的社区存在缺点,即这些用户一般未受过软件开发方面的训练。所以,他们编写的代码质量每每比较低(从软件的角度看)。专业的软件工程师广泛采用的最佳实践和习惯在这种未经培训的社区中并不出众。例如,CRAN 提供的许多 R 包缺乏相似自动化测试的东西——除了最小的 Python 软件包以外,这几乎是闻所未闻的。另外在风格上,R 和 Matlab 程序员编写的代码每每在人与人之间的一致性方面要低一些。结果是,在其余条件相同的状况下,用 Python 编写软件每每比用 R 编写的代码具有更高的稳健性。虽然 Python 的这种优点无疑与语言自己的内在特征无关(一我的可使用任何语言(包括 R、Matlab 等)编写出极高质量的代码),但仍然存在这样的状况,强调共同惯例和最佳实践规范的开发人员社区每每会使你们编写出更清晰、更规范、更高质量的代码。

结论

Python 太棒了。

原文发布时间为:2018-08-06
本文做者:GitHub
本文来自云栖社区合做伙伴“CDA数据分析师”,了解相关信息能够关注“CDA数据分析师