《電子技術(shù)應(yīng)用》
您所在的位置:首頁(yè) > 可編程邏輯 > 業(yè)界動(dòng)態(tài) > 人生苦短,為什么我要用Python?

人生苦短,為什么我要用Python?

2018-08-05

隨著機(jī)器學(xué)習(xí)的興起,Python 逐步成為了「最受歡迎」的語(yǔ)言。它簡(jiǎn)單易用、邏輯明確并擁有海量的擴(kuò)展包,因此其不僅成為機(jī)器學(xué)習(xí)與數(shù)據(jù)科學(xué)的首選語(yǔ)言,同時(shí)在網(wǎng)頁(yè)、數(shù)據(jù)爬取可科學(xué)研究等方面成為不二選擇。此外,很多入門級(jí)的機(jī)器學(xué)習(xí)開發(fā)者都是跟隨大流選擇 Python,但到底為什么要選擇 Python 就是本文的核心內(nèi)容。


本教程的目的是讓你相信兩件事:首先,Python 是一種非常棒的編程語(yǔ)言;其次,如果你是一名科學(xué)家,Python 很可能值得你去學(xué)習(xí)。本教程并非想要說明 Python 是一種萬(wàn)能的語(yǔ)言;相反,作者明確討論了在幾種情況下,Python 并不是一種明智的選擇。本教程的目的只是提供對(duì) Python 一些核心特征的評(píng)論,并闡述作為一種通用的科學(xué)計(jì)算語(yǔ)言,它比其他常用的替代方案(最著名的是 R 和 Matlab)更有優(yōu)勢(shì)。


本教程的其余部分假定你已經(jīng)有了一些編程經(jīng)驗(yàn),如果你非常精通其他以數(shù)據(jù)為中心的語(yǔ)言(如 R 或 Matlab),理解本教程就會(huì)非常容易。本教程不能算作一份關(guān)于 Python 的介紹,且文章重點(diǎn)在于為什么應(yīng)該學(xué)習(xí) Python 而不是怎樣寫 Python 代碼(盡管其他地方有大量的優(yōu)秀教程)。


概述


Python 是一種廣泛使用、易于學(xué)習(xí)、高級(jí)、通用的動(dòng)態(tài)編程語(yǔ)言。這很令人滿意,所以接下來(lái)分開討論一些特征。


Python(相對(duì)來(lái)說)易于學(xué)習(xí)


編程很難,因此從絕對(duì)意義上來(lái)說,除非你已經(jīng)擁有編程經(jīng)驗(yàn),否則編程語(yǔ)言難以學(xué)習(xí)。但是,相對(duì)而言,Python 的高級(jí)屬性(見下一節(jié))、語(yǔ)法可讀性和語(yǔ)義直白性使得它比其他語(yǔ)言更容易學(xué)習(xí)。例如,這是一個(gè)簡(jiǎn)單 Python 函數(shù)的定義(故意未注釋),它將一串英語(yǔ)單詞轉(zhuǎn)換為(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


以上函數(shù)事實(shí)上無(wú)法生成完全有效的 Pig Latin(假設(shè)存在「有效 Pig Latin」),但這沒有關(guān)系。有些情況下它是可行的:


test1 = pig_latin("let us see if this works")

print(test1)


拋開 Pig Latin 不說,這里的重點(diǎn)只是,出于幾個(gè)原因,代碼是很容易閱讀的。首先,代碼是在高級(jí)抽象中編寫的(下面將詳細(xì)介紹),因此每行代碼都會(huì)映射到一個(gè)相當(dāng)直觀的操作。這些操作可以是「取這個(gè)單詞的第一個(gè)字符」,而不是映射到一個(gè)沒那么直觀的低級(jí)操作,例如「為一個(gè)字符預(yù)留一個(gè)字節(jié)的內(nèi)存,稍后我會(huì)傳入一個(gè)字符」。其次,控制結(jié)構(gòu)(如,for—loops,if—then 條件等)使用諸如「in」,「and」和「not」的簡(jiǎn)單單詞,其語(yǔ)義相對(duì)接近其自然英語(yǔ)含義。第三,Python 對(duì)縮進(jìn)的嚴(yán)格控制強(qiáng)加了一種使代碼可讀的規(guī)范,同時(shí)防止了某些常見的錯(cuò)誤。第四,Python 社區(qū)非常強(qiáng)調(diào)遵循樣式規(guī)定和編寫「Python 式的」代碼,這意味著相比使用其他語(yǔ)言的程序員而言,Python 程序員更傾向于使用一致的命名規(guī)定、行的長(zhǎng)度、編程習(xí)慣和其他許多類似特征,它們共同使別人的代碼更易閱讀(盡管這可以說是社區(qū)的一個(gè)特征而不是語(yǔ)言本身)。


Python 是一種高級(jí)語(yǔ)言


與其他許多語(yǔ)言相比,Python 是一種相對(duì)「高級(jí)」的語(yǔ)言:它不需要(并且在許多情況下,不允許)用戶擔(dān)心太多底層細(xì)節(jié),而這是其他許多語(yǔ)言需要去處理的。例如,假設(shè)我們想創(chuàng)建一個(gè)名為「my_box_of_things」的變量當(dāng)作我們所用東西的容器。我們事先不知道我們想在盒子中保留多少對(duì)象,同時(shí)我們希望在添加或刪除對(duì)象時(shí),對(duì)象數(shù)量可以自動(dòng)增減。所以這個(gè)盒子需要占據(jù)一個(gè)可變的空間:在某個(gè)時(shí)間點(diǎn),它可能包含 8 個(gè)對(duì)象(或「元素」),而在另一個(gè)時(shí)間點(diǎn),它可能包含 257 個(gè)對(duì)象。在像 C 這樣的底層語(yǔ)言中,這個(gè)簡(jiǎn)單的要求就已經(jīng)給我們的程序帶來(lái)了一些復(fù)雜性,因?yàn)槲覀冃枰崆奥暶骱凶有枰紦?jù)多少空間,然后每次我們想要增加盒子需要的空間時(shí),我么需要明確創(chuàng)建一個(gè)占據(jù)更多空間的全新的盒子,然后將所有東西拷貝到其中。


