Питон для нормальных. Учебник Москва Базальт спо макс пресс 2018
Скачать 2.54 Mb.
|
Её первые два аргумента те же, что и у arange: начало и конец диапазона, а третий аргумент — количество элементов в диапазоне. К сожалению, по умолча- нию эти функции по разному обрабатывают второй аргумент: arange берёт его невключительно, linspace — включительно. Это можно поправить с помощью 116 Глава 5. Массивы. Модуль numpy A[:, 0] A[2:, 2:5] A[0, 3:] Рис. 5.2. Примеры срезов массивов: чёрным — срезы, в результате которых из двумерного массива получаются одномерные (из столбца и строки исходного мас- сива соответственно); тёмносерым — срез, в результате которого получается дву- мерный массив меньшего размера. опционного, четвёртого аргумента linspace, если установить его в False. Вот пример задания одинаковых массивов с помощью arange и linspace: >>> a = numpy . arange (0 , 2 , 0.25) >>> p r i n t ( a ) [ 0. 0.25 0.5 0.75 1. 1.25 1.5 1.75] >>> b = numpy . l i n s p a c e (0 , 2 , 8 , False ) >>> p r i n t ( a ) [ 0. 0.25 0.5 0.75 1. 1.25 1.5 1.75] Массивы построены на базе списков и сохраняют некоторые их черты. В част- ности, массивы можно обходить циклом любым из изложенных выше способов, а функция len возвращает длину массива. Важным свойством массивов является то, что над ними можно производить операции как над простыми числами, при этом операция, в нашем случае вычисление синуса, будет произведена с каждым элементом массива. Модуль numpy дублирует и расширяет функционал модуля math, он содержит все основные тригонометрические, логарифмические и прочие функции, а также константы, поэтому при работе с numpy надобности в math обычно не возникает. Одна из важнейших особенностей массивов — возможность делать срезы (см. рис. 5.2). Срез представляет собою массив из части элементов исходного массива, 5.1. Создание и индексация массивов 117 взятых по некоторому простому правилу, например, каждый второй или первые десять. Вот небольшая программа, иллюстрирующая создание срезов массивов: f r o m numpy i m p o r t * a1 = arange (0 , 4 , 0.5) p r i n t ( a1 ) a2 = a1 [0:4] p r i n t ( a2 ) a3 = a1 [0: l e n ( a1 ):2] p r i n t ( a3 ) Массив a2 состоит из первых четырёх (от нулевого включительно до чет- вёртого невключительно) элементов массива a1, массив a3 — из всех чётных элементов a1 (элементы от нулевого до последнего с шагом 2). Для обозначения индексации используются двоеточия, сначала ставится номер начального индек- са, потом конечного, потом шаг, все они — целые числа (точно в том же порядке, как и в функциях range и arange). Если шаг и стоящее перед ним двоеточие опустить, шаг будет автоматически единичным. Вот вывод программы: [ 0. 0.5 1. 1.5 2. 2.5 3. 3.5] [ 0. 0.5 1. 1.5] [ 0. 1. 2. 3.] Нужно отметить, что массив-срез делит с исходным массивом память. Это значит, что копирование элементов в новый массив не происходит, вместо этого просто делается сложная ссылка. Поэтому изменение элементов исходного мас- сива приводит к изменению элементов массива-среза и наоборот. Чтобы скопи- ровать элементы, нужно воспользоваться функцией copy. Здесь у внимательных читателей должен возникнуть эффект d´ej`a vu, потому что нечто подобное уже было в разделе про списки. f r o m numpy i m p o r t * a1 = arange (0 , 0.6 , 0.1) a2 = a1 [: l e n ( a1 )/2] p r i n t ( a1 , a2 ) a1 [0] = 10 a1 [ -1] = a1 [ -1] + 3 p r i n t ( a1 , a2 ) a2 [1] = 0 p r i n t ( a1 , a2 ) Вывод программы: [0. 0.1 0.2 0.3 0.4 0.5] [ 0. 0.1 0.2] [10. 0.1 0.2 0.3 0.4 3.5] [10. 0.1 0.2] [10. 0. 0.2 0.3 0.4 3.5] [10. 0. 0.2] 118 Глава 5. Массивы. Модуль numpy В приведённом примере мы поменяли нулевой и последний элементы массива a1, в результате в массиве a2 нулевой элемент тоже изменился, затем мы поменяли первый элемент a2, и первый элемент a1 также изменился. Теперь тот же пример, но с использованием функции copy: f r o m numpy i m p o r t * a1 = arange (0 , 0.6 , 0.1) a2 = copy ( a1 [: l e n ( a1 )/2]) p r i n t ( a1 , a2 ) a1 [0] = 10 a1 [ -1] = a1 [ -1] + 3 p r i n t ( a1 , a2 ) a2 [1] = 0 p r i n t ( a1 , a2 ) Теперь в выводе видно, что изменение одного из массивов не приводит к изме- нению другого: [0. 0.1 0.2 0.3 0.4 0.5] [0. 0.1 0.2] [10. 0.1 0.2 0.3 0.4 3.5] [0. 0.1 0.2] [10. 0.1 0.2 0.3 0.4 3.5] [0. 0. 0.2] В приведённых примерах было использовано ещё одно свойство индексации: если номер начального элемента опустить, то автоматически подставляется ноль, если опустить номер конечного — длина массива. Функция array() не единственная функция для создания массивов. Обычно элементы массива вначале неизвестны, а массив, в котором они будут хранить- ся, уже нужен. Поэтому имеется несколько функций для того, чтобы создавать массивы с каким-то исходным содержимым (по умолчанию тип элементов созда- ваемого массива — float64): ones создаёт массив из единиц, zeros — массив из нулей, empty — массив, значения элементов которого могут быть какими угод- но (берётся то, что лежало в ячейках памяти до того, как в них был размещён массив). Все эти функции принимают единственный параметр — длину вновь создаваемого массива. Вот пример их работы: f r o m numpy i m p o r t * a1 = ones (5) a2 = zeros (3) a3 = empty (4) p r i n t ( a1 ) p r i n t ( a2 ) p r i n t ( a3 ) Вывод: [1. 1. 1. 1. 1.] [0. 0. 0.] [4.39718425e-322 0.00000000e+000 0.00000000e+000 2.84236875e-316] 5.1. Создание и индексация массивов 119 Видно, что в a3 помещены 4 произвольных числа. В нашем случае они получи- лись равными или близкими к нулю, но рассчитывать на это в общем случае не приходится. Можно вручную задать тип данных: a3 = empty (5 , dtype = i n t ) p r i n t ( a3 ) Вывод: [500 0 0 0 0] В данном примере как раз хорошо видно, что нулевой элемент массива далеко не равен нулю. Если массив слишком большой, чтобы его печатать, numpy автоматически скрывает центральную часть массива и выводит только его уголки: A = arange (0 , 3000 , 1) p r i n t ( A ) Вывод: [ 0 1 2 ..., 2997 2998 2999] Если вам действительно нужно увидеть весь массив, используйте функцию numpy.set_printoptions : A = arange (0 , 3000 , 1) s e t _ p r i n t o p t i o n s ( t h r e s h o l d = nan ) p r i n t ( A ) И вообще, с помощью этой функции можно настроить печать массивов «под се- бя». Функция numpy.set_printoptions принимает несколько аргументов? опи- сание которых при необходимости можно с лёгкостью найти в официальной до- кументации к numpy в интернете. Действительные числа из numpy типа float64 обладают практически всеми теми же свойствами, что и встроенные числа Python, но могут принимать три дополнительных специальных значения: inf, -inf или nan, что можно перевести как бесконечность, минус бесконечность и неопределённость. Если поделить дей- ствительное число типа numpy.float64 на 0, получится inf -inf в зависимости от его знака, а вместо ошибки будет выдано только предупреждение, которое позволит продолжить вычисления. Программа: a = np . array ([2. , 0. , 5.]) f o r el i n a : p r i n t (1./ el ) выдаст: 120 Глава 5. Массивы. Модуль numpy 0.5 /usr/bin/ipython3:2: RuntimeWarning: divide by zero encountered in double_scalars inf 0.2 5.2 Арифметические операции и функции с массивами Python — объектно-ориентированный язык программирования. Каждая пе- ременная Python — объект, хотя часто это совсем незаметно. Поэтому многие переменные Python имеют встроенные методы — присущие им функции — и свойства. Так, массивы имеют ряд очень простых, но полезных методов, сильно упрощающих жизнь при написании сложных программ. Собственно, именно по- этому в сложных программах удобнее использовать именно массивы, а не списки. Самые часто используемые занесены в таблицу 5.1. Пусть у нас есть массив вида a=numpy.array([0.1, 0.25, -1.75, 0.4, -0.9]) : Таблица 5.1. Некоторые методы массивов Метод Описание a.sum() Сумма элементов массива: len(a) X i=0 a i = 0.1 + 0.25 − 1.75 + 0.4 − 0.9 = −1.9 a.mean() Среднее элементов массива: a = 1 len(a) len(a) X i=0 a i = (0.1 + 0.25 − 1.75 + 0.4 − 0.9)/5 = −0.38 a.min() Минимальный элемент массива: −1.75 a.min() Максимальный элемент массива: 0.4 a.var() Дисперсия элементов массива: σ 2 a = 1 len(a) len(a) X i=0 (a i − a) = ((0.1 + 0.38) 2 + (0.25 + 0.38) 2 (−1.78 + 0.38) 2 + (0.4 + 0.38) 2 + (−0.9 + 0.38) 2 )/5 = 0.6766 a.std() Среднеквадратичное отклонение элементов массива от среднего: σ a = 0.822556988907 Вот небольшая программа, показывающая, как эти функции работают: f r o m numpy i m p o r t * a = array ([0.1 , 0.25 , -1.75 , 0.4 , -0.9]) p r i n t ( a . s u m ()) p r i n t ( a . mean ()) p r i n t ( a . m i n ()) 5.2. Арифметические операции и функции с массивами 121 p r i n t ( a . m a x ()) p r i n t ( a . var ()) p r i n t ( a . std ()) Вывод программы: -1.9 -0.38 -1.75 0.4 0.6766 0.822556988907 Обратите внимание на круглые скобки после имени каждой функции — они указывают, что вызывается и исполняется соответствующая функция. Если скоб- ки забыть, никаких вычислений не будет произведено, а вместо их результа- тов будут напечатаны строки документации соответствующих функций. То есть a.min() — это результат вычисления, минимальный элемент массива a, но a.min — это сам метод вычисления: p r i n t ( a . m i n ) Вывод: Поскольку Python язык очень глубоко объектный, сами по себе методы, как и их параметры и результаты, тоже являются объектами. Тип результата встро- енных функций определяется их природою: min, max и sum всегда того же типа, что и элементы массива, а вот var, mean и std всегда действительнозначные, поскольку для их вычисления выполняются деление и — для std — извлечение квадратного корня. Библиотека numpy унаследовала от Fortran ряд особенностей работы с мас- сивами, не присущих C, Pascal, Java, C# и прочим распространённым языкам общего назначения: массивы numpy можно складывать, как простые числа, при- бавлять к ним число, умножать и делить друг на друга и на число. При этом все операции происходят поэлементно. Вот пример программы сложения двух массивов и умножения массива на число: f r o m numpy i m p o r t * a = array ([0.1 , 0.25 , -1.75 , 0.4 , -0.9]) b = array ([0.9 , 1.75 , -0.15 , 0.4 , 0.4]) c = a + b p r i n t ( c ) d = a + 1.5 p r i n t ( d ) Программа выведет: 122 Глава 5. Массивы. Модуль numpy [ 1. 2. -1.9 0.8 -0.5] [ 1.6 1.75 -0.25 1.9 0.6 ] Такие операции называются иногда «векторными» в том смысле, что оди- наковые действия производятся сразу с большим набором данных. Складывать можно только массивы одинаковой длины. Аналогично можно перемножать мас- сивы. Число можно прибавлять к любому массиву или умножать массив на чис- ло, в результате получается массив того же размера, что и исходный, а каждый его элемент есть результат сложения (или умножения) соответствующего элемен- та исходного массива и числа. Такой синтаксис операций позволяет существенно упростить запись сложных программ, избежав множества циклов, которые при- шлось бы писать на С или Pascal. В векторных операциях можно использовать как целые массивы, так и их срезы: f r o m numpy i m p o r t * a = array ([0.1 , 0.25 , -1.75 , 0.4 , -0.9]) b = array ([0.9 , 1.75 , -0.15 , 0.4 , 0.4]) c = a [:3] + b [1:4] p r i n t ( c ) d = a [2:4]* a [0:2] p r i n t ( d ) Вот вывод программы: [ 1.85 0.1 -1.35] [-0.175 0.1 ] Обратите внимание ещё раз: при перемножении массивов действия производятся поэлементно, в результате чего получается новый массив той же длины, а вовсе не их векторное произведение, как это принято в некоторых математических пакетах. Вот пример: >>> i m p o r t numpy >>> numpy . array ([ -1.75 , 0.4]) * numpy . array ([0.1 , 0.25]) array ([ -0.175 , 0.1 ]) в то время как в результате скалярного произведения должно было получиться: -1.75*0.1 + 0.4*0.25 = -0.075 Кроме арифметических операций над массивами можно также векторно вы- полнять все заложенные в numpy функции, например, взять синус или логарифм от массива (если написать просто log(X), то получится натуральный логарифм, иначе нужно написать основание логарифма вторым аргументом log(X, 10)). Вот пример такой программы: f r o m numpy i m p o r t * t = arange (0 , pi , pi /6) x = sin ( t ) 5.2. Арифметические операции и функции с массивами 123 y = log ( t ) p r i n t ( t ) p r i n t ( x ) p r i n t ( y ) Вывод: [0. 0.52359878 1.04719755 1.57079633 2.0943951 2.61799388] [0. 0.5 0.8660254 1. 0.8660254 0.5] [-inf -0.64702958 0.0461176 0.45158271 0.73926478 0.96240833] Здесь мы протабулировали (вычислили значения функций при изменении ар- гумента в некоторых пределах) две функции: sin(t) и ln(t) на полуинтервале [0; π) с шагом π/6. Как видим, можно проделывать над массивами сколь угодно сложные векторные операции. Так как натуральный логарифм нуля равен минус бесконечности, то получился соответствующий ответ: -inf. Массивы numpy можно обходить циклом, как списки. Чтобы сделать наше табулирование более удобным и привычным для чтения, модифицируем преды- дущую программу, чтобы она выдавала результат своей работы в три столбца. Для этого заменим последнюю строку, где стоят операторы print, на две новые: f o r i i n r a n g e ( l e n ( t )): p r i n t ( t [ i ] , x [ i ] , y [ i ]) Вывод программы: 0.0 0.0 -inf 0.523598775598 0.5 -0.647029583379 1.0471975512 0.866025403784 0.0461175971813 1.57079632679 1.0 0.451582705289 2.09439510239 0.866025403784 0.739264777741 2.61799387799 0.5 0.962408329055 Полученные столбцы чисел выглядят понятно, но не очень презентабельно, поскольку каждая запись выглядит по-своему — все они имеют разное число знаков после точки. Для получения стройных колонок можно воспользоваться встроенным методом форматирования, поменяв последнюю строку приведённой программы на следующую: p r i n t ( " {:12.8 f }\ t {:12.8 f }\ t {:12.8 f } " . f o r m a t ( t [ i ] , x [ i ] , y [ i ])) Здесь символ ’\t’ означает табуляцию, впереди идёт строка форматирования, где :12.8f означает, что на это место будет подставлено действительное чис- ло (на это указывает f) с 12 знаками, из них после десятичной точки 8. Три подставляемых числа взяты в скобки. Вывод программы: 124 Глава 5. Массивы. Модуль numpy 0.00000000 0.00000000 -inf 0.52359878 0.50000000 -0.64702958 1.04719755 0.86602540 0.04611760 1.57079633 1.00000000 0.45158271 2.09439510 0.86602540 0.73926478 2.61799388 0.50000000 0.96240833 В Python есть специальные операторы +=, -=, которые увеличивают и умень- шают одно число на другое, а также операторы *= и /=, которые умножают и делят одно число на другое, например: >>> a = 10 >>> a += 4 >>> p r i n t ( a ) 14 >>> a -= 8 >>> p r i n t ( a ) 6 По сути эти инструкции аналогичны инструкциям типа a = a + b или a = a - b , где b — число, на которое нужно увеличить или уменьшить a. Преимущество использования операторов +=, -= кроме краткости записи состоит в том, что при их использовании новый объект не создаётся, а только модифицируется исход- ный, в то время как запись a = a + b или a = a - b приводит к уничтожению старого объекта с именем a (его должен будет удалить из памяти сборщик мусо- ра) и созданию нового с тем же именем. Поэтому использование операторов +=, -= наиболее актуально при работе со сложными типами вроде массивов numpy, так как на их создание и размещение в памяти, а также удаление тратится го- раздо больше ресурсов. Вот пример: >>> x = numpy . array ([0. , 2. , 0.5 , -1.7 , 3.]) >>> x array ([ 0. , 2. , 0.5 , -1.7 , 3. ]) >>> x += 1 >>> p r i n t ( x ) [ 1. 3. 1.5 -0.7 4. ] >>> x -= 2.5 >>> p r i n t ( x ) [ -1.5 0.5 -1. -3.2 1.5] >>> x += numpy . l i n s p a c e (0 , 4 , 5) >>> p r i n t ( x ) [ -1.5 1.5 1. -0.2 5.5] Как видим, прибавлять и вычитать из массивов таким образом можно как числа, так и другие массивы (нужно, чтобы совпал размер). Использование операторов +=, -= в ряде случаев позволяет избежать некото- рых сложно уловимых ошибок с именами переменных и, иногда, с их типами. 5.3. Двумерные массивы, форма массивов 125 Скажем, нужно увеличить переменную со сложным именем, а затем использо- вать её в дальнейших вычислениях. Вот пример, когда переменная со сложным названием komputilo_de_nombro_de_eventoj используется в качестве счётчика событий (нужно посчитать число положительных элементов массива x, описан- ного ранее): >>> k o m p u t i l o _ d e _ n o m b r o _ d e _ e v e n t o j = 0 >>> f o r ela i n x : i f ela > 0: k o m p u t i l o _ d e _ n p m b r o j _ d e _ e v e n t o j = \ k o m p u t i l o _ d e _ n o m b r o _ d e _ e v e n t o j + 1 >>> p r i n t ( k o m p u t i l o _ d e _ n o m b r o _ d e _ e v e n t o j ) 0 Программа отработала без сообщений об ошибках, но результат получил- ся неверный: 0 вместо 3. Всё дело в том, что мы ошиблись в имени пере- менной в строке внутри условного оператора. В результате на каждом ша- ге цикла, когда условие оказалось истинно, создавалась новая переменная komputilo_de_npmbroj_de_eventoj |