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

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

2018-08-05

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


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


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


概述


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


Python(相對來說)易于學(xué)習(xí)


編程很難,,因此從絕對意義上來說,除非你已經(jīng)擁有編程經(jīng)驗,,否則編程語言難以學(xué)習(xí),。但是,相對而言,,Python 的高級屬性(見下一節(jié)),、語法可讀性和語義直白性使得它比其他語言更容易學(xué)習(xí)。例如,,這是一個簡單 Python 函數(shù)的定義(故意未注釋),,它將一串英語單詞轉(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ù)事實上無法生成完全有效的 Pig Latin(假設(shè)存在「有效 Pig Latin」),但這沒有關(guān)系,。有些情況下它是可行的:


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

print(test1)


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


Python 是一種高級語言


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


相比之下,,在 Python 中,盡管在底層這些過程或多或少會發(fā)生(效率較低),,但我們在使用高級語言編寫時并不需要擔(dān)心這一部分,。從我們的角度來看,我們可以創(chuàng)建自己的盒子并根據(jù)喜好添加或刪除對象:


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


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


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)建一個緩存,,以塊的形式讀取文件塊并將它們分配到緩存中等等,。相比之下,在 Python 中,,讀取文件中的全部內(nèi)容只需要如下代碼:


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


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


Python 是一種通用語言


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


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


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


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


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

re:正則表達(dá)

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

multiprocessing:簡單的并行化工具

pickle:簡單的序列化

json:讀和寫 JSON

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

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

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

cProfile:分析代碼的基本工具


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


JSON,,簡單的方法