相比之下,在 Python 中,盡管在底層這些過程或多或少會(huì)發(fā)生(效率較低),但我們?cè)谑褂酶呒?jí)語(yǔ)言編寫時(shí)并不需要擔(dān)心這一部分。從我們的角度來(lái)看,我們可以創(chuàng)建自己的盒子并根據(jù)喜好添加或刪除對(duì)象:


# 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)


更一般來(lái)說,Python(以及根據(jù)定義的其他所有高級(jí)語(yǔ)言)傾向于隱藏需要在底層語(yǔ)言中明確表達(dá)的各種死記硬背的聲明。這使得我們可以編寫非常緊湊、清晰的代碼(盡管它通常以降低性能為代價(jià),因?yàn)閮?nèi)部不再可訪問,因此優(yōu)化變得更加困難)。


例如,考慮從文件中讀取純文本這樣看似簡(jiǎn)單的行為。對(duì)于與文件系統(tǒng)直接接觸而傷痕累累的開發(fā)者來(lái)說,從概念上看似乎只需要兩個(gè)簡(jiǎn)單的操作就可以完成:首先打開一個(gè)文件,然后從其中讀取。實(shí)際過程遠(yuǎn)不止這些,并且比 Python 更底層的語(yǔ)言通常強(qiáng)制(或至少是鼓勵(lì))我們?nèi)コ姓J(rèn)這一點(diǎn)。例如,這是在 Java 中從文件中讀取內(nèi)容的規(guī)范(盡管肯定不是最簡(jiǎn)潔的)方法:


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();
    }
}


你可以看到我們不得不做一些令人苦惱的事,例如導(dǎo)入文件讀取器、為文件中的內(nèi)容創(chuàng)建一個(gè)緩存,以塊的形式讀取文件塊并將它們分配到緩存中等等。相比之下,在 Python 中,讀取文件中的全部?jī)?nèi)容只需要如下代碼:


# Read the contents of "hello_world.txt"
text = open("hello_world.txt").read()


當(dāng)然,這種簡(jiǎn)潔性并不是 Python 獨(dú)有的;還有其他許多高級(jí)語(yǔ)言同樣隱藏了簡(jiǎn)單請(qǐng)求所暗含的大部分令人討厭的內(nèi)部過程(如,Ruby,R,Haskell 等)。但是,相對(duì)來(lái)說比較少有其他語(yǔ)言能與接下來(lái)探討的 Python 特征相媲美。


Python 是一種通用語(yǔ)言


根據(jù)設(shè)計(jì),Python 是一種通用的語(yǔ)言。也就是說,它旨在允許程序員在任何領(lǐng)域編寫幾乎所有類型的應(yīng)用,而不是專注于一類特定的問題。在這方面,Python 可以與(相對(duì))特定領(lǐng)域的語(yǔ)言進(jìn)行對(duì)比,如 R 或 PHP。這些語(yǔ)言原則上可用于很多情形,但仍針對(duì)特定用例進(jìn)行了明確優(yōu)化(在這兩個(gè)示例中,分別用于統(tǒng)計(jì)和網(wǎng)絡(luò)后端開發(fā))。


