Модуль concurrent.futures

Библиотека NumPy

Модуль concurrent.futures

В этом модуле описано 2 класса, с помощью которых удобно запускать параллельные вычисления.

In [1]:
import concurrent.futures

Класс concurrent.futures.ThreadPoolExecutor

In [2]:
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for i in range(100):
        executor.submit(print, i, end=", ")
01, , 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1213, 14, 15, 16, 17, 18, 19, 20, 21, 22, , 23, 2425, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, , 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87888990, 91, 92, 93, , 94, 95, 96, 97, 98, 99, , , 
In [3]:
# Taken from Python Docs.
import urllib.request

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://europe.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://some-made-up-domain.com/']

# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # Start the load operations and mark each future with its URL
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print(f"{url} generated an exception: {exc}")
        else:
            print(f"{url} page is {len(data)} bytes")
http://some-made-up-domain.com/ generated an exception: <urlopen error [Errno -2] Name or service not known>
http://www.foxnews.com/ page is 218628 bytes
http://europe.wsj.com/ generated an exception: HTTP Error 404: Not Found
http://www.cnn.com/ page is 1141683 bytes
http://www.bbc.co.uk/ page is 262659 bytes

Класс concurrent.futures.ProcessPoolExecutor

In [4]:
with concurrent.futures.ProcessPoolExecutor(max_workers=10) as executor:
    for i in range(100):
        executor.submit(print, i, end=", ")
7, 17, 32, 42, 52, 70, 75, 84, 94, 9, 21, 31, 47, 56, 60, 63, 66, 81, 91, 0, 10, 30, 46, 57, 62, 73, 83, 97, 1, 11, 14, 23, 29, 37, 41, 51, 79, 88, 8, 20, 24, 33, 48, 58, 68, 78, 93, 98, 2, 12, 19, 34, 44, 59, 69, 72, 76, 86, 96, 4, 18, 28, 45, 55, 65, 80, 90, 5, 15, 25, 38, 50, 54, 64, 74, 95, 99, 6, 16, 26, 36, 40, 49, 53, 61, 71, 82, 87, 92, 3, 13, 22, 27, 35, 39, 43, 67, 77, 85, 89, 
In [5]:
# Taken and modified from Python Docs.
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n == 2:
        return True
    if n < 2 or n % 2 == 0:
        return False
    
    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

with concurrent.futures.ProcessPoolExecutor() as executor:
    for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
        print(f"{number} is {'' if prime else 'not '}prime")
112272535095293 is prime
112582705942171 is prime
112272535095293 is prime
115280095190773 is prime
115797848077099 is prime
1099726899285419 is not prime

В чем разница между Process и Thread?

Попробуем понять из примера:

In [2]:
numbers_thread = list()

def append_thread(item):
    numbers_thread.append(item)
In [3]:
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    for i in range(20):
        executor.submit(append_thread, i)
In [4]:
numbers_thread
Out[4]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

А что происходит с процессами?

In [5]:
numbers_process = list()

def append_process(item):
    numbers_process.append(item)
In [6]:
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
    for i in range(20):
        executor.submit(append_process, i)
In [7]:
numbers_process
Out[7]:
[]

Каждый процесс (Process) предоставляет все ресурсы для запуска программы.

Это значит, что каждому из них выделены свое пространство адресов, свой идентификатор, своя память и т.д. и предоставлен как минимум один поток.

Поток выполнения (Thread) же является сущностью внутри процесса, которую тот может запускать. Потоки имеют общее пространство адресов и общие системные ресурсы (память и т.п.).

Thread работает в рамках вашей сессии и может менять переменные внутри нее.

Process работает в отдельной сессии: для любого изменения вашей переменной необходимо ее скопировать в другую сессию.

Как вы думаете, что из этого работает быстрее?

Давайте проверим:

Создадим функцию, которая будет работать примерно секунду.

In [6]:
def long_sum():
    s = 0
    for i in range(20_000_000 + 1):
        s += i
    return s
In [7]:
%%time

long_sum()
CPU times: user 1.06 s, sys: 1.23 ms, total: 1.06 s
Wall time: 1.06 s
Out[7]:
200000010000000
In [8]:
%%time

for _ in range(10):
    long_sum()
CPU times: user 10.1 s, sys: 0 ns, total: 10.1 s
Wall time: 10.1 s

Теперь попробуем посчитать эту сумму несколько раз параллельно:

In [9]:
%%time

with concurrent.futures.ProcessPoolExecutor(max_workers=10) as executor:
    for _ in range(10):
        executor.submit(long_sum)
CPU times: user 18 ms, sys: 26.3 ms, total: 44.3 ms
Wall time: 2.97 s
In [10]:
%%time

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for _ in range(10):
        executor.submit(long_sum)
CPU times: user 14.2 s, sys: 217 ms, total: 14.4 s
Wall time: 14.1 s

wtf.gif

Какая-то ерунда; почему в случае Thread расчет идет еще дольше, чем в простом цикле?

Ответ заключается в архитектуре CPython.

Идея состоит в том, что в Python реализован механизм Global Interpreter Lock (GIL), запрещающий отдельным потокам внутри процесса Python выполняться одновременно.

GIL_wiki.gif

Как выполняются три потока одновременно. Источник - Wikipedia Commons.

Зачем тогда вообще нужны потоки?

Потоки медленнее только при активных вычислениях; быстрее потоки могут:

a) Ждать:

In [11]:
import time
In [12]:
%%time

for _ in range(10):
    time.sleep(2)
CPU times: user 2.26 ms, sys: 33 µs, total: 2.29 ms
Wall time: 20 s
In [13]:
%%time

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for _ in range(10):
        executor.submit(time.sleep, 2)
