Ниже обзор возможностей и приемов работы с последовательностями в 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 s | True, если в s есть элемент, равный x, иначе False | да |
x not in s | False, если в 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.
Комментариев нет:
Отправить комментарий