суббота, 14 апреля 2018 г.

Работа с последовательностями в Python 3

Ниже обзор возможностей и приемов работы с последовательностями в Python 3, включая следующие темы (но не ограничиваясь ими):

  • задание последовательностей,
  • доступ к элементам последовательности для чтения и изменения,
  • методы, общие для последовательностей,
  • преобразования последовательностей,
  • агрегирование элементов последовательности.

Последовательности конструируются явно, с помощью литерала или другой последовательности, или аналитически, с помощью итератора или генератора. Примеры явно заданных последовательностей:

[1, 13, 42, -7, 5]         # список
(1, 2, 3, 5)               # кортеж
'привет'                   # строка
b'hello'                   # строка байтов
bytearray([100, 101, 102]) # массив байтов

Список (list) и массив байтов (bytearray) - изменяемые последовательности, кортеж (tuple), строка (str) и строка байтов (bytes) - неизменяемые.

Примеры последовательностей, построенных с помощью итератора, предоставленного функцией range():

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> tuple(range(10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
>>> bytes(range(10))
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t'
>>> str(bytes(range(65, 91)), encoding='ascii')
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

Функция range() также позволяет задать шаг, в том числе отрицательный:

>>> list(range(1, 10, 2))
[1, 3, 5, 7, 9]
>>> list(range(10, 1, -2))
[10, 8, 6, 4, 2]

Генератор - это объект класса generator, который Python автоматически создает при вызове функции c предложением yield внутри:

>>> def down(n):
...     while n > 0:
...         yield n
...         n -= 1
...
>>> type(down)
<class 'function'>
>>> down10 = down(10)
>>> type(down10)
<class 'generator'>

Каждый генератор - это итератор, так что можно инициализировать последовательность с его помощью:

>>> list(down10)
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> bytes(down(127))
b'\x7f~}|{zyxwvutsrqponmlkjihgfedcba`_^]\\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)(\'&%$#"! \x1f\x1e\x1d\x1c\
x1b\x1a\x19\x18\x17\x16\x15\x14\x13\x12\x11\x10\x0f\x0e\r\x0c\x0b\n\t\x08\x07\x06\x05\x04\x03\x02\x01'

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

  • индексирование от начала последовательности начинается с 0 и заканчивается числом, равным длине последовательности минус один, а
  • индексирование с конца начинается с -1 и заканчивается отрицательным числом, равным длине последовательности.
>>> a = [1, 5, 13]
>>> a[0]
1
>>> a[1] = 3
>>> a
[1, 3, 13]
>>> a[2]
13
>>> a[-1]
13
>>> a[-2] = 5
>>> a
[1, 5, 13]
>>> a[-3]
1

Замечательной возможностью является срез (slice), позволяющий из исходной последовательности получить подпоследовательность, задаваемую начальным и конечным индексами и (опционально) шагом. В сущности, срез - это фильтр по индексу, порождающий из исходной последовательности новую:

>>> a
[1, 5, 13]
>>> a[1:2]    # c 1-го по 2-й элемент, не включая последний
[5]
>>> a[1:]     # c 1-го до последнего элемента
[5, 13]
>>> a[:2]     # c нулевого по 2-й элемент, не включая последний
[1, 5]
>>> a[::-1]   # отрицательный шаг работает от конца к началу
[13, 5, 1]

В последнем примере последовательность обращается. Обращение последовательности нагляднее всего продемонстрировать на строке:

>>> 'привет python'[::-1]
'nohtyp тевирп'

Срез без ограничений с двух сторон включает все элементы исходной последовательности и создает ее копию:

>>> a[:]
[1, 5, 13]

Само собой, срез позволяет получить копию только изменяемой последовательности, а в случае с неизменяемой последовательностью возвращает ее саму:

>>> a is a[:]
False
>>> s = 'привет'
>>> s is s[:]
True
>>> b = b'hello'
>>> b is b[:]
True

Если срез последовательности используется слева от знака присваивания, то семантика совсем другая: вместо создания новой последовательности выполняется замена элементов среза на значение справа от знака присваивания:

>>> a
[1, 5, 13]
>>> a[:1] = [5, 6]
>>> a
[5, 6, 5, 13]
>>> a[:] = [-1]
>>> a
[-1]

При использовании среза с шагом, каждому значению среза должно соответствовать значение справа от знака присваивания, иначе возникает ошибка:

>>> a[:] = [1, 2, 3, 4]
>>> a[::2]
[1, 3]
>>> a[::2] = [-1, -1]
>>> a
[-1, 2, -1, 4]
>>> a[::-1] = range(4)
>>> a
[3, 2, 1, 0]
>>> a[::-1] = -1
Traceback (most recent call last):
  File "", line 1, in 
TypeError: must assign iterable to extended slice

Следующая таблица представляет операции и методы, общие для всех последовательностей Python:

ОперацияОписаниеРаботает с range?
x in sTrue, если в s есть элемент, равный x, иначе Falseда
x not in sFalse, если в s нет элемента, равного x, иначе Trueда
s + tконкатенация s и t
s * n или n * sконкатенация s с собой n раз
s[i]i-ый элемент s, считая с 0да
s[i:j]срез s от i до jда
s[i:j:k]срез s от i до j с шагом kда
len(s)длина sда
min(s)наименьший элемент sда
max(s)наибольший элемент sда
s.index(x[, i[, j]])индекс первого вхождения x в s (начиная с i и заканчивая j)да
s.count(x)всего вхождений x в sда

Операции in и not in для строк и строк байтов способны проверить вхождение не только отдельных элементов, но и подпоследовательностей из нескольких элементов:

>>> 'o' in 'hello'
True
>>> 'hell' in 'hello'
True
>>> 'bye' not in 'hello'
True
>>> b'o' in b'hello'
True
>>> b'hell' in b'hello'
True
>>> b'bye' in b'hello'
False

Для других последовательностей проверятся вхождение ровно одного элемента:

>>> a
[3, 2, 1, 0]
>>> [3, 2] in a
False
>>> 3 in a
True
>>> 2 in a
True
>>> [3, 2] in [[3, 2], [1, 0]]
True

Конкатенация создает новую последовательность, содержащую элементы исходных последовательностей:

>>> 'hello' + ' python!'
'hello python!'
>>> [-1, 0] + [1, True, None]
[-1, 0, 1, True, None]
>>> [-1, 0] * 5
[-1, 0, -1, 0, -1, 0, -1, 0, -1, 0]
>>> 5 * [-1, 0]
[-1, 0, -1, 0, -1, 0, -1, 0, -1, 0]

Тип range можно рассматривать как неизменяемую последовательность с некоторыми ограничениями на общие операции. Так, нельзя сложить (конкатенировать) два объекта range или умножить объект range на целое число, зато объект range можно индексировать, срезать, проверять вхождение в него значений.

>>> range(10)[5]
5
>>> range(10)[5:]
range(5, 10)
>>> range(10)[::-1]
range(9, -1, -1)
>>> 7 in range(10)
True
>>> 10 in range(10)
False
>>> max(range(10))
9
>>> min(range(10))
0
>>> len(range(10))
10
>>> range(10).count(5)
1
>>> range(10).index(5)
5

Если срез, или slice - это фильтр по индексам, то встроенная функция filter() позволяет отфильтровать элементы последовательности по их значениям с помощью заданной функции (обычно лямбда):

>>> a
[3, 2, 1, 0]
>>> filter(lambda x: x > 2, a)
<filter object at 0x000001C405351860>
>>> list(filter(lambda x: x > 1, a))
[3, 2]

Встроенная функция map() позволяет получить новую последовательность из исходной путем замены каждого элемента на значение, вычисленное с помощью заданной функции (обычно лямбда):

>>> map(lambda x: x**2, a)
<map object at 0x000001C405351860>
>>> list(map(lambda x: x**2, a))
[9, 4, 1, 0]

Композиция filter() и map() позволяет и отфильтровать элементы по значению и получить новые значения из исходных:

>>> a
[3, 2, 1, 0]
>>> list(map(lambda x: x**2, filter(lambda x: x > 2, a)))
[9]
>>> list(filter(lambda x: x > 2, map(lambda x: x**2, a)))
[9, 4]

Конструкция list comprehension (как это по-русски?) может работать как filter(), map() или их комбинация:

>>> # filter
>>> [x for x in a if x > 2]
[3]
>>> # map
>>> [x**2 for x in a]
[9, 4, 1, 0]
>>> # filter then map
>>> [x**2 for x in a if x > 2]
[9]
>>> # map then filter
>>> [x for x in [x**2 for x in a] if x > 2]
[9, 4]

А следующий фрагмент демонстрирует, как с помощью list comprehension и метода count() найти повторяющиеся элементы в последовательности:

>>> s = 'qwertyq'
>>> [x for x in s if s.count(x) > 1]
['q', 'q']

List comprehension поддерживает вложенность как циклов, так и условий:

>>> b = [
...     [0, 1, 2],
...     [3, 4, 5]
... ]
>>> [y for x in b for y in x]
[0, 1, 2, 3, 4, 5]

Последнее предложение эквивалентно следующему фрагменту:

>>> a = []
>>> for x in b:
...     for y in x:
...         a.append(y)
...
>>> a
[0, 1, 2, 3, 4, 5]

Вложенные условия в list comprehension эквивалентны составному условию с оператором and или вложенным if внутри цикла:

>>> [x for x in a if x > 0 if x < 5]
[1, 2, 3, 4]
>>> [x for x in a if x > 0 and x < 5]
[1, 2, 3, 4]
>>> c = []
>>> for x in a:
...     if x > 0:
...         if x < 5:
...             c.append(x)
...
>>> c
[1, 2, 3, 4]

Если list comprehension немедленно порождает список (объект класса list), то генераторное выражение, заключенное, в отличие от list comprehension, в обычные скобки ( и ), порождает генератор, который будет возвращать элементы последовательности, когда они понадобятся:

>>> a
[0, 1, 2, 3, 4, 5]
>>> g = (x*x for x in a if x%2 == 0)
>>> type(g)

>>> list(g)
[0, 4, 16]

От list comprehension генераторное выражение отличается только ленивым предоставлением элементов последовательности, в остальном поддерживая синтаксис и семантику list comprehension. Для передачи генераторного выражения в качестве аргумента функции достаточно одной пары скобок:

>>> max(x*x for x in a if x%2 == 0)
16
>>> bytes(x*x for x in a if x%2 == 0)
b'\x00\x04\x10'
>>> list(x*x for x in a if x%2 == 0)
[0, 4, 16]

Встроенные функции max(), min() и sum() возвращают максимальное, минимальное значение и сумму элементов последовательности, соответственно. Вместо готовой последовательности эти функции принимают также итерируемые объекты:

>>> a
[0, 1, 2, 3, 4, 5]
>>> max(a)
5
>>> min(a)
0
>>> min('qwerty')
'e'
>>> max('qwerty')
'y'
>>> sum(a)
15
>>> sum(x*x for x in a if x%2 == 0)
20

Что если нужно найти не сумму, а произведение элементов? Или сумму их квадратов? С этим нам поможет функция reduce() из модуля functools:

>>> a = a[1:]
>>> a
[1, 2, 3, 4, 5]

>>> from functools import reduce
>>> reduce(lambda x, y: x*y, a)
120

Здесь первый параметр x лямбда-функции есть аккумулятор, в котором накапливается результат вычисления, а второй параметр y - каждый следующий элемент последовательности. Перед началом вычисления аккумулятору присваивается значение первого элемента. Если же необходимо использовать другое начальное значение, то оно передается третьим аргументом:

>>> reduce(lambda x, y: x+y*2, 'qwerty', '')
'qqwweerrttyy'

Мы удвоили каждую букву в слове, но без третьего аргумента этого бы сделать не удалось:

>>> reduce(lambda x, y: x+y*2, 'qwerty')
'qwweerrttyy'

Теперь посчитаем сумму квадратов элементов списка a, инициализировав аккумулятор нулем:

>>> reduce(lambda x, y: x+y**2, a, 0)
55

Еще две встроенные функции, сводящие последовательность к единственному значению, - это any() и all(), возвращающие булевы значения.

>>> a
[1, 2, 3, 4, 5]
>>> any(a)
True
>>> all(a)
True
>>> any(x-1 for x in a)
True
>>> all(x-1 for x in a)
False

Функция any() возвращает True, если хотя бы один из элементов последовательности оценивается как True, иначе - возвращает False. Функция all() возвращает True, если все элементы последовательности оцениваются как True, иначе - False. С помощью этих функций и генераторного выражения легко проверить, удовлетворяют ли элементы последовательности некоторому условию:

>>> any(x>5 for x in a)
False
>>> all(x<=5 for x in a)
True

Циклы, list comprehension и генераторное выражение позволяют обходить все элементы последовательности. А для параллельного обхода нескольких последовательностей - двух и более - Python предлагает функцию zip():

>>> a
[1, 2, 3, 4, 5]
>>> b = 'hello'
>>> c = range(-5, 0)
>>> for x, y, z in zip(a, b, c):
...     print(x, y, z)
...
1 h -5
2 e -4
3 l -3
4 l -2
5 o -1

Функция zip() завершает работу по концу самой короткой из последовательностей:

>>> b = 'bye'
>>> for x, y, z in zip(a, b, c):
...     print(x, y, z)
...
1 b -5
2 y -4
3 e -3

Если необходимо дойти до конца самой длинной из последовательностей, то нужно воспользоваться функцией zip_longest() из модуля itertools:

>>> from itertools import zip_longest
>>> for x, y, z in zip_longest(a, b, c):
...     print(x, y, z)
...
1 b -5
2 y -4
3 e -3
4 None -2
5 None -1

Вместо None на месте отсутствующих элементов можно получить значение, заданное с помощью именованного параметра fillvalue:

>>> for x, y, z in zip_longest(a, b, c, fillvalue='*'):
...     print(x, y, z)
...
1 b -5
2 y -4
3 e -3
4 * -2
5 * -1

В завершение обзора, приведу операции и методы, общие для изменяемых последовательностей, то есть, для list и bytearray:

ОперацияОписание
s[i] = xзамена i-го элемента s на x
del s[i]удаление i-го элемента из s
s[i:j] = tзамена среза s от i до j на содержимое t
del s[i:j]то же, что и s[i:j] = []
s[i:j:k] = tзамена элементов s[i:j:k] на элементы t
del s[i:j:k]удаление элементов s[i:j:k] из s
s.append(x)добавление x в конец последовательности (эквивалентно s[len(s):len(s)] = [x])
s.clear()удаление всех элементов из s (эквивалентно del s[:])
s.copy()создание поверхностной копии s (эквивалентно s[:])
s.extend(t)
или
s += t
расширяет s содержимым t
s *= nобновляет s его содержимым, повторенным n раз
s.insert(i, x)вставляет x в s по индексу i (эквивалентно s[i:i] = [x])
s.pop([i])извлекает элемент с индексом i и удаляет его из s
s.remove(x)удаляет первое вхождение x в s
s.reverse()меняет порядок элементов в s на обратный

Часть перечисленных операций и методов были продемонстрированы в действии, другие ждут ваших экспериментов с ними.

Это был (неисчерпывающий) обзор возможностей и приемов работы с последовательностями в Python 3.

Комментариев нет:

Отправить комментарий