CPU times: user 2.26 ms, sys: 3.93 ms, total: 6.19 ms
Wall time: 2 s

Зачем вообще может понадобиться ждать?

Когда вы запускаете множество подпроцессов, вам приходится ждать их завершения (как десятки картирований на геном, сотни докингов и т.п.). Потоки прекрасно справятся с такой задачей.

b) Работать с операциями, на которые не распространяется GIL.

Обычно это потенциально блокирующие или очень длительные операции, как ввод-вывод (на экран и в файлы), обработка изображений или NumPy "number crunching".

In [14]:
a = numpy.arange(1_000_000)
a
Out[14]:
array([     0,      1,      2, ..., 999997, 999998, 999999])
In [15]:
%%time

sum(a)
CPU times: user 194 ms, sys: 0 ns, total: 194 ms
Wall time: 193 ms
Out[15]:
499999500000
In [16]:
%%time

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for _ in range(100):
        executor.submit(sum, a)
CPU times: user 19.9 s, sys: 136 ms, total: 20.1 s
Wall time: 19.8 s
In [17]:
%%time

numpy.sum(a)
CPU times: user 2.32 ms, sys: 25 µs, total: 2.35 ms
Wall time: 1.24 ms
Out[17]:
499999500000
In [18]:
%%time

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for _ in range(100):
        executor.submit(numpy.sum, a)
CPU times: user 119 ms, sys: 4.17 ms, total: 123 ms
Wall time: 23.8 ms

c) Когда требуется одновременная работа двух разных алгоритмов.

Наиболее простой пример - responsive user interface.

Ваша программа не должна полностью зависать во время подсчета; именно это и реализуется потоками.

Когда что использовать?

Простое правило:

  • Когда у вас длительные параллельные вычисления в Python, используйте multiprocessing;
  • Когда у вас параллельный запуск других программ из Python, используйте threading;
  • Когда вы проводите параллельные вычисления с NumPy, используйте threading;
  • Когда вам доступно одно ядро процессора, используйте threading.

Библиотека NumPy

NumPy [/ˈnʌmpaɪ/ (NUM-py)] - библиотека языка Python, добавляющая поддержку многомерных массивов и матриц, а также целую коллекцию формул и высокоуровневых математических функций для работы с массивами и матрицами.

In [19]:
import numpy as np
In [20]:
np.__version__
Out[20]:
'1.17.2'

Давайте подумаем, зачем нужен целый модуль для математики, массивов и матриц?

Мне будет достаточно списков! - they said.

Что есть в numpy?

numpy-operations.png

Зачем же нужен numpy?

Сколько программистов нужно, чтобы заменить лампочку?

Ни одного - проблема тут по части железа.

Numpy нужен для векторизованных вычислений!

In [21]:
mln = 10 ** 6
In [22]:
a = list(range(mln))
In [23]:
%%timeit

[e * e for e in a]
61.7 ms ± 548 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [24]:
a = np.arange(mln)
In [25]:
%%timeit

a * a
2.31 ms ± 48.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Какая операция здесь самая медленная?

Конечно же, цикл!

Ещё дольше выполняются вложенные циклы:

In [26]:
rows, cols = 1000, 1000

a = [list(range(i, i + cols)) for i in range(0, rows)]
b = [[0 for j in range(cols)] for i in range(rows)]
In [27]:
%%timeit

for i in range(rows):
    for j in range(cols):
        b[i][j] = 2 * a[i][j]
133 ms ± 2.05 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Сравним с векторизованными циклами:

In [28]:
a = np.asarray([list(range(i, i + cols)) for i in range(0, rows)])
In [29]:
%%timeit

2 * a
2.42 ms ± 85.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Выигрыш во времени примерно в 50 раз!

Массив numpy.array

В чем его отличие от списка?

list-array.png

Почему в массиве важно иметь один и тот же тип данных?

Создание массива

Самый простой способ создать массив numpy.array - из стандартного списка list.

In [30]:
mydata = np.array([1, 2, 3])
mydata
Out[30]:
array([1, 2, 3])

vis-numpy-2.jpg

Массив может быть и многомерным!

In [31]:
mydata_2d = np.array([[1, 2],
                      [3, 4]])
mydata_2d
Out[31]:
array([[1, 2],
       [3, 4]])

vis-numpy-10.jpg

Важнейшая информация о массиве

numpy-array-small.png

In [32]:
mydata = np.array([[1, 2, 3, 4, 5],
                   [6, 7, 8, 9, 0],
                   [7, 0, 8, 8, 1]], dtype=float)
mydata
Out[32]:
array([[1., 2., 3., 4., 5.],
       [6., 7., 8., 9., 0.],
       [7., 0., 8., 8., 1.]])
In [33]:
mydata.dtype
Out[33]:
dtype('float64')

Типы данных в numpy

Тип данных Описание
bool Boolean (True or False) stored as a byte
int Platform integer (normally either int32 or int64)
int8 Byte (-128 to 127)
int16 Integer (-32768 to 32767)
int32 Integer (-2147483648 to 2147483647)
int64 Integer (-9223372036854775808 to 9223372036854775807)
uint8 Unsigned integer (0 to 255)
uint16 Unsigned integer (0 to 65535)
uint32 Unsigned integer (0 to 4294967295)
uint64 Unsigned integer (0 to 18446744073709551615)
float Shorthand for float64
float32 Single precision float: sign bit, 8 bits exponent, 23 bits mantissa
float64 Double precision float: sign bit, 11 bits exponent, 52 bits mantissa
complex Shorthand for complex128
complex64 Complex number, represented by two 32-bit floats (real and imaginary components)
complex128 Complex number, represented by two 64-bit floats (real and imaginary components)
In [34]:
mydata.astype(np.int8)
Out[34]:
array([[1, 2, 3, 4, 5],
       [6, 7, 8, 9, 0],
       [7, 0, 8, 8, 1]], dtype=int8)

