Python List 與 Array 的差異完整解析

深入理解兩種資料結構的特性與使用時機

Python List 與 Array 的差異完整解析

前言

在 Python 程式設計中,List 和 Array 是兩個容易混淆的概念。很多初學者會認為它們是相同的東西,但實際上它們有著本質上的差異。本文將深入探討這兩種資料結構的特性、使用場景和效能差異。

快速對照表

特性ListArray
資料類型可混合不同類型必須相同類型
匯入需求Python 內建需要匯入模組
記憶體效率較低較高
運算速度較慢較快
功能豐富度豐富基本
使用難易度簡單稍複雜

什麼是 Python List?

定義與特性

List 是 Python 內建的資料結構,可以儲存任意類型的元素序列。它是 Python 中最常用的資料結構之一。

# List 可以包含不同類型的元素
mixed_list = [1, "hello", 3.14, True, [1, 2, 3], {"key": "value"}]
print(f"混合類型 List: {mixed_list}")
print(f"類型: {type(mixed_list)}")

# List 是可變的(mutable)
numbers = [1, 2, 3, 4, 5]
numbers[0] = 10  # 修改元素
numbers.append(6)  # 新增元素
print(f"修改後: {numbers}")

List 的內部實作

Python List 在底層是使用動態陣列實作的:

import sys

# 觀察 List 的記憶體使用
list_sizes = []
list_obj = []

for i in range(20):
    list_obj.append(i)
    size = sys.getsizeof(list_obj)
    list_sizes.append((len(list_obj), size))
    
# 顯示結果
print("元素數量 | 記憶體大小(bytes)")
print("-" * 30)
for length, size in list_sizes[:10]:  # 顯示前10筆
    print(f"{length:8} | {size:10}")

List 的常用操作

# 1. 建立 List
# 空 List
empty_list = []
empty_list2 = list()

# 從其他可迭代物件建立
from_string = list("hello")
from_range = list(range(5))
from_tuple = list((1, 2, 3))

print(f"從字串: {from_string}")
print(f"從 range: {from_range}")
print(f"從 tuple: {from_tuple}")

# 2. List Comprehension
squares = [x**2 for x in range(10)]
even_squares = [x**2 for x in range(10) if x % 2 == 0]
matrix = [[i+j for j in range(3)] for i in range(3)]

print(f"\n平方數: {squares}")
print(f"偶數平方: {even_squares}")
print(f"矩陣: {matrix}")

# 3. 常用方法
fruits = ["apple", "banana", "orange"]

# 新增元素
fruits.append("grape")           # 尾端新增
fruits.insert(1, "mango")        # 指定位置插入
fruits.extend(["kiwi", "pear"])  # 擴展多個元素

# 移除元素
removed = fruits.pop()           # 移除並回傳最後一個
fruits.remove("banana")          # 移除指定值
# del fruits[0]                  # 刪除指定索引

# 搜尋與排序
print(f"'mango' 的索引: {fruits.index('mango')}")
print(f"'apple' 出現次數: {fruits.count('apple')}")

numbers = [3, 1, 4, 1, 5, 9, 2, 6]
numbers.sort()  # 原地排序
print(f"排序後: {numbers}")

sorted_numbers = sorted([3, 1, 4, 1, 5, 9, 2, 6])  # 回傳新的已排序 List
print(f"sorted() 結果: {sorted_numbers}")

什麼是 Python Array?

Python 中的 Array 主要有兩種:標準函式庫的 array 模組和第三方套件 NumPy 的 array。

1. 標準函式庫的 array

import array

# 建立 array 需要指定類型碼
# 'i' 代表 signed int
int_array = array.array('i', [1, 2, 3, 4, 5])
print(f"整數 array: {int_array}")
print(f"類型: {type(int_array)}")

# 常見的類型碼
# 'b': signed char (int, 1 byte)
# 'B': unsigned char (int, 1 byte)
# 'h': signed short (int, 2 bytes)
# 'H': unsigned short (int, 2 bytes)
# 'i': signed int (int, 4 bytes)
# 'I': unsigned int (int, 4 bytes)
# 'f': float (float, 4 bytes)
# 'd': double (float, 8 bytes)

# 不同類型的 array
float_array = array.array('f', [1.1, 2.2, 3.3])
char_array = array.array('u', 'hello')  # Unicode 字元

print(f"\n浮點數 array: {float_array}")
print(f"字元 array: {char_array}")

# array 只能存放相同類型
try:
    mixed_array = array.array('i', [1, 2, "3"])  # 這會出錯
except TypeError as e:
    print(f"\n錯誤: {e}")

