Skip to content

    Chapter 10, Basic

    Copies and views

    Almost every confusing bug with arrays is, at its core, a case of "I changed this and it changed that." Understanding when NumPy copies and when it shares eliminates this class of errors.

    Code for this chapter: básico/cap10_copias_views.py

    A view shares memory

    When you slice an array, NumPy does not copy the numbers: it creates a new object that points to the same memory, with a different start and a different size. That’s why slicing costs almost nothing, even in huge arrays. The consequence is that writing to the slice writes to the original:

    básico/cap10_copias_views.pylinhas 10 a 16
    import numpy as np
    
    original = np.arange(6)
    fatia = original[1:4]
    fatia[0] = 99
    print(original)
    print(np.shares_memory(original, fatia), fatia.base is original)
    
    Output
    [ 0 99  2  3  4  5]
    True True
    

    The attribute base points to the array that owns the memory. A view has base, and an array that owns its own data has base equal to None.

    When you want an independent array

    For a true copy, ask explicitly with .copy():

    básico/cap10_copias_views.pylinhas 21 a 23
    independente = original[1:4].copy()
    independente[0] = -1
    print(original[1], np.shares_memory(original, independente), independente.base is None)
    
    Output
    99 False True
    
    OperationReturnsNote
    Slice a[1:4]ViewAlways
    a.reshape(...)View, if possibleCopy only if memory does not allow
    a.T, a.transpose()View
    a.ravel()View, if possibleflatten() always copies
    a[mascara], a[[0, 2]]Copy
    a + 1, np.sqrt(a)New arrayArithmetic operations create a new result

    The same object, two names

    The assignment b = a does not copy anything: it creates a second name for the same array (the same label model from the Python course). And the compound assignment operators (+=) alter the array in place, while b = b + 10 creates a new one:

    básico/cap10_copias_views.pylinhas 28 a 33
    a = np.array([1, 2, 3])
    b = a
    b += 10
    print(a)
    b = b + 10
    print(a, b)
    
    Output
    [11 12 13]
    [11 12 13] [21 22 23]
    

    The b += 10 also changed the a, because both are the same object. On the other hand, b = b + 10 created a new array and linked it to the name b, and the a remained as it was.

    Functions that modify what they receive

    An array passed to a function is the same object, and not a copy. If the function alters the array, the caller sees the change. This is useful for saving memory and dangerous when it happens unintentionally:

    básico/cap10_copias_views.pylinhas 38 a 45
    def zerar_negativos(a):
        a[a < 0] = 0
        return a
    
    
    dados = np.array([3, -1, 4, -5])
    resultado = zerar_negativos(dados)
    print(dados, resultado is dados)
    
    Output
    [3 0 4 0] True
    

    The dados originals have been modified, and the resultado is the same object. A version that does not alters the input uses an operation that creates a new result, like np.maximum(a, 0).

    `asarray` and `array`

    The np.asarray(x) does not copy if x it is already an array, and the np.array(x) copies by default. Functions that accept "anything resembling an array" usually start with np.asarray(entrada), which converts lists without copying what is already an array:

    básico/cap10_copias_views.pylinhas 50 a 51
    x = np.arange(3)
    print(np.asarray(x) is x, np.array(x) is x)
    
    Output
    True False
    

    The rule I follow

    If I am going to modify an array that came from outside (from another function, from a parameter, from a slice), I do .copy() first. If I am only going to read, I do not copy. And when a function intentionally alters the argument, its name or documentation states that.

    Exercise 1

    Normalize without mutating the input

    Write normalizar(a), which returns (a - mínimo) / (máximo - mínimo) without altering the received array.