Ещё важнейшая информация о массиве

In [35]:
mydata = np.array([[1, 2, 3, 4, 5],
                   [6, 7, 8, 9, 0],
                   [7, 0, 8, 8, 1]], dtype=np.int8)
mydata
Out[35]:
array([[1, 2, 3, 4, 5],
       [6, 7, 8, 9, 0],
       [7, 0, 8, 8, 1]], dtype=int8)
In [36]:
len(mydata)
Out[36]:
3
In [37]:
mydata.ndim
Out[37]:
2
In [38]:
mydata.shape
Out[38]:
(3, 5)
In [39]:
mydata.strides
Out[39]:
(5, 1)

numpy.array.shape

dimensions.png

Array reshaping

In [40]:
data = np.array([1, 2, 3, 4, 5, 6], dtype=np.int8)
In [41]:
data.reshape(2, 3)
Out[41]:
array([[1, 2, 3],
       [4, 5, 6]], dtype=int8)
In [42]:
data.reshape(3, 2)
Out[42]:
array([[1, 2],
       [3, 4],
       [5, 6]], dtype=int8)

vis-numpy-20.jpg

Array reshaping

In [43]:
data
Out[43]:
array([1, 2, 3, 4, 5, 6], dtype=int8)
In [44]:
data.reshape(2, -1)
Out[44]:
array([[1, 2, 3],
       [4, 5, 6]], dtype=int8)
In [45]:
data.reshape(-1, 3)
Out[45]:
array([[1, 2, 3],
       [4, 5, 6]], dtype=int8)
In [46]:
data.reshape(3, 3)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-46-013b97b673e4> in <module>
----> 1 data.reshape(3, 3)

ValueError: cannot reshape array of size 6 into shape (3,3)

Array reshaping

In [47]:
data = np.array([[1, 2, 3],
                 [4, 5, 6]], dtype=np.int8)
In [48]:
data2 = data.reshape(-1)
data2
Out[48]:
array([1, 2, 3, 4, 5, 6], dtype=int8)
In [49]:
data[0,2] = -1

data2
Out[49]:
array([ 1,  2, -1,  4,  5,  6], dtype=int8)

Что это означает? :^)

Array reshaping

In [50]:
data = np.array([[1, 2, 3],
                 [4, 5, 6]], dtype=np.int8)
In [51]:
a = data.ravel()
a
Out[51]:
array([1, 2, 3, 4, 5, 6], dtype=int8)
In [52]:
b = data.flatten()
b
Out[52]:
array([1, 2, 3, 4, 5, 6], dtype=int8)
In [53]:
data[0,0] = -1
In [54]:
a, b
Out[54]:
(array([-1,  2,  3,  4,  5,  6], dtype=int8),
 array([1, 2, 3, 4, 5, 6], dtype=int8))

Транспонирование

Что значит "транспонировать матрицу"?

Как транспонировать в стандартном Python?

In [55]:
list_data = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
list_T = list(zip(*list_data))
list_T
Out[55]:
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
In [56]:
data = np.array(list_data)
data.T
Out[56]:
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])
In [57]:
data.transpose()
Out[57]:
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])

Другие способы инициализации numpy.array...

Но сначала... Кто знает, что называют индусским кодом? :^)

In [58]:
int a0 = 0;
int a1 = 0;
int a2 = 0;
int a3 = 0;
int a4 = 0;
int a5 = 0;
int a6 = 0;
int a7 = 0;
int a8 = 0;
int a9 = 0;

Допустим, мы хотим сделать массив numpy.array из нулей.

Сделаем способом, который помним!

In [59]:
x = 10
y = 3
zeros = []
for i in range(y):
    sub_zero = []
    for j in range(x):
        sub_zero.append(0)
    zeros.append(sub_zero)
zeros = np.array(zeros)
zeros
Out[59]:
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

jack-sparrow.jpg

Как сделать это нормально?

В стандартном Python:

In [60]:
x = 10
y = 3
zeros = [[0 for _ in range(x)] for _ in range(y)]
zeros
Out[60]:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
In [61]:
[[0] * x] * y
Out[61]:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
In [62]:
np.zeros(shape=(3, 10), dtype=int)
Out[62]:
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

Чтобы создавать простые массивы numpy.array, можно воспользоваться специальными функциями:

In [63]:
np.ones(3)
Out[63]:
array([1., 1., 1.])
In [64]:
np.ones(shape=(3,))
Out[64]:
array([1., 1., 1.])
In [65]:
np.random.random(3)
Out[65]:
array([0.57664778, 0.72303613, 0.44506472])
In [66]:
np.full(3, fill_value=-8.0)
Out[66]:
array([-8., -8., -8.])

vis-numpy-3.jpg

Аналогично с многомерными массивами:

In [67]:
np.zeros(shape=(3, 2))
Out[67]:
array([[0., 0.],
       [0., 0.],
       [0., 0.]])
In [68]:
np.random.random((2, 3))
Out[68]:
array([[0.42878116, 0.01896822, 0.23738247],
       [0.68345334, 0.5032586 , 0.86208521]])
In [69]:
np.full((2, 4), fill_value="F")
Out[69]:
array([['F', 'F', 'F', 'F'],
       ['F', 'F', 'F', 'F']], dtype='<U1')

vis-numpy-11.jpg

Массив такого же размера, но из нулей:

