色婷婷狠狠18禁久久YY,CHINESE性内射高清国产,国产女人18毛片水真多1,国产AV在线观看

為什么Python是入行人工智能的首選語言

錢諍諍2年前20瀏覽0評論

為什么Python是入行人工智能的首選語言?

隨著機器學習的興起,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 thingsmy_box_of_things += ['bathing suit', 'bowling ball', 'clarinet', 'ring']# Maybe add one last thingmy_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 jsondata = 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 floatsimport randommy_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 npmy_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 CythonThe 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 npdef 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 npfrom numba import jit@jitdef 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 attributesdir(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 instancebrain = Brain(owner="Sue", age="62", status="hanging out in a jar")print(brain.owner)---------------------------------------------------------------------------sueprint(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 AttributeErrorAttributeError:

重要的是,我們不用忍受這種行為。假設我們想創建一個替代接口用于通過以「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 Nifti1Imagefrom nilearn.image import new_img_likefrom nilearn.plotting import plot_stat_mapimport numpy as npimport matplotlib.pyplot as plt%matplotlib inlineclass 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 & img2fig, 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 太棒了。