6.10. Rozgłaszanie (broadcasting)

Gdy operator binarny otrzymuje dwie tablice o kształtach, które nie pasują dokładnie, numpy nie zgłasza błędu – rozgłasza je. Rozgłaszanie to niewielki zestaw reguł decydujących, czy dwa kształty są zgodne, a jeśli tak, to w jaki sposób mniejszy z nich jest wirtualnie rozciągany, aby dopasować się do większego.

6.10.1. Reguły

Gdy dwa operandy mają kształty A i B, numpy przetwarza je w dwóch krokach.

  1. Dopasowanie rzędów. Jeśli jeden operand ma mniej osi niż drugi, numpy wirtualnie uzupełnia przód jego kształtu osiami o rozmiarze 1, aż oba kształty będą miały tę samą liczbę osi. Operand 1-W o kształcie (3,) w parze z operandem 2-W o kształcie (2, 3) staje się (1, 3) wobec (2, 3).

  2. Sprawdzenie każdej osi. Przechodząc przez kształty o teraz równej długości oś po osi, każda para rozmiarów musi spełniać jeden z dwóch warunków: rozmiary są równe albo jeden z nich wynosi 1. Oś o rozmiarze 1 jest wirtualnie rozciągana do rozmiaru drugiej strony na potrzeby operacji. Para (1, 3) wobec (2, 3) jest zgodna, ponieważ pierwsza oś ma 1 (rozciąga się do 2), a druga oś pasuje (3 == 3); wynik ma kształt (2, 3).

Jeśli któraś para osi nie spełnia żadnego z warunków, kształty są niezgodne i operator zgłasza ValueError.

6.10.2. Przykłady

Skalar wobec dowolnej tablicy. Skalar zachowuje się jak kształt (1,) i rozciąga się do czegokolwiek:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float)
a + 10                 # (2, 3) + scalar -> (2, 3)

Wektor 1-W wobec macierzy 2-W. Reguła 1 dokleja z przodu oś o rozmiarze 1, zamieniając (3,) w (1, 3); reguła 2 następnie rozciąga ten wiersz w dół wzdłuż każdej kolumny a

row = np.array([100, 200, 300], dtype=np.float)
a + row                # (2, 3) + (3,) -> (2, 3)

Dwie tablice 1-W o równej długości dodają się element po elemencie – rozgłaszanie nie jest potrzebne:

np.arange(4) + np.arange(4)

Wektor kolumnowy wobec wektora wierszowego daje 2-W kształt „zewnętrzny”: (4, 1) w parze z (3,) staje się (4, 1) wobec (1, 3) po doklejeniu rzędu, a reguła 2 rozciąga każdy operand wzdłuż jego osi o rozmiarze 1:

x = np.array([1, 2, 3, 4]).reshape((4, 1))     # column
y = np.array([10, 20, 30])                      # row
x + y                                           # (4, 3) matrix

Te same reguły kształtu obowiązują dla każdego dwuargumentowego ufunc, w tym arctan2()

np.arctan2(y, 1.0)
np.arctan2(y, x)

6.10.3. Czego rozgłaszanie nie alokuje

Rozciąganie jest wirtualne. numpy przechodzi przez oba operandy jednocześnie, ponownie odczytując mniejszy z nich wzdłuż jego osi rozgłaszania zamiast go kopiować. Dane krótszej tablicy nigdy nie są powielane w pamięci.

Dla pamięci znaczenie ma rozmiar tablicy wyjściowej. a + row alokuje wyjście o kształcie a, a nie o kształcie a powiększonym o kształt row. Długie łańcuchy rozgłaszania nadal mogą tworzyć duże wyniki pośrednie.

6.10.4. Gdy rozgłaszanie idzie nie tak

Klasycznym niepowodzeniem są dwa kształty, w których żaden nie ma osi o rozmiarze 1 do rozciągnięcia, a rozmiary się nie zgadzają – na przykład (3, 4) wobec (4, 3). Reguła 2 nie potrafi dopasować 3 do 4, więc numpy zgłasza ValueError.

Subtelniejszym problemem jest rozgłaszanie, które się powiedzie, ale nie w sposób zamierzony przez aplikację. (5,) wobec (5, 1) to kanoniczny przypadek: doklejenie rzędu zamienia (5,) w (1, 5), co rozgłasza się wobec (5, 1), dając macierz (5, 5) – iloczyn zewnętrzny dwóch wektorów, a nie wynik element po elemencie o długości 5, którego aplikacja prawdopodobnie chciała. W razie wątpliwości wypisz shape po obu stronach i prześledź reguły, zanim sięgniesz po reshape() lub transpose().