In [70]:
data
Out[70]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
In [71]:
np.zeros_like(data)
Out[71]:
array([[0, 0, 0],
       [0, 0, 0],
       [0, 0, 0]])

E - единичный массив

In [72]:
np.eye(5)
Out[72]:
array([[1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0.],
       [0., 0., 1., 0., 0.],
       [0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 1.]])

Аналоги range, сразу возвращающие массив numpy.array:

In [73]:
np.arange(1, 10)
Out[73]:
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
In [74]:
np.arange(1, 10, 2)
Out[74]:
array([1, 3, 5, 7, 9])
In [75]:
np.arange(1, 10, 0.5)
Out[75]:
array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. ,
       7.5, 8. , 8.5, 9. , 9.5])
In [76]:
np.linspace(0, 1, 5, endpoint=True)
Out[76]:
array([0.  , 0.25, 0.5 , 0.75, 1.  ])

В чем разница?

Алгебраические операции

— Какие храбрые поступки вы совершали в своей жизни?

— Однажды на алгебре я руку поднял.

sci-calc.png

Базовая арифметика

In [77]:
data = np.array([[1, 2],
                 [3, 4],
                 [5, 6]], dtype=np.int8)
In [78]:
data + 2
Out[78]:
array([[3, 4],
       [5, 6],
       [7, 8]], dtype=int8)
In [79]:
data - 5
Out[79]:
array([[-4, -3],
       [-2, -1],
       [ 0,  1]], dtype=int8)
In [80]:
1 - data
Out[80]:
array([[ 0, -1],
       [-2, -3],
       [-4, -5]], dtype=int8)
In [81]:
data * 2
Out[81]:
array([[ 2,  4],
       [ 6,  8],
       [10, 12]], dtype=int8)
In [82]:
data / 3
Out[82]:
array([[0.33333333, 0.66666667],
       [1.        , 1.33333333],
       [1.66666667, 2.        ]])
In [83]:
data ** 2
Out[83]:
array([[ 1,  4],
       [ 9, 16],
       [25, 36]], dtype=int8)
In [84]:
2 ** data
Out[84]:
array([[ 2,  4],
       [ 8, 16],
       [32, 64]], dtype=int8)

То же самое функциями

In [85]:
np.add(data, 2)
Out[85]:
array([[3, 4],
       [5, 6],
       [7, 8]], dtype=int8)
In [86]:
np.subtract(data, 5)
Out[86]:
array([[-4, -3],
       [-2, -1],
       [ 0,  1]], dtype=int8)
In [87]:
np.subtract(1, data)
Out[87]:
array([[ 0, -1],
       [-2, -3],
       [-4, -5]], dtype=int8)
In [88]:
np.multiply(data, 2)
Out[88]:
array([[ 2,  4],
       [ 6,  8],
       [10, 12]], dtype=int8)
In [89]:
np.divide(data, 3)
Out[89]:
array([[0.33333333, 0.66666667],
       [1.        , 1.33333333],
       [1.66666667, 2.        ]])
In [90]:
np.power(data, 2)
Out[90]:
array([[ 1,  4],
       [ 9, 16],
       [25, 36]], dtype=int8)
In [91]:
np.power(2, data)
Out[91]:
array([[ 2,  4],
       [ 8, 16],
       [32, 64]], dtype=int8)

А если складывать не с числом?

Как вы думаете, что произойдет, если попытаться сложить 2 массива?

In [92]:
data = np.array([1, 2])
ones = np.ones(2)
In [93]:
data + ones
Out[93]:
array([2., 3.])

vis-numpy-5.jpg

С другими операциями это тоже работает:

In [94]:
data - ones
Out[94]:
array([0., 1.])
In [95]:
data * data
Out[95]:
array([1, 4])
In [96]:
data / data
Out[96]:
array([1., 1.])

vis-numpy-6.jpg

А что получится с многомерными массивами?

In [97]:
data = np.array([[1, 2],
                 [3, 4]], dtype=int)
ones = np.ones(shape=(2, 2))

Всё так же!

In [98]:
data + ones
Out[98]:
array([[2., 3.],
       [4., 5.]])

vis-numpy-12.jpg

Можно складывать и разные по размерностям массивы.

In [99]:
data = np.array([[1, 2],
                 [3, 4],
                 [5, 6]])
ones_row = np.ones(2)
In [100]:
data + ones_row
Out[100]:
array([[2., 3.],
       [4., 5.],
       [6., 7.]])

vis-numpy-13.jpg

In [101]:
data.shape
Out[101]:
(3, 2)
In [102]:
data + np.ones(2)
Out[102]:
array([[2., 3.],
       [4., 5.],
       [6., 7.]])
In [103]:
data + np.ones(3)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-103-59adb053f41f> in <module>
----> 1 data + np.ones(3)

ValueError: operands could not be broadcast together with shapes (3,2) (3,) 
In [104]:
data + np.ones(shape=(3, 1))
Out[104]:
array([[2., 3.],
       [4., 5.],
       [6., 7.]])

karina.jpg

Почему для 2D-массивов 1D-массив прибавляется по axis=1, но не прибавляется по axis=0?

Еще раз; как идут измерения:

dimensions.png

Как понять, можно ли произвести бинарную операцию (типа сложения) для пары массивов?

Посмотрим на пример:

In [105]:
data = np.arange(30).reshape(3, -1)
data
Out[105]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
In [106]:
adder = np.arange(data.shape[1])
adder
Out[106]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [107]:
print(f"data.shape  = {data.shape}")
print(f"adder.shape = {adder.shape}")
data.shape  = (3, 10)
adder.shape = (10,)
In [108]:
data + adder
Out[108]:
array([[ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18],
       [10, 12, 14, 16, 18, 20, 22, 24, 26, 28],
       [20, 22, 24, 26, 28, 30, 32, 34, 36, 38]])