Python 通常被親切地成為「所有事物的第二個(gè)最好的語(yǔ)言」,它很好地捕捉到了這樣的情緒,盡管在很多情況下 Python 并不是用于特定問題的最佳語(yǔ)言,但它通常具有足夠的靈活性和良好的支持性,使得人們?nèi)匀豢梢韵鄬?duì)有效地解決問題。事實(shí)上,Python 可以有效地應(yīng)用于許多不同的應(yīng)用中,這使得學(xué)習(xí) Python 成為一件相當(dāng)有價(jià)值的事。因?yàn)樽鳛橐粋€(gè)軟件開發(fā)人員,能夠使用單一語(yǔ)言實(shí)現(xiàn)所有事情,而不是必須根據(jù)所執(zhí)行的項(xiàng)目在不同語(yǔ)言和環(huán)境間進(jìn)行切換,是一件非常棒的事。


標(biāo)準(zhǔn)庫(kù)


通過瀏覽標(biāo)準(zhǔn)庫(kù)中可用的眾多模塊列表,即 Python 解釋器自帶的工具集(沒有安裝第三方軟件包),這可能是最容易理解 Python 通用性的方式。若考慮以下幾個(gè)示例:


os: 系統(tǒng)操作工具

re:正則表達(dá)

collections:有用的數(shù)據(jù)結(jié)構(gòu)

multiprocessing:簡(jiǎn)單的并行化工具

pickle:簡(jiǎn)單的序列化

json:讀和寫 JSON

argparse:命令行參數(shù)解析

functools:函數(shù)化編程工具

datetime:日期和時(shí)間函數(shù)

cProfile:分析代碼的基本工具


這張列表乍一看并不令人印象深刻,但對(duì)于 Python 開發(fā)者來(lái)說,使用它們是一個(gè)相對(duì)常見的經(jīng)歷。很多時(shí)候用谷歌搜索一個(gè)看似重要甚至有點(diǎn)深?yuàn)W的問題,我們很可能找到隱藏在標(biāo)準(zhǔn)庫(kù)模塊內(nèi)的內(nèi)置解決方案。


JSON,簡(jiǎn)單的方法