# array 的操作方法
numbers = array.array('i', [1, 2, 3])
numbers.append(4)              # 新增元素
numbers.extend([5, 6, 7])      # 擴展
numbers.insert(0, 0)           # 插入
print(f"\n操作後: {numbers}")

# 型別資訊
print(f"型別碼: {numbers.typecode}")
print(f"每個元素大小: {numbers.itemsize} bytes")

2. NumPy Array

NumPy 是 Python 科學計算的核心套件,其 array 功能更加強大:

import numpy as np

# 建立 NumPy array
np_array = np.array([1, 2, 3, 4, 5])
print(f"NumPy array: {np_array}")
print(f"類型: {type(np_array)}")
print(f"資料型態: {np_array.dtype}")
print(f"形狀: {np_array.shape}")

# 建立不同維度的 array
# 1D array
arr_1d = np.array([1, 2, 3, 4])

# 2D array (矩陣)
arr_2d = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

# 3D array
arr_3d = np.array([[[1, 2], [3, 4]],
                   [[5, 6], [7, 8]]])

print(f"\n1D array: {arr_1d}")
print(f"2D array:\n{arr_2d}")
print(f"3D array:\n{arr_3d}")

# 使用內建函數建立
zeros = np.zeros((3, 3))          # 全零矩陣
ones = np.ones((2, 4))            # 全一矩陣
eye = np.eye(3)                   # 單位矩陣
random = np.random.rand(3, 3)     # 隨機矩陣

print(f"\n零矩陣:\n{zeros}")
print(f"\n單位矩陣:\n{eye}")

# 指定資料型態
int_array = np.array([1.1, 2.2, 3.3], dtype=int)
float_array = np.array([1, 2, 3], dtype=float)

print(f"\n強制轉換為整數: {int_array}")
print(f"強制轉換為浮點數: {float_array}")

記憶體使用比較

import sys
import numpy as np
import array

# 測試資料:10000 個整數
size = 10000
data = list(range(size))

# Python List
py_list = data
list_size = sys.getsizeof(py_list)

# array 模組
arr = array.array('i', data)
array_size = sys.getsizeof(arr)

# NumPy array
np_arr = np.array(data)
numpy_size = np_arr.nbytes

print(f"儲存 {size} 個整數的記憶體使用:")
print(f"Python List: {list_size:,} bytes")
print(f"array.array: {array_size:,} bytes")
print(f"NumPy array: {numpy_size:,} bytes")
print(f"\n記憶體效率:")
print(f"array 相比 List 節省: {(1 - array_size/list_size)*100:.1f}%")
print(f"NumPy 相比 List 節省: {(1 - numpy_size/list_size)*100:.1f}%")

# 詳細分析單個元素的記憶體使用
print(f"\n單個元素的記憶體開銷:")
print(f"Python int object: {sys.getsizeof(1)} bytes")
print(f"array element: {arr.itemsize} bytes")
print(f"NumPy element: {np_arr.itemsize} bytes")

效能比較

1. 基本運算效能

import time
import numpy as np
import array

# 準備測試資料
size = 1000000
py_list = list(range(size))
arr = array.array('i', range(size))
np_arr = np.arange(size)

# 測試函數
def time_operation(func, name):
    start = time.time()
    result = func()
    end = time.time()
    print(f"{name}: {(end - start)*1000:.2f} ms")
    return result

print("=== 求和運算 ===")
time_operation(lambda: sum(py_list), "Python List (sum)")
time_operation(lambda: sum(arr), "array.array (sum)")
time_operation(lambda: np.sum(np_arr), "NumPy array (np.sum)")

print("\n=== 元素平方運算 ===")
time_operation(lambda: [x**2 for x in py_list[:10000]], "List comprehension")
time_operation(lambda: np_arr[:10000]**2, "NumPy vectorized")

print("\n=== 元素相乘 ===")
def list_multiply():
    return [a * b for a, b in zip(py_list[:10000], py_list[:10000])]

def numpy_multiply():
    return np_arr[:10000] * np_arr[:10000]

time_operation(list_multiply, "List zip multiply")
time_operation(numpy_multiply, "NumPy multiply")

2. 索引存取效能

import random

# 隨機存取測試
indices = [random.randint(0, size-1) for _ in range(10000)]

print("\n=== 隨機存取 10000 次 ===")

def list_access():
    return [py_list[i] for i in indices]

def array_access():
    return [arr[i] for i in indices]

def numpy_access():
    return np_arr[indices]  # NumPy 支援向量化索引

time_operation(list_access, "Python List")
time_operation(array_access, "array.array")
time_operation(numpy_access, "NumPy array")

實際應用場景

1. 何時使用 List