In [109]:
data
Out[109]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
In [110]:
adder = np.arange(data.shape[0])
adder
Out[110]:
array([0, 1, 2])
In [111]:
print(f"data.shape  = {data.shape}")
print(f"adder.shape = {adder.shape}")
data.shape  = (3, 10)
adder.shape = (3,)
In [112]:
data + adder
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-112-e23945759bf2> in <module>
----> 1 data + adder

ValueError: operands could not be broadcast together with shapes (3,10) (3,) 
In [113]:
data
Out[113]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
In [114]:
adder = np.arange(data.shape[0]).reshape(-1, 1)
adder
Out[114]:
array([[0],
       [1],
       [2]])
In [115]:
print(f"data.shape  = {data.shape}")
print(f"adder.shape = {adder.shape}")
data.shape  = (3, 10)
adder.shape = (3, 1)
In [116]:
data + adder
Out[116]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
       [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
       [22, 23, 24, 25, 26, 27, 28, 29, 30, 31]])

На самом деле существуют специальное правило приведения размерностей:

  1. Предположим, что a.shape = (a_1, a_2, ..., a_n) и b.shape = (b_1, b_2, ..., b_n). Над a и b можно произвести поэлементую бинарную операцию, если $\forall \; i \in (1...n)$ выполнено хотя бы одно из условий:
    • a_i == b_i;
    • a_i == 1;
    • b_i == 1.
  1. Если размерности не совпадают, то к массиву меньшей размерности добавляются ведущие фиктивные размерности.

Документация: https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html

(из материалов Техносферы@Mail.Ru)

Проверить явное приведение размерности можно через функции np.broadcast_to и np.broadcast_arrays.

Такие же правила применяются к вычитанию.

In [117]:
data = np.array([1, 3])
ones = np.ones(2)

data - ones
Out[117]:
array([0., 2.])
In [118]:
data = np.array([[1, 2],
                 [3, 4]])
ones = np.ones(shape=(2, 2))

data - ones
Out[118]:
array([[0., 1.],
       [2., 3.]])

А как будет происходить умножение?

In [119]:
data1 = np.array([[1, 2],
                  [3, 4]])

data2 = np.array([[0, 10],
                  [20, -1]])
In [120]:
data1 * data2
Out[120]:
array([[ 0, 20],
       [60, -4]])

Но что делать, если нам нужно матричное умножение?

In [121]:
data = np.array([1, 2, 3])
In [122]:
powers_of_ten = (10 ** np.arange(6)).reshape(3, 2)
powers_of_ten
Out[122]:
array([[     1,     10],
       [   100,   1000],
       [ 10000, 100000]])
In [123]:
data.dot(powers_of_ten)
Out[123]:
array([ 30201, 302010])

vis-numpy-14.jpg

Для тех, кто забыл, что такое матричное умножение:

vis-numpy-14.jpg

vis-numpy-15.jpg

А теперь перейдем от базовой арифметики к чуть более cложным функциям.

Подождите, это все была базовая арифметика?

In [124]:
data = np.array([[1, 2, 3],
                 [4, 5, 8]])
In [125]:
np.sqrt(data)
Out[125]:
array([[1.        , 1.41421356, 1.73205081],
       [2.        , 2.23606798, 2.82842712]])
In [126]:
np.exp(data)
Out[126]:
array([[2.71828183e+00, 7.38905610e+00, 2.00855369e+01],
       [5.45981500e+01, 1.48413159e+02, 2.98095799e+03]])
In [127]:
np.log(data)
Out[127]:
array([[0.        , 0.69314718, 1.09861229],
       [1.38629436, 1.60943791, 2.07944154]])
In [128]:
np.log2(data)
Out[128]:
array([[0.        , 1.        , 1.5849625 ],
       [2.        , 2.32192809, 3.        ]])

Агрегирующие операции

In [129]:
np.random.seed(777)
a = np.random.randint(0, 6, size=(8,))
a[3] = 10
a
Out[129]:
array([ 3,  1,  5, 10,  1,  2,  0,  2])
In [130]:
a.min(), a.max(), a.argmax(), a.sum(), a.prod(), a.mean()
Out[130]:
(0, 10, 3, 24, 0, 3.0)
In [131]:
np.min(a), np.max(a), np.argmax(a), np.sum(a), np.prod(a), np.mean(a)
Out[131]:
(0, 10, 3, 24, 0, 3.0)

vis-numpy-17.jpg

Старайтесь не использовать с массивами numpy.array нативные функции min, max и sum.

Какая у этого причина?

In [132]:
a = np.random.randint(-2, 8, size=(6,))
max(a), a
Out[132]:
(6, array([ 3,  6, -1,  5,  5,  0]))
In [133]:
a = np.random.randint(-2, 8, size=(4, 3))
max(a), a
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-133-5c91eed52270> in <module>
      1 a = np.random.randint(-2, 8, size=(4, 3))
----> 2 max(a), a

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

Почему агрегирующие операции?

Просто потому что они выполняют агрегацию (редукцию) по определенной оси!

Разберем, что делает numpy.array.???(axis=axis) – агрегирующая операция вдоль оси axis:

  • выполняет редукцию (агрегирующую операцию) по оси axis;
  • удаляет ось axis из исходного массива.

Что все это значит? Как это применять?

Разберем пример:

In [134]:
data = np.array([[1, 2],
                 [5, 3],
                 [4, 6]], dtype=int)