例如,假設(shè)你想從 web.JSON 中讀取一些 JSON 數(shù)據(jù),如下所示:


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"
  }
]
'''


我們可以花一些時(shí)間自己編寫 json 解析器,或試著去找一個(gè)有效讀取 json 的第三方包。但我們很可能是在浪費(fèi)時(shí)間,因?yàn)?Python 內(nèi)置的 json 模塊已經(jīng)能完全滿足我們的需要:


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'}]


請(qǐng)注意,在我們能于 json 模塊內(nèi)使用 loads 函數(shù)前,我們必須導(dǎo)入 json 模塊。這種必須將幾乎所有功能模塊明確地導(dǎo)入命名空間的模式在 Python 中相當(dāng)重要,且基本命名空間中可用的內(nèi)置函數(shù)列表非常有限。許多用過 R 或 Matlab 的開發(fā)者會(huì)在剛接觸時(shí)感到惱火,因?yàn)檫@兩個(gè)包的全局命名空間包含數(shù)百甚至上千的內(nèi)置函數(shù)。但是,一旦你習(xí)慣于輸入一些額外字符,它就會(huì)使代碼更易于讀取和管理,同時(shí)命名沖突的風(fēng)險(xiǎn)(R 語(yǔ)言中經(jīng)常出現(xiàn))被大大降低。


優(yōu)異的外部支持


當(dāng)然,Python 提供大量?jī)?nèi)置工具來(lái)執(zhí)行大量操作并不意味著總需要去使用這些工具。可以說比 Python 豐富的標(biāo)準(zhǔn)庫(kù)更大的賣點(diǎn)是龐大的 Python 開發(fā)者社區(qū)。多年來(lái),Python 一直是世界上最流行的動(dòng)態(tài)編程語(yǔ)言,開發(fā)者社區(qū)也貢獻(xiàn)了眾多高質(zhì)量的安裝包。


如下 Python 軟件包在不同領(lǐng)域內(nèi)提供了被廣泛使用的解決方案(這個(gè)列表在你閱讀本文的時(shí)候可能已經(jīng)過時(shí)了?。?/p>


Web 和 API 開發(fā):flask,Django,F(xiàn)alcon,hug

爬取數(shù)據(jù)和解析文本/標(biāo)記: requests,beautifulsoup,scrapy

自然語(yǔ)言處理(NLP):nltk,gensim,textblob

數(shù)值計(jì)算和數(shù)據(jù)分析:numpy,scipy,pandas,xarray

機(jī)器學(xué)習(xí):scikit-learn,Theano,Tensorflow,keras

圖像處理:pillow,scikit-image,OpenCV

作圖:matplotlib,seaborn,ggplot,Bokeh

等等


 Python 的一個(gè)優(yōu)點(diǎn)是有出色的軟件包管理生態(tài)系統(tǒng)。雖然在 Python 中安裝包通常比在 R 或 Matlab 中更難,這主要是因?yàn)?Python 包往往具有高度的模塊化和/或更多依賴于系統(tǒng)庫(kù)。但原則上至少大多數(shù) Python 的包可以使用 pip 包管理器通過命令提示符安裝。更復(fù)雜的安裝程序和包管理器,如 Anaconda 也大大減少了配置新 Python 環(huán)境時(shí)產(chǎn)生的痛苦。


Python 是一種(相對(duì))快速的語(yǔ)言


這可能令人有點(diǎn)驚訝:從表面上看,Python 是一種快速語(yǔ)言的說法看起來(lái)很愚蠢。因?yàn)樵跇?biāo)準(zhǔn)測(cè)試時(shí),和 C 或 Java 這樣的編譯語(yǔ)言相比,Python 通常會(huì)卡頓。毫無(wú)疑問,如果速度至關(guān)重要(例如,你正在編寫 3D 圖形引擎或運(yùn)行大規(guī)模的流體動(dòng)力學(xué)模擬實(shí)驗(yàn)),Python 可能不會(huì)成為你最優(yōu)選擇的語(yǔ)言,甚至不會(huì)是第二好的語(yǔ)言。但在實(shí)際中,許多科學(xué)家工作流程中的限制因素不是運(yùn)行時(shí)間而是開發(fā)時(shí)間。一個(gè)花費(fèi)一個(gè)小時(shí)運(yùn)行但只需要 5 分鐘編寫的腳本通常比一個(gè)花費(fèi) 5 秒鐘運(yùn)行但是需要一個(gè)禮拜編寫和調(diào)試的腳本更合意。此外,正如我們將在下面看到的,即使我們所用的代碼都用 Python 編寫,一些優(yōu)化操作通??梢允蛊溥\(yùn)行速度幾乎與基于 C 的解決方案一樣快。實(shí)際上,對(duì)大多數(shù)科學(xué)家家來(lái)說,基于 Python 的解決方案不夠快的情況并不是很多,而且隨著工具的改進(jìn),這種情況的數(shù)量正在急劇減少。


不要重復(fù)做功


軟件開發(fā)的一般原則是應(yīng)該盡可能避免做重復(fù)工作。當(dāng)然,有時(shí)候是沒法避免的,并且在很多情況下,為問題編寫自己的解決方案或創(chuàng)建一個(gè)全新的工具是有意義的。但一般來(lái)說,你自己編寫的 Python 代碼越少,性能就越好。有以下幾個(gè)原因:


Python 是一種成熟的語(yǔ)言,所以許多現(xiàn)有的包有大量的用戶基礎(chǔ)并且經(jīng)過大量?jī)?yōu)化。例如,對(duì) Python 中大多數(shù)核心科學(xué)庫(kù)(numpy,scipy,pandas 等)來(lái)說都是如此。

大多數(shù) Python 包實(shí)際上是用 C 語(yǔ)言編寫的,而不是用 Python 編寫的。對(duì)于大多數(shù)標(biāo)準(zhǔn)庫(kù),當(dāng)你調(diào)用一個(gè) Python 函數(shù)時(shí),實(shí)際上很大可能你是在運(yùn)行具有 Python 接口的 C 代碼。這意味著無(wú)論你解決問題的算法有多精妙,如果你完全用 Python 編寫,而內(nèi)置的解決方案是用 C 語(yǔ)言編寫的,那你的性能可能不如內(nèi)置的方案。例如,以下是運(yùn)行內(nèi)置的 sum 函數(shù)(用 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)


從算法上來(lái)說,你沒有太多辦法來(lái)加速任意數(shù)值列表的加和計(jì)算。所以你可能會(huì)想這是什么鬼,你也許可以用 Python 自己寫加和函數(shù),也許這樣可以封裝內(nèi)置 sum 函數(shù)的開銷,以防它進(jìn)行任何內(nèi)部驗(yàn)證。嗯……并非如此。


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)


至少在這個(gè)例子中,運(yùn)行你自己簡(jiǎn)單的代碼很可能不是一個(gè)好的解決方案。但這不意味著你必須使用內(nèi)置 sum 函數(shù)作為 Python 中的性能上限!由于 Python 沒有針對(duì)涉及大型輸入的數(shù)值運(yùn)算進(jìn)行優(yōu)化,因此內(nèi)置方法在加和大型列表時(shí)是表現(xiàn)次優(yōu)。在這種情況下我們應(yīng)該做的是提問:「是否有其他一些 Python 庫(kù)可用于對(duì)潛在的大型輸入進(jìn)行數(shù)值分析?」正如你可能想的那樣,答案是肯定的:NumPy 包是 Python 的科學(xué)生態(tài)系統(tǒng)中的主要成分,Python 中的絕大多數(shù)科學(xué)計(jì)算包都以某種方式構(gòu)建在 NumPy 上,它包含各種能幫助我們的計(jì)算函數(shù)。


在這種情況下,新的解決方案是非常簡(jiǎn)單的:如果我們將純 Python 列表轉(zhuǎn)化為 NumPy 數(shù)組,我們就可以立即調(diào)用 NumPy 的 sum 方法,我們可能期望它應(yīng)該比核心的 Python 實(shí)現(xiàn)更快(技術(shù)上講,我們可以傳入一個(gè) Python 列表到 numpy.sum 中,它會(huì)隱式地將其轉(zhuǎn)換為數(shù)組,但如果我們打算復(fù)用該 NumPy 數(shù)組,最好明確地轉(zhuǎn)化它)。


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)


因此簡(jiǎn)單地切換到 NumPy 可加快一個(gè)數(shù)量級(jí)的列表加和速度,而不需要自己去實(shí)現(xiàn)任何東西。


需要更快的速度?


當(dāng)然,有時(shí)候即使使用所有基于 C 的擴(kuò)展包和高度優(yōu)化的實(shí)現(xiàn),你現(xiàn)有的 Python 代碼也無(wú)法快速削減時(shí)間。在這種情況下,你的下意識(shí)反應(yīng)可能是放棄并轉(zhuǎn)化到一個(gè)「真正」的語(yǔ)言。并且通常,這是一種完全合理的本能。但是在你開始使用 C 或 Java 移植代碼前,你需要考慮一些不那么費(fèi)力的方法。


使用 Python 編寫 C 代碼


首先,你可以嘗試編寫 Cython 代碼。Cython 是 Python 的一個(gè)超集(superset),它允許你將(某些)C 代碼直接嵌入到 Python 代碼中。Cython 不以編譯的方式運(yùn)行,相反你的 Python 文件(或其中特定的某部分)將在運(yùn)行前被編譯為 C 代碼。實(shí)際的結(jié)果是你可以繼續(xù)編寫看起來(lái)幾乎完全和 Python 一樣的代碼,但仍然可以從 C 代碼的合理引入中獲得性能提升。特別是簡(jiǎn)單地提供 C 類型的聲明通??梢燥@著提高性能。


以下是我們簡(jiǎn)單加和代碼的 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)


關(guān)于 Cython 版本有幾點(diǎn)需要注意一下。首先,在你第一次執(zhí)行定義該方法的單元時(shí),需要很少的(但值得注意的)時(shí)間來(lái)編譯。那是因?yàn)?,與純粹的 Python 不同,代碼在執(zhí)行時(shí)不是逐行解譯的;相反,Cython 式的函數(shù)必須先編譯成 C 代碼才能調(diào)用。


其次,雖然 Cython 式的加和函數(shù)比我們上面寫的簡(jiǎn)單的 Python 加和函數(shù)要快,但仍然比內(nèi)置求和方法和 NumPy 實(shí)現(xiàn)慢得多。然而,這個(gè)結(jié)果更有力地說明了我們特定的實(shí)現(xiàn)過程和問題的本質(zhì),而不是 Cython 的一般好處;在許多情況下,一個(gè)有效的 Cython 實(shí)現(xiàn)可以輕易地將運(yùn)行時(shí)間提升一到兩個(gè)數(shù)量級(jí)。


使用 NUMBA 進(jìn)行清理


Cython 并不是提升 Python 內(nèi)部性能的唯一方法。從開發(fā)的角度來(lái)看,另一種更簡(jiǎn)單的方法是依賴于即時(shí)編譯,其中一段 Python 代碼在第一次調(diào)用時(shí)被編譯成優(yōu)化的 C 代碼。近年來(lái),在 Python 即時(shí)編譯器上取得了很大進(jìn)展。也許最成熟的實(shí)現(xiàn)可以在 numba 包中找到,它提供了一個(gè)簡(jiǎn)單的 jit 修飾器,可以輕易地結(jié)合其他任何方法。


我們之前的示例并沒有強(qiáng)調(diào) JITs 可以產(chǎn)生多大的影響,所以我們轉(zhuǎn)向一個(gè)稍微復(fù)雜點(diǎn)的問題。這里我們定義一個(gè)被稱為 multiply_randomly 的新函數(shù),它將一個(gè)一維浮點(diǎn)數(shù)數(shù)組作為輸入,并將數(shù)組中的每個(gè)元素與其他任意一個(gè)隨機(jī)選擇的元素相乘。然后它返回所有隨機(jī)相乘的元素和。


讓我們從定義一個(gè)簡(jiǎn)單的實(shí)現(xiàn)開始,我們甚至都不采用向量化來(lái)代替隨機(jī)相乘操作。相反,我們簡(jiǎn)單地遍歷數(shù)組中的每個(gè)元素,從中隨機(jī)挑選一個(gè)其他元素,將兩個(gè)元素相乘并將結(jié)果分配給一個(gè)特定的索引。如果我們用基準(zhǔn)問題測(cè)試這個(gè)函數(shù),我們會(huì)發(fā)現(xiàn)它運(yùn)行得相當(dāng)慢。


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)


在我們即時(shí)編譯之前,我們應(yīng)該首先自問是否上述函數(shù)可以用更加符合 NumPy 形式的方法編寫。NumPy 針對(duì)基于數(shù)組的操作進(jìn)行了優(yōu)化,因此應(yīng)該不惜一切代價(jià)地避免使用循環(huán)操作,因?yàn)樗鼈儠?huì)非常慢。幸運(yùn)的是,我們的代碼非常容易向量化(并且易于閱讀):


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)


在作者的機(jī)器上,向量化版本的運(yùn)行速度比循環(huán)版本的代碼快大約 100 倍。循環(huán)和數(shù)組操作之間的這種性能差異對(duì)于 NumPy 來(lái)說是非常典型的,因此我們要在算法上思考你所做的事的重要性。


假設(shè)我們不是花時(shí)間重構(gòu)我們樸素的、緩慢的實(shí)現(xiàn),而是簡(jiǎn)單地在我們的函數(shù)上加一個(gè)修飾器去告訴 numba 庫(kù)我們要在第一次調(diào)用它時(shí)將函數(shù)編譯為 C。字面上,下面的函數(shù) multiply_randomly_naive_jit 與上面定義的函數(shù) multiply_randomly_naive 之間的唯一區(qū)別是 @jit 修飾器。當(dāng)然,4 個(gè)小字符是沒法造成那么大的差異的。對(duì)吧?


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 編譯版本的樸素函數(shù)事實(shí)上比向量化的版本跑得更快。


有趣的是,將 @jit 修飾器應(yīng)用于函數(shù)的向量化版本(將其作為聯(lián)系留給讀者)并不能提供更多幫助。在 numba JIT 編譯器用于我們的代碼之后,Python 實(shí)現(xiàn)的兩個(gè)版本都以同樣的速度運(yùn)行。因此,至少在這個(gè)例子中,即時(shí)編譯不僅可以毫不費(fèi)力地為我們提供類似 C 的速度,而且可以避免以 Python 式地去優(yōu)化代碼。


這可能是一個(gè)相當(dāng)有力的結(jié)論,因?yàn)椋╝)現(xiàn)在 numba 的 JIT 編譯器只覆蓋了 NumPy 特征的一部分,(b)不能保證編譯的代碼一定比解譯的代碼運(yùn)行地更快(盡管這通常是一個(gè)有效的假設(shè))。這個(gè)例子真正的目的是提醒你,在你宣稱它慢到無(wú)法去實(shí)現(xiàn)你想要做的事之前,其實(shí)你在 Python 中有許多可用的選擇。值得注意的是,如 C 集成和即時(shí)編譯,這些性能特征都不是 Python 獨(dú)有的。Matlab 最近的版本自動(dòng)使用即時(shí)編譯,同時(shí) R 支持 JIT 編譯(通過外部庫(kù))和 C ++ 集成(Rcpp)。


Python 是天生面向?qū)ο蟮?/p>


即使你正在做的只是編寫一些簡(jiǎn)短的腳本去解析文本或挖掘一些數(shù)據(jù),Python 的許多好處也很容易領(lǐng)會(huì)到。在你開始編寫相對(duì)大型的代碼片段前,Python 的最佳功能之一可能并不明顯:Python 具有設(shè)計(jì)非常優(yōu)雅的基于對(duì)象的數(shù)據(jù)模型。事實(shí)上,如果你查看底層,你會(huì)發(fā)現(xiàn) Python 中的一切都是對(duì)象。甚至函數(shù)也是對(duì)象。當(dāng)你調(diào)用一個(gè)函數(shù)的時(shí)候,你事實(shí)上正在調(diào)用 Python 中每個(gè)對(duì)象都運(yùn)行的 __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__']


事實(shí)上,因?yàn)?Python 中的一切都是對(duì)象,Python 中的所有內(nèi)容遵循相同的核心邏輯,實(shí)現(xiàn)相同的基本 API,并以類似的方式進(jìn)行擴(kuò)展。對(duì)象模型也恰好非常靈活:可以很容易地定義新的對(duì)象去實(shí)現(xiàn)有意思的事,同時(shí)仍然表現(xiàn)得相對(duì)可預(yù)測(cè)。也許并不奇怪,Python 也是編寫特定領(lǐng)域語(yǔ)言(DSLs)的一個(gè)絕佳選擇,因?yàn)樗试S用戶在很大程度上重載和重新定義現(xiàn)有的功能。


魔術(shù)方法


Python 對(duì)象模型的核心部分是它使用「魔術(shù)」方法。這些在對(duì)象上實(shí)現(xiàn)的特殊方法可以更改 Python 對(duì)象的行為——通常以重要的方式。魔術(shù)方法(Magic methods)通常以雙下劃線開始和結(jié)束,一般來(lái)說,除非你知道自己在做什么,否則不要輕易篡改它們。但一旦你真的開始改了,你就可以做些相當(dāng)了不起的事。


舉個(gè)簡(jiǎn)單的例子,我們來(lái)定義一個(gè)新的 Brain 對(duì)象。首先,Barin 不會(huì)進(jìn)行任何操作,它只會(huì)待在那兒禮貌地發(fā)呆。


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__ 方法是對(duì)象的初始化方法——當(dāng)我們嘗試創(chuàng)建一個(gè)新的 Brain 實(shí)例時(shí),它會(huì)被調(diào)用。通常你需要在編寫新類時(shí)自己實(shí)現(xiàn)__init__,所以如果你之前看過 Python 代碼,那__init__ 可能看起來(lái)就比較熟悉了,本文就不再贅述。


相比之下,大多數(shù)用戶很少明確地實(shí)現(xiàn)__getattr__方法。但它控制著 Python 對(duì)象行為的一個(gè)非常重要的部分。具體來(lái)說,當(dāng)用戶試圖通過點(diǎn)語(yǔ)法(如 brain.owner)訪問類屬性,同時(shí)這個(gè)屬性實(shí)際上并不存在時(shí),__getattr__方法將會(huì)被調(diào)用。此方法的默認(rèn)操作僅是引發(fā)一個(gè)錯(cuò)誤:


# 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: 


重要的是,我們不用忍受這種行為。假設(shè)我們想創(chuàng)建一個(gè)替代接口用于通過以「get」開頭的 getter 方法從 Brain 類的內(nèi)部檢索數(shù)據(jù)(這是許多其他語(yǔ)言中的常見做法),我們當(dāng)然可以通過名字(如 get_owner、get_age 等)顯式地實(shí)現(xiàn) getter 方法。但假設(shè)我們很懶,并且不想為每個(gè)屬性編寫一個(gè)顯式的 getter。此外,我們可能想要為已經(jīng)創(chuàng)建的 Brains 類添加新的屬性(如,brain.foo = 4),在這種情況下,我們不需要提前為那些未知屬性創(chuàng)建 getter 方法(請(qǐng)注意,在現(xiàn)實(shí)世界中,這些是為什么我們接下來(lái)要這么做的可怕理由;當(dāng)然這里完全是為了舉例說明)。我們可以做的是,當(dāng)用戶請(qǐng)求任意屬性時(shí),通過指示 Brain 類的操作去改變它的行為。


在上面的代碼片段中,我們的 __getattr__ 實(shí)現(xiàn)首先檢查了傳入屬性的名稱。如果名稱以 get_ 開頭,我們將檢查對(duì)象內(nèi)是否存在期望屬性的名稱。如果確實(shí)存在,則返回該對(duì)象。否則,我們會(huì)引發(fā)錯(cuò)誤的默認(rèn)操作。這讓我們可以做一些看似瘋狂的事,比如:


print(brain.get_owner())


其他不可思議的方法允許你動(dòng)態(tài)地控制對(duì)象行為的其他各種方面,而這在其他許多語(yǔ)言中你沒法做到。事實(shí)上,因?yàn)?Python 中的一切都是對(duì)象,甚至數(shù)學(xué)運(yùn)算符實(shí)際上也是對(duì)對(duì)象的秘密方法調(diào)用。例如,當(dāng)你用 Python 編寫表達(dá)式 4 + 5 時(shí),你實(shí)際上是在整數(shù)對(duì)象 4 上調(diào)用 __add__,其參數(shù)為 5。如果我們?cè)敢猓ú⑶椅覀儜?yīng)該小心謹(jǐn)慎地行使這項(xiàng)權(quán)利!),我們能做的是創(chuàng)建新的特定領(lǐng)域的「迷你語(yǔ)言」,為通用運(yùn)算符注入全新的語(yǔ)義。


舉個(gè)簡(jiǎn)單的例子,我們來(lái)實(shí)現(xiàn)一個(gè)表示單一 Nifti 容積的新類。我們將依靠繼承來(lái)實(shí)現(xiàn)大部分工作;只需從 nibabel 包中繼承 NiftierImage 類。我們要做的就是定義 __and__ 和 __or__ 方法,它們分別映射到 & 和 | 運(yùn)算符。看看在執(zhí)行以下幾個(gè)單元前你是否搞懂了這段代碼的作用(可能你需要安裝一些包,如 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 社區(qū)


我在這里提到的 Python 的最后一個(gè)特征就是它優(yōu)秀的社區(qū)。當(dāng)然,每種主要的編程語(yǔ)言都有一個(gè)大型的社區(qū)致力于該語(yǔ)言的開發(fā)、應(yīng)用和推廣;關(guān)鍵是社區(qū)內(nèi)的人是誰(shuí)。一般來(lái)說,圍繞編程語(yǔ)言的社區(qū)更能反映用戶的興趣和專業(yè)基礎(chǔ)。對(duì)于像 R 和 Matlab 這樣相對(duì)特定領(lǐng)域的語(yǔ)言來(lái)說,這意味著為語(yǔ)言貢獻(xiàn)新工具的人中很大一部分不是軟件開發(fā)人員,更可能是統(tǒng)計(jì)學(xué)家、工程師和科學(xué)家等等。當(dāng)然,統(tǒng)計(jì)學(xué)家和工程師沒什么不好。例如,與其他語(yǔ)言相比,統(tǒng)計(jì)學(xué)家較多的 R 生態(tài)系統(tǒng)的優(yōu)勢(shì)之一就是 R 具有一系列統(tǒng)計(jì)軟件包。


然而,由統(tǒng)計(jì)或科學(xué)背景用戶所主導(dǎo)的社區(qū)存在缺點(diǎn),即這些用戶通常未受過軟件開發(fā)方面的訓(xùn)練。因此,他們編寫的代碼質(zhì)量往往比較低(從軟件的角度看)。專業(yè)的軟件工程師普遍采用的最佳實(shí)踐和習(xí)慣在這種未經(jīng)培訓(xùn)的社區(qū)中并不出眾。例如,CRAN 提供的許多 R 包缺少類似自動(dòng)化測(cè)試的東西——除了最小的 Python 軟件包之外,這幾乎是聞所未聞的。另外在風(fēng)格上,R 和 Matlab 程序員編寫的代碼往往在人與人之間的一致性方面要低一些。結(jié)果是,在其他條件相同的情況下,用 Python 編寫軟件往往比用 R 編寫的代碼具備更高的穩(wěn)健性。雖然 Python 的這種優(yōu)勢(shì)無(wú)疑與語(yǔ)言本身的內(nèi)在特征無(wú)關(guān)(一個(gè)人可以使用任何語(yǔ)言(包括 R、Matlab 等)編寫出極高質(zhì)量的代碼),但仍然存在這樣的情況,強(qiáng)調(diào)共同慣例和最佳實(shí)踐規(guī)范的開發(fā)人員社區(qū)往往會(huì)使大家編寫出更清晰、更規(guī)范、更高質(zhì)量的代碼。


本站內(nèi)容除特別聲明的原創(chuàng)文章之外,轉(zhuǎn)載內(nèi)容只為傳遞更多信息,并不代表本網(wǎng)站贊同其觀點(diǎn)。轉(zhuǎn)載的所有的文章、圖片、音/視頻文件等資料的版權(quán)歸版權(quán)所有權(quán)人所有。本站采用的非本站原創(chuàng)文章及圖片等內(nèi)容無(wú)法一一聯(lián)系確認(rèn)版權(quán)者。如涉及作品內(nèi)容、版權(quán)和其它問題,請(qǐng)及時(shí)通過電子郵件或電話通知我們,以便迅速采取適當(dāng)措施,避免給雙方造成不必要的經(jīng)濟(jì)損失。聯(lián)系電話:010-82306118;郵箱:aet@chinaaet.com。