# 場景 1: 儲存異質資料
user_data = [
    "John Doe",           # 姓名 (str)
    25,                   # 年齡 (int)
    175.5,                # 身高 (float)
    True,                 # 是否活躍 (bool)
    ["Python", "Java"],   # 技能 (list)
    {"city": "Taipei"}    # 其他資訊 (dict)
]

# 場景 2: 動態資料收集
collected_data = []
for i in range(10):
    # 模擬收集不同類型的資料
    if i % 2 == 0:
        collected_data.append(i)
    else:
        collected_data.append(f"item_{i}")

print(f"收集的資料: {collected_data}")

# 場景 3: 資料處理管線
def process_pipeline(data):
    # 可以存放不同階段的處理結果
    results = []
    results.append(("原始資料", data))
    results.append(("過濾後", [x for x in data if x > 0]))
    results.append(("排序後", sorted(data)))
    return results

pipeline_results = process_pipeline([3, -1, 4, 1, -5, 9])
for stage, result in pipeline_results:
    print(f"{stage}: {result}")

2. 何時使用 Array

# 場景 1: 大量數值運算
import numpy as np

# 影像處理(影像是數值矩陣)
image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
# 調整亮度
brightened = np.clip(image + 50, 0, 255)

# 場景 2: 科學計算
# 計算統計資料
data = np.random.normal(100, 15, 10000)  # 平均100, 標準差15
print(f"\n統計資料:")
print(f"平均值: {np.mean(data):.2f}")
print(f"標準差: {np.std(data):.2f}")
print(f"最小值: {np.min(data):.2f}")
print(f"最大值: {np.max(data):.2f}")

# 場景 3: 矩陣運算
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

print(f"\n矩陣運算:")
print(f"A + B =\n{A + B}")
print(f"A × B =\n{np.dot(A, B)}")
print(f"A 的轉置 =\n{A.T}")

# 場景 4: 訊號處理
# 產生正弦波
sample_rate = 1000
duration = 1
t = np.linspace(0, duration, sample_rate)
frequency = 5
signal = np.sin(2 * np.pi * frequency * t)

# 加入雜訊
noise = np.random.normal(0, 0.1, len(signal))
noisy_signal = signal + noise

進階比較:List vs NumPy Array

1. 記憶體配置方式

# List: 存放物件的參照
list_example = [1, 2, 3]
# 實際上是: [指向int(1)的指標, 指向int(2)的指標, 指向int(3)的指標]

# NumPy: 連續的記憶體區塊
np_example = np.array([1, 2, 3])
# 實際上是: [1][2][3] (連續存放的數值)

# 視覺化差異
import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