In [135]:
data.max(axis=0)
Out[135]:
array([5, 6])
In [136]:
data.max(axis=1)
Out[136]:
array([2, 5, 6])

vis-numpy-18.jpg

In [137]:
np.random.seed(777)

data = np.random.randint(0, 10, size=(3, 7))
data[1, 3] = 15

data
Out[137]:
array([[ 7,  6,  7,  1,  7,  4,  7],
       [ 9,  8,  7, 15,  0,  1,  2],
       [ 4,  5,  7,  1,  7,  2,  2]])
In [138]:
data.argmax()
Out[138]:
10
In [139]:
data.ravel()[data.argmax()]
Out[139]:
15
In [140]:
i = np.argmax(data)
i, j = i // data.shape[1], i % data.shape[1]

print(f"value = {data[i, j]}")
print(f"index = {(i, j)}")
value = 15
index = (1, 3)
In [141]:
np.unravel_index(np.argmax(data), data.shape)
Out[141]:
(1, 3)

Логические операции

In [142]:
data = np.arange(6)
data
Out[142]:
array([0, 1, 2, 3, 4, 5])
In [143]:
data = data > 2
data
Out[143]:
array([False, False, False,  True,  True,  True])
In [144]:
data.any(), np.any(data), any(data)
Out[144]:
(True, True, True)
In [145]:
data.all(), np.all(data), all(data)
Out[145]:
(False, False, False)
In [146]:
~data
Out[146]:
array([ True,  True,  True, False, False, False])

Логические операции в многомерных массивах

In [147]:
data = np.arange(12).reshape(3, 4) >= 3
data
Out[147]:
array([[False, False, False,  True],
       [ True,  True,  True,  True],
       [ True,  True,  True,  True]])
In [148]:
data.all()
Out[148]:
False
In [149]:
data.all(axis=0), data.all(axis=1)
Out[149]:
(array([False, False, False,  True]), array([False,  True,  True]))
In [150]:
all(data)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-150-4eb86e975eae> in <module>
----> 1 all(data)

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

Бинарные логические операции

In [151]:
a = np.array([1, 1, 0, 0, 1], dtype=bool)
b = np.array([1, 0, 0, 1, 0], dtype=bool)
In [152]:
a and b
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-152-61df3bd186ad> in <module>
----> 1 a and b

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
In [153]:
a & b
Out[153]:
array([ True, False, False, False, False])
In [154]:
a | b
Out[154]:
array([ True,  True, False,  True,  True])
In [155]:
a ^ b
Out[155]:
array([False,  True, False,  True,  True])

Индексация numpy.array

Общее правило: массив[первый индекс:последний индекс:шаг].

Значения по умолчанию:

  • первый индекс = 0;
  • последний индекс = len(массив);
  • шаг = 1;
In [156]:
data = np.array([1, 2, 3], dtype=np.int8)
In [157]:
print(f"data[0]   = {data[0]}")
print(f"data[1]   = {data[1]}")
print(f"data[0:2] = {data[0:2]}")
print(f"data[1:]  = {data[1:]}")
data[0]   = 1
data[1]   = 2
data[0:2] = [1 2]
data[1:]  = [2 3]

vis-numpy-8.jpg

Для многомерных массивов индексация немного отличается от default Python.

Вспомним, как индексируются обычные вложенные списки?

In [158]:
data = [[        10,          12],
        [        28, "Oh, fish!"]]
data[1][1]
Out[158]:
'Oh, fish!'
In [159]:
data = np.array(data)
data[1, 1]
Out[159]:
'Oh, fish!'
In [160]:
data = np.array([[1, 2],
                 [3, 4],
                 [5, 6]])
In [162]:
data[0,1], data[1,0]
Out[162]:
(2, 3)
In [163]:
data[1:3]
Out[163]:
array([[3, 4],
       [5, 6]])
In [164]:
data[0:2,0]
Out[164]:
array([1, 3])

vis-numpy-16.jpg

Булева индексация

In [165]:
data = np.arange(10)
ind = np.array([1, 0, 0, 1, 0, 1, 1, 1, 1, 0], dtype=bool)
data, ind
Out[165]:
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
 array([ True, False, False,  True, False,  True,  True,  True,  True,
        False]))
In [166]:
data[ind]
Out[166]:
array([0, 3, 5, 6, 7, 8])

Возможно, это одна из самых крутых "фишек" NumPy!

Почему? Чем это полезно?

Можно очень легко делать фильтры! Например:

In [167]:
np.random.seed(777)

a = np.random.randint(-2, 7, 34)
a
Out[167]:
array([ 5,  4,  5, -1,  5,  2,  5,  6,  5,  0, -2, -1,  0,  2,  3,  5, -1,
        5,  0,  0,  5,  2,  6,  4,  0,  5,  6, -2,  6,  1,  0, -2,  1,  1])

Пусть мы хотим найти все отрицательные элементы.

In [168]:
a < 0
Out[168]:
array([False, False, False,  True, False, False, False, False, False,
       False,  True,  True, False, False, False, False,  True, False,
       False, False, False, False, False, False, False, False, False,
        True, False, False, False,  True, False, False])
In [169]:
a[a < 0]
Out[169]:
array([-1, -2, -1, -1, -2, -2])

Найдем все элементы, кратные 3 (сравнимые с нулем по модулю 3):

In [170]:
a
Out[170]:
array([ 5,  4,  5, -1,  5,  2,  5,  6,  5,  0, -2, -1,  0,  2,  3,  5, -1,
        5,  0,  0,  5,  2,  6,  4,  0,  5,  6, -2,  6,  1,  0, -2,  1,  1])
