
1. 為什么說NumPy是Python科學計算的基石如果你剛開始接觸Python數據分析、機器學習或者科學計算那么“NumPy”這個名字一定會高頻出現。很多教程會告訴你“先裝NumPy”但很少會深入解釋為什么偏偏是它而不是別的庫成為了這個領域事實上的標準。今天我們不談空洞的概念就從最實際的“手感”和“性能”出發聊聊NumPy到底解決了什么痛點以及它的核心——ndarray對象是如何從根本上改變我們處理數據的方式的。想象一下你用Python原生的列表list存儲100萬個浮點數然后想給每個數都加上1。你會怎么寫一個for循環列表推導式[x1 for x in my_list]代碼寫起來簡單但當你真正跑起來尤其是數據量再大幾個數量級時你會明顯感覺到“慢”。這種慢根源在于Python列表的靈活性列表里的每個元素都是一個完整的Python對象比如一個float對象它們可以類型不同可以嵌套但也因此每個元素都攜帶了類型信息、引用計數等額外開銷。進行運算時Python解釋器需要逐個檢查、解包、計算、再打包這個動態過程消耗了大量時間。NumPy的ndarrayN-dimensional arrayN維數組就是為了終結這種低效而生的。它要求數組內的所有元素必須是同質的通常是數字類型并且在內存中連續存儲。這就好比把一隊散兵游勇Python列表整編成了一支紀律嚴明、裝備統一的方陣NumPy數組。計算機的CPU和內存對于處理這種整齊劃一的數據塊極其高效因為可以利用向量化指令和緩存預取等底層優化。同樣給100萬個數加1NumPy只需要一條語句arr 1這個操作不是在Python層面用循環實現的而是被編譯成底層的C代碼在連續的二進制數據塊上一次性完成速度可能提升幾十甚至上百倍。所以學習NumPy首要任務就是忘掉列表的思維定勢建立起“數組計算”的思維模型。它不是Python列表的替代品而是一個專為高性能數值計算設計的、全新的數據結構。接下來我們就從最基礎的數組創建開始一步步拆解NumPy的核心函數。2. 數組的誕生從無到有構建你的數據容器創建數組是使用NumPy的第一步。NumPy提供了十幾種創建數組的函數看似繁雜但核心邏輯就幾條從已有數據轉換、按照規則生成序列、或者用特定值填充。理解這些函數的區別和適用場景能讓你在寫代碼時信手拈來。2.1 從“原材料”直接鑄造np.array()與np.asarray()這是最直接、最常用的方法將Python的序列型數據列表、元組等轉化為NumPy數組。import numpy as np # 從列表創建一維數組 list_data [1, 2, 3, 4, 5] arr_from_list np.array(list_data) print(arr_from_list) # 輸出: [1 2 3 4 5] print(type(arr_from_list)) # 輸出: class numpy.ndarray # 從嵌套列表創建二維數組矩陣 list_2d [[1, 2, 3], [4, 5, 6]] arr_2d np.array(list_2d) print(arr_2d) # 輸出: # [[1 2 3] # [4 5 6]] print(arr_2d.shape) # 輸出: (2, 3) 表示2行3列這里有個關鍵細節np.array()會默認復制輸入數據生成一個新的數組對象。如果你有一個巨大的列表并且確定后續不會修改原始列表這個復制操作可能會成為內存和性能的瓶頸。此時np.asarray()就派上用場了。original_list [10, 20, 30] arr_a np.array(original_list) arr_b np.asarray(original_list) # 修改原始列表 original_list[0] 999 print(arr_a) # 輸出: [10 20 30] 未受影響因為它是副本 print(arr_b) # 輸出: [10 20 30] 同樣未受影響等等這里有個常見的誤解等等arr_b怎么也沒變不是說asarray不復制嗎這里是一個重要的知識點np.asarray()只在輸入數據本身還不是NumPy數組時且無法直接轉換為目標數據類型時才會嘗試避免復制。如果輸入是一個Python列表asarray的行為和array幾乎一樣都會創建新的數組。它的主要優勢體現在輸入已經是ndarray時。# 更典型的 asarray 使用場景 existing_arr np.array([1.0, 2.0, 3.0]) # 假設我們有一個函數它要求輸入是數組但我們不確定調用者給的是數組還是列表 def some_function(input_data): arr np.asarray(input_data) # 如果input_data已經是數組這里不會發生復制 # ... 對arr進行操作 return arr result some_function(existing_arr) # 高效無復制注意np.array()有一個關鍵參數dtype用于指定數組的數據類型。如果你不指定NumPy會自行推斷有時推斷結果可能不是你想要的比如從整數列表推斷出int32但你需要int64。明確指定dtype是好習慣尤其是在涉及跨平臺或精度要求高的計算時。例如np.array([1, 2, 3], dtypenp.float64)。2.2 生成規則序列np.arange()、np.linspace()與np.logspace()當我們需要一個等間隔的數字序列時np.arange()是range()函數的強化版。# 類似 range(start, stop, step) seq1 np.arange(5) # 默認從0開始步長為1不包括5 print(seq1) # [0 1 2 3 4] seq2 np.arange(2, 10, 2) # 從2開始到10不含步長為2 print(seq2) # [2 4 6 8] seq3 np.arange(0, 1, 0.2) # 支持浮點數步長 print(seq3) # [0. 0.2 0.4 0.6 0.8]np.arange的一個經典“坑”在于處理浮點數步長時的精度問題。由于浮點數的二進制表示并不精確np.arange(0, 1, 0.1)生成的最后一個值可能是0.9999999999999999而不是1.0這可能導致后續判斷出錯。因此當需要固定數量的等分點時更推薦使用np.linspace。np.linspace(start, stop, num)用于在指定的區間內生成等間隔的num個點。它關注的是點的數量而不是步長。# 在0到1之間包括0和1生成5個等間隔的點 points np.linspace(0, 1, 5) print(points) # [0. 0.25 0.5 0.75 1. ] # 在0到2π之間生成100個點常用于生成正弦函數采樣點 x np.linspace(0, 2*np.pi, 100) y np.sin(x)np.logspace(start, stop, num)則是生成等比數列在[10^start, 10^stop]區間內生成num個對數等間隔的點。這在需要測試跨越多個數量級的參數時非常有用比如學習率搜索。# 生成從10^0到10^2之間的5個點 log_points np.logspace(0, 2, 5) print(log_points) # [ 1. 3.16227766 10. 31.6227766 100. ]2.3 創建特殊值數組np.zeros()、np.ones()、np.full()與np.empty()在初始化數組尤其是為后續計算準備“容器”時這些函數極其高效。np.zeros(shape)創建指定形狀的全0數組。np.ones(shape)創建指定形狀的全1數組。np.full(shape, fill_value)創建指定形狀并用fill_value填充的數組。np.empty(shape)創建指定形狀的數組但不初始化其值其內容是內存中的隨機值取決于當時內存的狀態。速度最快但必須立即用有效數據覆蓋。# 創建一個3行4列的全零矩陣 zero_matrix np.zeros((3, 4)) print(zero_matrix) # 創建一個長度為5元素全為3.14的數組 pi_array np.full(5, 3.14) print(pi_array) # [3.14 3.14 3.14 3.14 3.14] # 創建一個2x2的未初始化數組危險僅用于性能關鍵且立即賦值的場景 uninitialized np.empty((2, 2)) print(uninitialized) # 輸出內容不確定可能是任意值實操心得np.empty的速度優勢在需要創建超大臨時數組并立刻進行填充計算的循環中才能體現。對于日常使用np.zeros或np.full是更安全、更可讀的選擇。永遠不要直接使用np.empty創建的數組進行計算除非你百分之百確定你在下一步就會覆蓋所有元素。2.4 創建單位矩陣與對角陣np.eye()與np.diag()在線性代數運算中單位矩陣和對角陣非常常見。np.eye(N, MNone, k0)用于創建單位矩陣對角線為1其余為0。N是行數M是列數默認等于N。k參數控制對角線的位置k0是主對角線k0在主對角線之上k0在主對角線之下。# 3x3單位矩陣 I np.eye(3) print(I) # [[1. 0. 0.] # [0. 1. 0.] # [0. 0. 1.]] # 創建3x4矩陣且第一超對角線k1為1 eye_k np.eye(3, 4, k1) print(eye_k) # [[0. 1. 0. 0.] # [0. 0. 1. 0.] # [0. 0. 0. 1.]]np.diag(v, k0)有兩種用法1如果v是1-D數組則將其作為對角線元素構造一個方陣2如果v是2-D數組則提取其第k條對角線。# 用法1用一維數組創建對角陣 diag_arr np.array([1, 2, 3]) D np.diag(diag_arr) print(D) # [[1 0 0] # [0 2 0] # [0 0 3]] # 用法2提取矩陣的對角線 matrix np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) main_diag np.diag(matrix) # k0 print(main_diag) # [1 5 9] super_diag np.diag(matrix, k1) print(super_diag) # [2 6]3. 數組的“形”與“魂”維度操作與索引切片創建了數組接下來就要學會如何操縱它。NumPy數組有兩個核心屬性形狀shape和數據類型dtype。形狀決定了數組的維度和每個維度的大小而數據類型決定了數組中每個元素在內存中如何解釋。3.1 重塑與展平reshape()、resize()與ravel()/flatten()reshape(newshape)是改變數組視圖形狀最常用的方法。它返回一個新視圖數據本身不變。前提是新形狀的元素總數必須與原數組一致。arr np.arange(12) # 一維數組12個元素 [0, 1, 2, ..., 11] print(arr.shape) # (12,) # 重塑為3行4列的二維數組 arr_2d arr.reshape(3, 4) print(arr_2d.shape) # (3, 4) print(arr_2d) # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]] # 也可以使用-1來自動推斷某一維的大小 # 將二維數組重塑為有6列行數自動計算 arr_reshaped arr_2d.reshape(-1, 6) print(arr_reshaped.shape) # (2, 6)resize(new_shape)與reshape功能類似但有一個關鍵區別如果新形狀的元素總數大于原數組resize會用0填充多余的部分如果小于則會截斷數據。更重要的是arr.resize()是原地操作會直接修改原數組且沒有返回值。而np.resize(arr, new_shape)是一個函數返回新數組原數組不變同樣遵循填充/截斷規則。arr np.array([1, 2, 3]) arr.resize(5) # 原地修改用0填充 print(arr) # [1 2 3 0 0] arr2 np.array([1, 2, 3, 4, 5]) new_arr np.resize(arr2, (2, 2)) # 函數返回新數組截斷數據 print(new_arr) # [[1 2] # [3 4]]將多維數組變成一維有兩種方法ravel()和flatten()。ravel()返回數組的展平視圖如果可能這意味著修改ravel()返回的數組可能會影響原數組。flatten()則總是返回原數組數據的一份拷貝修改它不影響原數組。arr_2d np.array([[1, 2], [3, 4]]) flat_view arr_2d.ravel() flat_copy arr_2d.flatten() flat_view[0] 99 print(arr_2d) # [[99 2] [ 3 4]] 原數組被修改了 flat_copy[1] 100 print(arr_2d) # [[99 2] [ 3 4]] 原數組不受影響3.2 索引與切片像操作列表一樣但更強大NumPy的索引切片語法和Python列表非常相似但在多維情況下更加靈活。基本索引對于一維數組arr[index]對于二維數組arr[row_index, col_index]。arr np.arange(10, 20) print(arr[3]) # 13 print(arr[-1]) # 19 負索引從末尾開始 arr_2d np.arange(12).reshape(3, 4) print(arr_2d[1, 2]) # 第1行第2列從0開始計數6切片Slicingstart:stop:step。與列表切片一樣結果是原數組的一個視圖。arr np.arange(10) print(arr[2:7]) # [2 3 4 5 6] print(arr[::2]) # [0 2 4 6 8] 步長為2 print(arr[::-1]) # [9 8 7 6 5 4 3 2 1 0] 反轉數組 arr_2d np.arange(12).reshape(3, 4) print(arr_2d[:2, 1:3]) # 前兩行第1到2列不含第3列 # [[1 2] # [5 6]]花式索引Fancy Indexing使用整數數組或布爾數組進行索引。這會產生數據的拷貝而不是視圖。arr np.array([10, 20, 30, 40, 50]) indices [0, 2, 4] print(arr[indices]) # [10 30 50] # 布爾索引非常強大的數據篩選工具 mask arr 25 print(mask) # [False False True True True] print(arr[mask]) # [30 40 50]對于多維數組花式索引可以組合使用實現更復雜的選取。arr_2d np.arange(12).reshape(3, 4) # 選取第0行和第2行的所有列 rows [0, 2] print(arr_2d[rows, :]) # 選取第0行和第2行的第1列和第3列 cols [1, 3] print(arr_2d[rows][:, cols]) # 注意這里用了兩次索引第一次是行第二次是列 # 更簡潔的寫法是arr_2d[np.ix_(rows, cols)] np.ix_函數用于構建開放網格 print(arr_2d[np.ix_(rows, cols)]) # [[ 1 3] # [ 9 11]]踩坑實錄切片產生視圖花式索引產生拷貝。這是一個極易出錯的地方。如果你通過切片修改了數組原數組也會變。如果你不希望這樣需要顯式調用.copy()方法。例如sub_arr arr[2:5].copy()。4. 數組的“骨架”廣播機制與通用函數NumPy最強大的特性之一就是允許形狀不同的數組進行數學運算。這背后的魔法叫做廣播Broadcasting。理解廣播規則是寫出高效、簡潔NumPy代碼的關鍵。4.1 廣播機制的核心規則廣播的核心思想是將較小的數組“廣播”到較大數組的形狀以便它們具有兼容的形狀進行逐元素運算。規則可以簡化為兩條維度對齊從最右邊的維度開始比較兩個數組的形狀。如果兩個維度相等或者其中一個為1或者其中一個數組在該維度上不存在即維度數為1則這兩個維度是兼容的。擴展在兼容的維度上大小為1的維度會被“拉伸”以匹配另一個數組對應維度的大小。如果某個數組在某個維度上缺失即維度數少則在該維度前補1然后進行拉伸。聽起來有點抽象看例子就明白了import numpy as np # 例子1標量與數組運算標量被視為0維數組 arr np.ones((3, 4)) result arr 5 # 標量5被廣播為形狀(3,4)的全5矩陣 print(result[0,0]) # 6.0 # 例子2向量與矩陣運算 row_vector np.array([1, 2, 3, 4]) # 形狀 (4,) matrix np.ones((3, 4)) # 形狀 (3, 4) # row_vector形狀(4,) - 補1 - (1, 4) - 拉伸 - (3, 4) result matrix row_vector print(result) # [[2. 3. 4. 5.] # [2. 3. 4. 5.] # [2. 3. 4. 5.]] # 例子3列向量與行向量運算 col_vector np.array([[1], [2], [3]]) # 形狀 (3, 1) row_vector np.array([10, 20, 30, 40]) # 形狀 (4,) # col_vector (3,1) 與 row_vector (4,) 比較 # 首先將row_vector補1 - (1, 4) # 然后對齊col_vector的第二個維度是1 row_vector的第一個維度是1都是1兼容。 # 廣播后col_vector拉伸為(3,4) row_vector拉伸為(3,4) result col_vector row_vector print(result) # [[11 21 31 41] # [12 22 32 42] # [13 23 33 43]]不兼容的例子A np.ones((3, 4, 5)) B np.ones((4, 5)) # 比較最右邊A的5 vs B的5 - 兼容 # 比較次右邊A的4 vs B的4 - 兼容 # 比較最左邊A的3 vs B的缺失- B在此維度補1 - 1 vs 3 - 兼容因為1可以拉伸到3 # 所以可以廣播B最終被廣播為(1,4,5) - (3,4,5) C np.ones((4, 3)) # 比較最右邊A的5 vs C的3 - 不相等且都不是1 - 不兼容會報錯。 # result A C # ValueError: operands could not be broadcast together...4.2 通用函數向量化計算的引擎通用函數ufunc是NumPy中另一種實現高效運算的機制。它們是能對數組進行逐元素操作的函數。NumPy內置了大量的ufunc涵蓋數學運算、三角函數、邏輯比較等。arr np.array([1.0, 4.0, 9.0, 16.0]) # 數學運算 print(np.sqrt(arr)) # 開方 [1. 2. 3. 4.] print(np.exp(arr)) # 指數 print(np.log(arr)) # 自然對數 print(np.sin(arr)) # 正弦 # 二元運算 a np.array([1, 2, 3]) b np.array([4, 5, 6]) print(np.add(a, b)) # 加法 [5 7 9] print(np.multiply(a, b)) # 乘法 [4 10 18] print(np.maximum(a, b)) # 逐元素取最大值 [4 5 6]ufunc通常有兩個有用的輸出參數out和where。out參數允許你將結果直接存入一個已存在的數組避免創建臨時數組節省內存。result np.empty_like(a) np.add(a, b, outresult) # 將ab的結果直接存入result print(result)where參數允許你進行條件計算只有滿足條件的元素才參與運算。arr np.array([1, 2, 3, 4, 5]) # 只在arr大于2的位置進行平方運算其他位置保持原值 result np.square(arr, wherearr2) print(result) # [1 2 9 16 25] 注意1和2的位置保留了原值但where不保證這些位置的值它們可能是未初始化的。更安全的做法是結合out參數。 safe_result arr.copy() np.square(arr, outsafe_result, wherearr2) print(safe_result) # [1 2 9 16 25]4.3 聚合函數從數據中提取信息聚合函數對數組的整個或某個軸上的數據進行統計計算返回一個標量或更小的數組。arr np.random.randn(1000) # 生成1000個標準正態分布隨機數 print(np.mean(arr)) # 平均值接近0 print(np.std(arr)) # 標準差接近1 print(np.var(arr)) # 方差 print(np.min(arr), np.max(arr)) # 最小最大值 print(np.median(arr)) # 中位數 print(np.sum(arr)) # 總和 print(np.prod(arr)) # 所有元素的乘積 # 對于多維數組可以指定軸axis arr_2d np.array([[1, 2, 3], [4, 5, 6]]) print(np.sum(arr_2d)) # 所有元素和 21 print(np.sum(arr_2d, axis0)) # 沿第0軸行求和即每列的和 [5 7 9] print(np.sum(arr_2d, axis1)) # 沿第1軸列求和即每行的和 [6 15]理解axis參數至關重要。你可以把axis想象成將要被“壓縮”掉的維度。axis0意味著沿著行的方向壓縮垂直方向結果的行數減少或變為1axis1意味著沿著列的方向壓縮水平方向結果的列數減少。5. 實戰演練用NumPy解決一個簡單圖像處理問題理論說了這么多我們用一個接近實際的小項目來串聯一下。假設我們有一張灰度圖片用二維數組表示值在0-255之間我們想實現一個簡單的“底片”效果反色和一個“閾值化”效果將低于某個值的像素變黑高于的變白。import numpy as np # 模擬一張5x5的小灰度圖數值范圍0-255 image np.array([ [100, 120, 130, 110, 90], [80, 140, 160, 100, 70], [200, 180, 50, 30, 220], [40, 60, 80, 150, 170], [10, 20, 250, 240, 230] ], dtypenp.uint8) # 使用無符號8位整數類型模擬圖像數據 print(原始圖像數據) print(image) # 1. 底片效果反色255 - 像素值 inverted_image 255 - image print(\n底片效果) print(inverted_image) # 2. 閾值化處理將像素值大于128的設為255白小于等于128的設為0黑 threshold 128 # 方法1使用布爾索引 binary_image np.zeros_like(image) # 創建一個全零的同形狀數組 binary_image[image threshold] 255 print(\n閾值化效果方法1) print(binary_image) # 方法2使用np.where函數更簡潔 binary_image_2 np.where(image threshold, 255, 0) print(\n閾值化效果方法2) print(binary_image_2) # 3. 計算圖像的一些基本統計信息 print(f\n圖像統計:) print(f 平均亮度: {np.mean(image):.2f}) print(f 最大亮度: {np.max(image)}) print(f 最小亮度: {np.min(image)}) print(f 亮度標準差: {np.std(image):.2f})這個簡單的例子展示了NumPy如何讓像素級的圖像操作變得異常簡潔高效。所有操作都是向量化的沒有顯式的循環。在實際中對于百萬像素的圖片這種效率優勢是決定性的。6. 性能陷阱與最佳實踐掌握了基本操作后要想寫出高性能的NumPy代碼還需要避開一些常見的陷阱。陷阱一在Python循環中操作NumPy數組元素。這是最大的性能殺手。永遠記住能向量化就向量化。# 糟糕的做法 arr np.random.rand(10000) result np.empty_like(arr) for i in range(len(arr)): result[i] arr[i] * 2 1 # 每次循環都有Python解釋器開銷 # 正確的做法向量化 result arr * 2 1 # 一次性在C層完成速度極快陷阱二不必要的拷貝。如前所述切片產生視圖花式索引產生拷貝。對于大數組無意識的拷貝會消耗大量內存和時間。使用arr.base屬性可以檢查一個數組是否是視圖base指向原數組還是拷貝base為None。陷阱三數據類型不匹配導致的隱式轉換。NumPy運算會向上轉換到更精確或更通用的數據類型?;旌蟟nt和float會得到float混合int32和int64可能會得到int64。這可能導致內存使用增加和性能下降。在創建數組時盡量使用合適的dtype。最佳實踐建議預分配數組在循環開始前使用np.zeros、np.empty或np.full分配好結果數組避免在循環中通過np.append或列表拼接來增長數組后者會帶來巨大的性能開銷。善用原地操作許多NumPy函數和運算符有對應的原地操作版本如、*或者函數的out參數。它們可以避免創建臨時數組。使用np.einsum進行復雜的張量運算對于多維數組的求和、乘積等操作np.einsum愛因斯坦求和約定語法極其強大且高效能表達復雜的線性代數運算并且通常由高度優化的后端實現。了解np.vectorize的局限性np.vectorize函數可以將一個接受標量輸入、輸出標量的Python函數“向量化”。但它本質上還是一個Python級別的循環只是語法上看起來像向量化。它的性能提升有限主要用于方便不能替代真正的ufunc。對于性能關鍵的部分應盡量使用NumPy內置的ufunc或用Cython/Numba等工具重寫。NumPy的世界遠不止這些基礎函數還有隨機數生成np.random、線性代數np.linalg、傅里葉變換np.fft等強大的子模塊。但萬變不離其宗深刻理解ndarray對象、廣播機制和向量化思想是自如運用所有這些高級功能的基礎。當你習慣用數組的思維方式去解決問題時你會發現很多原本復雜的計算任務都能被優雅地簡化為幾行高效的NumPy代碼。這就是NumPy的魅力所在。