45.10 array: Typed Arrays for Compact Storage
The array module provides a space-efficient alternative to lists when you need to store large sequences of homogeneous data types. While Python lists are incredibly flexible, capable of holding objects of different types, this flexibility comes with a memory overhead. Each element in a list is a full-fledged Python object, requiring storage for its value, type information, reference count, and other metadata. An array.array object, by contrast, stores elements as compact C-style data types directly in memory, significantly reducing storage overhead for large datasets of numbers or characters.
Creating Arrays and Type Codes
Arrays are created using the array(typecode, initializer) constructor. The typecode is a single character that dictates the type of data the array will hold and consequently, its memory footprint. The initializer can be an iterable (like a list or range) providing the initial values.
import array
# Arrays of signed integers
int_arr = array.array('i', [1, -2, 3, 4, 5]) # 4-byte signed int
short_arr = array.array('h', [10, 20, 30]) # 2-byte signed short
# Arrays of unsigned integers
uint_arr = array.array('I', [1, 2, 3, 4, 5]) # 4-byte unsigned int
# Arrays of floating-point numbers
float_arr = array.array('f', [1.0, 2.5, 3.14]) # 4-byte single-precision float
double_arr = array.array('d', [1.0, 2.5]) # 8-byte double-precision float
# Array of characters (unicode code points)
char_arr = array.array('u', 'hello') # Unicode character
It is crucial to choose the correct typecode. Using 'i' (signed int) for values you know will always be positive wastes the potential benefits of an unsigned type ('I'), which can store larger positive values in the same number of bytes. The array.typecodes attribute contains a string of all available type codes for reference.
Core Operations and List-like Behavior
Arrays behave like mutable sequences and support many familiar list operations, making them easy to adopt for developers accustomed to lists.
import array
arr = array.array('f', [1.5, 2.0, 3.5])
# Indexing and slicing
print(arr[0]) # Output: 1.5
arr[1] = 99.9 # Assignment by index
slice_of_arr = arr[1:3] # Creates a new array: array('f', [99.9, 3.5])
# Appending and extending
arr.append(4.0) # arr is now array('f', [1.5, 99.9, 3.5, 4.0])
arr.extend([5.5, 6.0]) # arr is now array('f', [1.5, 99.9, 3.5, 4.0, 5.5, 6.0])
# Removing elements
arr.pop(2) # Removes and returns the element at index 2 (3.5)
arr.remove(99.9) # Removes the first occurrence of 99.9
print(arr) # Output: array('f', [1.5, 4.0, 5.5, 6.0])
Memory Efficiency and Performance
The primary advantage of arrays is their compact memory representation. This can be demonstrated by comparing the memory footprint of a list and an array storing the same data.
import array
import sys
data = list(range(1000))
list_size = sys.getsizeof(data)
arr = array.array('i', data)
array_size = sys.getsizeof(arr)
print(f"List size: {list_size} bytes") # e.g., List size: 8856 bytes
print(f"Array size: {array_size} bytes") # e.g., Array size: 4064 bytes
print(f"Memory savings: {(1 - array_size/list_size) * 100:.1f}%")
This memory efficiency often translates to performance benefits, especially for numerical computations. Operations that process the entire array can be faster because there is less data to move from RAM into the CPU cache. Furthermore, arrays can be easily passed to C libraries (via ctypes or byteswap methods for endianness) for high-performance operations, a common technique in scientific computing libraries like NumPy, which internally use similar concepts.
Bytes Conversion and File I/O
A powerful feature of arrays is their ability to be converted to and from raw bytes with virtually no cost, as the underlying memory buffer is already a contiguous block of bytes. This makes them exceptionally efficient for reading from and writing to binary files.
import array
# Create an array and write it to a binary file
arr_to_save = array.array('d', [3.14159, 2.71828, 1.61803])
with open('data.bin', 'wb') as f:
arr_to_save.tofile(f)
# Read the data back into a new array
arr_loaded = array.array('d')
with open('data.bin', 'rb') as f:
# We must know the number of items or the file size to read correctly
num_items = os.path.getsize('data.bin') // arr_loaded.itemsize
arr_loaded.fromfile(f, num_items)
print(arr_loaded) # Output: array('d', [3.14159, 2.71828, 1.61803])
Common Pitfalls and Best Practices
Type Enforcement: Unlike lists, arrays are strictly typed. Attempting to insert a value of an incompatible type will raise a
TypeError. This prevents accidental data corruption but requires vigilance.int_arr = array.array('i', [1, 2, 3]) int_arr[0] = 3.14 # TypeError: integer argument expected, got floatSlicing Returns New Arrays: Slicing an array creates a new array object. For large arrays, this can be an expensive operation in terms of both memory and time. Use slicing judiciously.
Know Your Data’s Range: Using a typecode that is too small for your data will result in overflow or underflow, which is often silently ignored, leading to corrupted data.
byte_arr = array.array('b', [127]) # Max value for signed byte byte_arr[0] += 1 # Silently overflows to -128 print(byte_arr[0]) # Output: -128Use for Homogeneous Data: The benefits of
arrayare negated if you need to store heterogeneous data types. In such cases, a list is the appropriate tool.
In summary, the array module is a specialized tool for a specific job: storing large sequences of primitive, homogeneous data types in a memory-efficient manner. It is an ideal choice for applications like reading binary file formats, implementing custom data structures, or handling large numerical datasets where the overhead of a full Python list is prohibitive. For more advanced numerical work, the NumPy library builds upon these concepts, offering a vastly richer set of operations and optimized routines.