In [171]:
a[a % 3 == 0]
Out[171]:
array([6, 0, 0, 3, 0, 0, 6, 0, 6, 6, 0])
In [172]:
a[np.logical_not(a % 3)]
Out[172]:
array([6, 0, 0, 3, 0, 0, 6, 0, 6, 6, 0])
In [173]:
a[~((a % 3).astype(bool))]
Out[173]:
array([6, 0, 0, 3, 0, 0, 6, 0, 6, 6, 0])

Найдем все элементы, кратные 3 или 5:

In [174]:
a
Out[174]:
array([ 5,  4,  5, -1,  5,  2,  5,  6,  5,  0, -2, -1,  0,  2,  3,  5, -1,
        5,  0,  0,  5,  2,  6,  4,  0,  5,  6, -2,  6,  1,  0, -2,  1,  1])
In [175]:
del35 = (a % 3 == 0) | (a % 5 == 0)
del35
Out[175]:
array([ True, False,  True, False,  True, False,  True,  True,  True,
        True, False, False,  True, False,  True,  True, False,  True,
        True,  True,  True, False,  True, False,  True,  True,  True,
       False,  True, False,  True, False, False, False])
In [176]:
a[del35]
Out[176]:
array([5, 5, 5, 5, 6, 5, 0, 0, 3, 5, 5, 0, 0, 5, 6, 0, 5, 6, 6, 0])

Функция np.where позволит преобразовать булев массив в индексы.

In [177]:
np.random.seed(777)

a = np.random.randint(-10, 10, size=(3, 5))
a
Out[177]:
array([[ -3,   5,  -4,   7,  -3],
       [ -3,   4,  -3,   8,   3],
       [  4, -10,  -9,   8,  -5]])
In [178]:
bool_array = a > 0
bool_array
Out[178]:
array([[False,  True, False,  True, False],
       [False,  True, False,  True,  True],
       [ True, False, False,  True, False]])
In [179]:
where = np.where(bool_array)
where
Out[179]:
(array([0, 0, 1, 1, 1, 2, 2]), array([1, 3, 1, 3, 4, 0, 3]))

Другие операции с массивами numpy.array

Генерация случайных чисел

In [180]:
np.random.seed(777)
In [181]:
np.random.rand(10)
Out[181]:
array([0.15266373, 0.30235661, 0.06203641, 0.45986034, 0.83525338,
       0.92699705, 0.72698898, 0.76849622, 0.26920507, 0.64402929])
In [182]:
np.random.randint(0, 10, 10)
Out[182]:
array([1, 2, 4, 5, 7, 1, 7, 2, 2, 7])
In [183]:
np.random.permutation(10)
Out[183]:
array([1, 5, 0, 3, 9, 7, 2, 6, 8, 4])
In [184]:
np.random.choice(10, size=10)
Out[184]:
array([8, 3, 2, 0, 3, 3, 4, 0, 6, 5])

Объединение массивов

In [185]:
np.random.seed(777)

a = np.random.randint(1, 10, 12).reshape(4, 3)
a
Out[185]:
array([[8, 7, 8],
       [2, 8, 5],
       [8, 9, 8],
       [3, 1, 2]])
In [186]:
b = np.random.randint(-5, 0, 9).reshape(3, 3)
b
Out[186]:
array([[-3, -1, -4],
       [-2, -3, -3],
       [-3, -1, -5]])
In [187]:
np.concatenate((a, b))
Out[187]:
array([[ 8,  7,  8],
       [ 2,  8,  5],
       [ 8,  9,  8],
       [ 3,  1,  2],
       [-3, -1, -4],
       [-2, -3, -3],
       [-3, -1, -5]])

А как склеить по другой оси?

In [188]:
a.T
Out[188]:
array([[8, 2, 8, 3],
       [7, 8, 9, 1],
       [8, 5, 8, 2]])
In [189]:
b
Out[189]:
array([[-3, -1, -4],
       [-2, -3, -3],
       [-3, -1, -5]])
In [190]:
np.concatenate((a.T, b), axis=1)
Out[190]:
array([[ 8,  2,  8,  3, -3, -1, -4],
       [ 7,  8,  9,  1, -2, -3, -3],
       [ 8,  5,  8,  2, -3, -1, -5]])
In [191]:
np.hstack((a.T, b))  # concatenate with axis=1
Out[191]:
array([[ 8,  2,  8,  3, -3, -1, -4],
       [ 7,  8,  9,  1, -2, -3, -3],
       [ 8,  5,  8,  2, -3, -1, -5]])
In [192]:
np.vstack((a, b))  # concatenate with axis=0
Out[192]:
array([[ 8,  7,  8],
       [ 2,  8,  5],
       [ 8,  9,  8],
       [ 3,  1,  2],
       [-3, -1, -4],
       [-2, -3, -3],
       [-3, -1, -5]])

Сортировка массивов

In [193]:
np.random.seed(777)

data = np.random.randint(1, 20, 10)
data
Out[193]:
array([ 8, 16,  7, 18,  8,  8, 15,  8, 19, 14])
In [194]:
np.sort(data), data
Out[194]:
(array([ 7,  8,  8,  8,  8, 14, 15, 16, 18, 19]),
 array([ 8, 16,  7, 18,  8,  8, 15,  8, 19, 14]))
In [195]:
data.sort(), data
Out[195]:
(None, array([ 7,  8,  8,  8,  8, 14, 15, 16, 18, 19]))

Сортировка индексов

In [196]:
data = np.random.randint(1, 20, 10)
data
Out[196]:
array([15,  1,  2, 19,  6,  8,  2,  8, 11, 19])
In [197]:
np.argsort(data)
Out[197]:
array([1, 2, 6, 4, 5, 7, 8, 0, 3, 9])
In [198]:
data[np.argsort(data)]
Out[198]:
array([ 1,  2,  2,  6,  8,  8, 11, 15, 19, 19])