# List 結構圖
ax1.text(0.5, 0.9, 'Python List', ha='center', va='center', fontsize=14, weight='bold')
ax1.text(0.5, 0.7, 'List Object', ha='center', va='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue"))

# 畫箭頭指向各個物件
for i, val in enumerate([1, 2, 3]):
    x = 0.2 + i * 0.3
    ax1.arrow(0.5, 0.6, x-0.5, -0.25, head_width=0.03, head_length=0.02, fc='black', ec='black')
    ax1.text(x, 0.2, f'int({val})', ha='center', va='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow"))

# NumPy 結構圖
ax2.text(0.5, 0.9, 'NumPy Array', ha='center', va='center', fontsize=14, weight='bold')
ax2.text(0.5, 0.7, 'Array Header', ha='center', va='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen"))
ax2.text(0.5, 0.4, '[1][2][3]', ha='center', va='center', bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral"))
ax2.text(0.5, 0.2, '連續記憶體', ha='center', va='center', fontsize=12)

for ax in [ax1, ax2]:
    ax.set_xlim(0, 1)
    ax.set_ylim(0, 1)
    ax.axis('off')

plt.tight_layout()
# plt.show()  # 如果要顯示圖形

2. 向量化運算

# List 需要迴圈
list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]

# 相加
list_sum = []
for i in range(len(list1)):
    list_sum.append(list1[i] + list2[i])
print(f"List 相加 (迴圈): {list_sum}")

# 或使用 list comprehension
list_sum2 = [a + b for a, b in zip(list1, list2)]
print(f"List 相加 (comprehension): {list_sum2}")

# NumPy 向量化運算
arr1 = np.array([1, 2, 3, 4, 5])
arr2 = np.array([6, 7, 8, 9, 10])

arr_sum = arr1 + arr2  # 直接相加!
print(f"NumPy 相加: {arr_sum}")

# 更複雜的運算
result = (arr1 ** 2 + arr2 ** 2) ** 0.5  # 計算歐幾里得距離
print(f"複雜運算結果: {result}")

3. 廣播(Broadcasting)

# NumPy 的廣播機制
arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])

# 數值與陣列運算
print("原始陣列:")
print(arr)

print("\n陣列 + 10:")
print(arr + 10)  # 自動將 10 廣播到每個元素

print("\n陣列 * [1, 2, 3]:")
print(arr * [1, 2, 3])  # 自動將 [1,2,3] 廣播到每一行

# List 無法這樣做
try:
    list_2d = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    result = list_2d + 10  # 這會出錯
except TypeError as e:
    print(f"\nList 不支援廣播: {e}")

選擇指南

決策流程圖

def choose_data_structure(requirements):
    """幫助選擇適合的資料結構"""
    
    print("=== 資料結構選擇指南 ===\n")
    
    # 問題 1
    mixed_types = input("需要儲存不同類型的資料嗎?(y/n): ").lower() == 'y'
    if mixed_types:
        print("→ 建議使用 Python List")
        return "List"
    
    # 問題 2
    numerical_ops = input("需要進行大量數值運算嗎?(y/n): ").lower() == 'y'
    if numerical_ops:
        print("→ 建議使用 NumPy Array")
        return "NumPy"
    
    # 問題 3
    size = input("資料量大嗎(超過10萬筆)?(y/n): ").lower() == 'y'
    if size:
        memory_critical = input("記憶體使用是關鍵考量嗎?(y/n): ").lower() == 'y'
        if memory_critical:
            print("→ 建議使用 array.array 或 NumPy Array")
            return "array.array"
    
    # 預設
    print("→ 建議使用 Python List (最通用)")
    return "List"

# 執行選擇器(取消註解以執行)
# recommendation = choose_data_structure({})

實用建議總結

# 1. 一般程式設計:使用 List
shopping_cart = ["蘋果", "香蕉", "牛奶"]
user_info = ["Alice", 25, "alice@email.com"]

# 2. 數值計算:使用 NumPy
import numpy as np
temperatures = np.array([23.5, 24.1, 22.8, 25.3, 23.9])
average_temp = np.mean(temperatures)

# 3. 記憶體受限但不需要 NumPy 功能:使用 array
import array
sensor_readings = array.array('f', [0.0] * 1000000)  # 預分配空間

# 4. 資料分析:使用 NumPy 或 Pandas
data_matrix = np.random.randn(1000, 50)  # 1000筆資料,50個特徵
correlation = np.corrcoef(data_matrix.T)  # 計算相關係數

常見錯誤與陷阱

1. 類型混淆

# 錯誤:試圖在 array 中混合類型
import array

try:
    # 這會失敗
    mixed = array.array('i', [1, 2, 3.14])
except TypeError as e:
    print(f"錯誤: {e}")

# 正確:使用適當的類型碼
float_arr = array.array('f', [1, 2, 3.14])  # 使用 'f' 來存浮點數
print(f"正確: {float_arr}")

2. 效能迷思

# 迷思:NumPy 總是比較快
# 事實:對於小資料或簡單操作,List 可能更快

import time

small_list = [1, 2, 3, 4, 5]
small_array = np.array([1, 2, 3, 4, 5])

# 小資料的簡單操作
start = time.time()
for _ in range(100000):
    _ = small_list[2]
list_time = time.time() - start

start = time.time()
for _ in range(100000):
    _ = small_array[2]
array_time = time.time() - start

print(f"存取單一元素 (100,000次):")
print(f"List: {list_time:.4f} 秒")
print(f"NumPy: {array_time:.4f} 秒")

3. 記憶體管理

# List 的記憶體增長策略
import sys

lst = []
sizes = []

for i in range(20):
    lst.append(i)
    sizes.append(sys.getsizeof(lst))

# 觀察記憶體增長模式
growth_points = []
for i in range(1, len(sizes)):
    if sizes[i] > sizes[i-1]:
        growth_points.append((i, sizes[i] - sizes[i-1]))

print("List 記憶體增長點:")
for index, growth in growth_points:
    print(f"在第 {index} 個元素時增長 {growth} bytes")

總結

核心差異回顧

  1. 類型限制

    • List:無限制,可混合任意類型
    • Array:嚴格限制,只能相同類型
  2. 記憶體效率

    • List:較低(存放物件參照)
    • Array:較高(直接存放數值)
  3. 運算效能

    • List:需要迴圈,較慢
    • NumPy Array:向量化運算,極快
  4. 使用便利性

    • List:內建、簡單、功能豐富
    • Array:需匯入、功能較少

選擇建議

  • 預設選擇 List:除非有特定理由
  • 大量數值運算選 NumPy:科學計算、資料分析、機器學習
  • 記憶體關鍵選 array.array:嵌入式系統、大量簡單數值
  • 混合使用:根據不同階段的需求轉換

記住:「沒有最好的資料結構,只有最適合的資料結構」。理解它們的差異,才能在正確的場景使用正確的工具!

0%