例如,假設(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"
  }
]
'''


我們可以花一些時間自己編寫 json 解析器,,或試著去找一個有效讀取 json 的第三方包。但我們很可能是在浪費(fèi)時間,,因為 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'}]


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


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


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


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


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

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

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

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

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

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

作圖:matplotlib,,seaborn,ggplot,,Bokeh

等等


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


Python 是一種(相對)快速的語言


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


不要重復(fù)做功


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


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

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


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


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)


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


在這種情況下,,新的解決方案是非常簡單的:如果我們將純 Python 列表轉(zhuǎn)化為 NumPy 數(shù)組,我們就可以立即調(diào)用 NumPy 的 sum 方法,,我們可能期望它應(yīng)該比核心的 Python 實現(xiàn)更快(技術(shù)上講,,我們可以傳入一個 Python 列表到 numpy.sum 中,它會隱式地將其轉(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)


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


需要更快的速度?


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


使用 Python 編寫 C 代碼


首先,,你可以嘗試編寫 Cython 代碼。Cython 是 Python 的一個超集(superset),,它允許你將(某些)C 代碼直接嵌入到 Python 代碼中,。Cython 不以編譯的方式運(yùn)行,相反你的 Python 文件(或其中特定的某部分)將在運(yùn)行前被編譯為 C 代碼,。實際的結(jié)果是你可以繼續(xù)編寫看起來幾乎完全和 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)


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


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


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


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


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


讓我們從定義一個簡單的實現(xiàn)開始,,我們甚至都不采用向量化來代替隨機(jī)相乘操作,。相反,我們簡單地遍歷數(shù)組中的每個元素,,從中隨機(jī)挑選一個其他元素,,將兩個元素相乘并將結(jié)果分配給一個特定的索引。如果我們用基準(zhǔn)問題測試這個函數(shù),,我們會發(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)


在我們即時編譯之前,我們應(yīng)該首先自問是否上述函數(shù)可以用更加符合 NumPy 形式的方法編寫,。NumPy 針對基于數(shù)組的操作進(jìn)行了優(yōu)化,,因此應(yīng)該不惜一切代價地避免使用循環(huán)操作,因為它們會非常慢,。幸運(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ù)組操作之間的這種性能差異對于 NumPy 來說是非常典型的,,因此我們要在算法上思考你所做的事的重要性,。


假設(shè)我們不是花時間重構(gòu)我們樸素的、緩慢的實現(xiàn),,而是簡單地在我們的函數(shù)上加一個修飾器去告訴 numba 庫我們要在第一次調(diào)用它時將函數(shù)編譯為 C,。字面上,下面的函數(shù) multiply_randomly_naive_jit 與上面定義的函數(shù) multiply_randomly_naive 之間的唯一區(qū)別是 @jit 修飾器,。當(dāng)然,,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 編譯版本的樸素函數(shù)事實上比向量化的版本跑得更快。


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


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


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


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


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


魔術(shù)方法


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


舉個簡單的例子,,我們來定義一個新的 Brain 對象。首先,,Barin 不會進(jìn)行任何操作,,它只會待在那兒禮貌地發(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__ 方法是對象的初始化方法——當(dāng)我們嘗試創(chuàng)建一個新的 Brain 實例時,,它會被調(diào)用。通常你需要在編寫新類時自己實現(xiàn)__init__,,所以如果你之前看過 Python 代碼,,那__init__ 可能看起來就比較熟悉了,本文就不再贅述,。


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


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


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


print(brain.get_owner())


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


舉個簡單的例子,我們來實現(xiàn)一個表示單一 Nifti 容積的新類,。我們將依靠繼承來實現(xiàn)大部分工作,;只需從 nibabel 包中繼承 NiftierImage 類。我們要做的就是定義 __and__ 和 __or__ 方法,,它們分別映射到 & 和 | 運(yùn)算符,。看看在執(zhí)行以下幾個單元前你是否搞懂了這段代碼的作用(可能你需要安裝一些包,,如 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 的最后一個特征就是它優(yōu)秀的社區(qū)。當(dāng)然,,每種主要的編程語言都有一個大型的社區(qū)致力于該語言的開發(fā),、應(yīng)用和推廣;關(guān)鍵是社區(qū)內(nèi)的人是誰,。一般來說,,圍繞編程語言的社區(qū)更能反映用戶的興趣和專業(yè)基礎(chǔ)。對于像 R 和 Matlab 這樣相對特定領(lǐng)域的語言來說,,這意味著為語言貢獻(xiàn)新工具的人中很大一部分不是軟件開發(fā)人員,,更可能是統(tǒng)計學(xué)家、工程師和科學(xué)家等等,。當(dāng)然,,統(tǒng)計學(xué)家和工程師沒什么不好。例如,,與其他語言相比,,統(tǒng)計學(xué)家較多的 R 生態(tài)系統(tǒng)的優(yōu)勢之一就是 R 具有一系列統(tǒng)計軟件包。


然而,,由統(tǒng)計或科學(xué)背景用戶所主導(dǎo)的社區(qū)存在缺點(diǎn),,即這些用戶通常未受過軟件開發(fā)方面的訓(xùn)練。因此,,他們編寫的代碼質(zhì)量往往比較低(從軟件的角度看),。專業(yè)的軟件工程師普遍采用的最佳實踐和習(xí)慣在這種未經(jīng)培訓(xùn)的社區(qū)中并不出眾。例如,,CRAN 提供的許多 R 包缺少類似自動化測試的東西——除了最小的 Python 軟件包之外,,這幾乎是聞所未聞的。另外在風(fēng)格上,,R 和 Matlab 程序員編寫的代碼往往在人與人之間的一致性方面要低一些,。結(jié)果是,在其他條件相同的情況下,,用 Python 編寫軟件往往比用 R 編寫的代碼具備更高的穩(wěn)健性,。雖然 Python 的這種優(yōu)勢無疑與語言本身的內(nèi)在特征無關(guān)(一個人可以使用任何語言(包括 R,、Matlab 等)編寫出極高質(zhì)量的代碼),但仍然存在這樣的情況,,強(qiáng)調(diào)共同慣例和最佳實踐規(guī)范的開發(fā)人員社區(qū)往往會使大家編寫出更清晰,、更規(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)容無法一一聯(lián)系確認(rèn)版權(quán)者,。如涉及作品內(nèi)容、版權(quán)和其它問題,,請及時通過電子郵件或電話通知我們,,以便迅速采取適當(dāng)措施,避免給雙方造成不必要的經(jīng)濟(jì)損失,。聯(lián)系電話:010-82306118,;郵箱:[email protected]