Подождите, в NumPy можно индексировать по массиву индексов?

Да, это называется "Fancy indexing".

Fancy indexing

In [199]:
np.random.seed(1337)

a = np.random.randint(-5, 5, size=(5, 5))
a
Out[199]:
array([[ 2,  3,  2,  4,  2],
       [-3, -3, -1,  3,  4],
       [ 1,  1,  2,  3, -4],
       [ 1,  1, -3, -3,  4],
       [ 3, -4,  2, -2, -4]])

Как получить 2, 4 и 3 строки?

In [200]:
a[[2, 4, 3]]
Out[200]:
array([[ 1,  1,  2,  3, -4],
       [ 3, -4,  2, -2, -4],
       [ 1,  1, -3, -3,  4]])
In [201]:
a
Out[201]:
array([[ 2,  3,  2,  4,  2],
       [-3, -3, -1,  3,  4],
       [ 1,  1,  2,  3, -4],
       [ 1,  1, -3, -3,  4],
       [ 3, -4,  2, -2, -4]])

Как получить элементы в строках 2, 4, 3 и в 0 и последнем столбце?

In [202]:
a[[2, 4, 3], [0, -1]]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-202-9ed845fbe954> in <module>
----> 1 a[[2, 4, 3], [0, -1]]

IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,) 
In [203]:
a[[2, 4, 3], [0, -1, 2]]
Out[203]:
array([ 1, -4, -3])
In [204]:
a[[2, 4, 3]][:,[0, -1]]
Out[204]:
array([[ 1, -4],
       [ 3, -4],
       [ 1,  4]])

Fancy indexing может применяться и так:

In [205]:
a
Out[205]:
array([[ 2,  3,  2,  4,  2],
       [-3, -3, -1,  3,  4],
       [ 1,  1,  2,  3, -4],
       [ 1,  1, -3, -3,  4],
       [ 3, -4,  2, -2, -4]])
In [206]:
where = np.where(a >= 0)
where
Out[206]:
(array([0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4]),
 array([0, 1, 2, 3, 4, 3, 4, 0, 1, 2, 3, 0, 1, 4, 0, 2]))
In [207]:
a[where]
Out[207]:
array([2, 3, 2, 4, 2, 3, 4, 1, 1, 2, 3, 1, 1, 4, 3, 2])

Можно сделать еще и так:

In [208]:
a
Out[208]:
array([[ 2,  3,  2,  4,  2],
       [-3, -3, -1,  3,  4],
       [ 1,  1,  2,  3, -4],
       [ 1,  1, -3, -3,  4],
       [ 3, -4,  2, -2, -4]])
In [209]:
took = a.take([2, 4, 3], axis=1)
took
Out[209]:
array([[ 2,  2,  4],
       [-1,  4,  3],
       [ 2, -4,  3],
       [-3,  4, -3],
       [ 2, -4, -2]])
In [210]:
a[0,4] = 100
took
Out[210]:
array([[ 2,  2,  4],
       [-1,  4,  3],
       [ 2, -4,  3],
       [-3,  4, -3],
       [ 2, -4, -2]])

Последняя тема:

Отличие view от copy

view-copy.png

Что есть что?

view - разные объекты ссылаются на одно и то же пространство в памяти.

copy - пространство в памяти от "старого" объекта было скопировано, "новый" объект ссылается на копию.

Как отличить одно от другого?

In [211]:
data = np.random.randint(-5, 5, size=(2, 5))
data
Out[211]:
array([[-2,  4, -2, -1,  3],
       [ 3,  2,  4, -1, -4]])
In [212]:
v = data[:,1:4]
v
Out[212]:
array([[ 4, -2, -1],
       [ 2,  4, -1]])
In [213]:
v.base is data
Out[213]:
True

v - это view*

*не vendetta.

In [214]:
data
Out[214]:
array([[-2,  4, -2, -1,  3],
       [ 3,  2,  4, -1, -4]])
In [215]:
c = data[:,[1, 2, 3]]
c
Out[215]:
array([[ 4, -2, -1],
       [ 2,  4, -1]])
In [216]:
c.base is data
Out[216]:
False

c - это copy.

Чем отличаются view и copy?

  1. При изменении view изменяется и исходный массив.
  2. Создание copy занимает обычно в ~2 раза больше времени (чем больше массив, тем хуже).
  3. view имеет ту же базу (numpy.array.base), что и исходный массив; copy имеет в качестве базы другой объект.
  4. view расположен в том же участке памяти, что и исходный массив, copy же занимает дополнительную память.

Соответственно, если вам нужен и исходный массив и его измененная версия, то вам нужно использовать copy.

Если же ваш массив занимает очень много памяти или если вам нужно создать очень много срезов массива, то следует использовать view.

В большинстве остальных случаев нет существенной разницы.

Когда вы получите view, а когда - copy?

View Copy
Slices Indexing, e.g. a[0,:] Fancy indexing, e.g. a[[0],:]
Changing dtype - a.as_type(np.float32)
Converting to 1D array a.ravel() a.flatten()

Вы всегда получите копию, если скопируете объект :^)

In [217]:
unambiguous_copy = data.copy()
unambiguous_copy.base is data
Out[217]:
False

На этом всё!

Полную документацию NumPy смотрите на сайте:

https://docs.scipy.org/doc/numpy

В NumPy есть еще множество потенциально полезных для вас функций, так что не поленитесь зайти и посмотреть :^)

folks.gif