"Удивительный Python", или отличительные особенности и возможности языка программирования Python

Серия заметок о специфических особенностях языка программирования Python, делающих его непохожим на другие языки и позволяющих выполнять более широкий спектр задач, или делать это более легким способом.

Генераторы списков в Python

В языке программирования Python существует специальная синтаксическая конструкция, позволяющая создавать заполненные списки по определенным правилам. Создаваемые списки могут быть разными, содержание конструкции немного отличаться, поэтому такие конструкции называют генераторами списков. Их удобство заключается в более короткой записи, чем если создавать список обычным способом.

Например, надо создать список, заполненный натуральными числами до определенного числа. "Классический" способ будет выглядеть так:

>>> a = []
>>> for i in range(1,15):
...     a.append(i)
... 
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

Создание списка заняло у нас три строчки кода. Генератор же списка сделает это за одну:

>>> a = [i for i in range(1,15)]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

В данном случае конструкция [i for i in range(1,15)] является генератором списка. Вся конструкция заключается в квадратные скобки, что как бы говорит, что будет создан список. Внутри квадратных скобок можно выделить три части: 1) что делаем с элементом (в данном случае ничего не делаем, просто добавляем в список), 2) что берем (в данном случае элемент i), 3) откуда берем (здесь из объекта range). Части отделены друг от друга ключевыми словами for и in.

Рассмотрим такой пример:

>>> a = [2,-2,4,-4,7,5]
>>> b = [i**2 for i in a]
>>> b
[4, 4, 16, 16, 49, 25]

В данном случае в генераторе списка берется каждый элемент из списка a и возводится в квадрат. Таким образом, 1) что делаем - возводим элемент в квадрат, 2) что берем - элемент, 3) откуда берем - из списка a.

>>> a = {1:10, 2:20, 3:30}
>>> b = [i*a[i] for i in a]
>>> b
[10, 40, 90]

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

>>> a = {1:10, 2:20, 3:30}
>>> b = [[i,a[i]] for i in a]
>>> b
[[1, 10], [2, 20], [3, 30]]
>>> c = [j for i in b for j in i]
>>> c
[1, 10, 2, 20, 3, 30]

В этом примере список b состоит из вложенных списков. Если бы в генераторе мы опустили квадратные скобки в выражении [i,a[i]], то произошла бы ошибка. Если все же надо получить одноуровневый список из ключей и значений словаря, надо взять каждый вложенный список и из него взять каждый элемент. Это достигается за счет вложенной конструкции for. "Классический" синтаксис для заполнения списка c выглядел бы так:

>>> c = []
>>> for i in b:
...     for j in i:
...             c.append(j)
... 
>>> c
[1, 10, 2, 20, 3, 30]

В генераторы списков в конце можно добавлять конструкцию if. Например, надо из строки извлечь все цифры:

>>> a = "lsj94ksd231 9"
>>> b = [int(i) for i in a if '0'<=i<='9']
>>> b
[9, 4, 2, 3, 1, 9]

Или заполнить список числами, кратными 30 или 31:

>>> a = [i for i in range(30,250) if i%30 == 0 or i%31 == 0]
>>> a
[30, 31, 60, 62, 90, 93, 120, 124, 150, 155, 180, 186, 210, 217, 240, 248]

Таким образом, генераторы списков позволяют создавать списки легче и быстрее. Однако заменить ими достаточно сложные конструкции не получится. Например, когда условие проверки должно включать ветку else.

Сортировка по произвольным элементам вложенных списков

В Python легко выполнить сортировку списка с помощью функции sort():

>>> a = [10,3,4,1,9]
>>> a.sort()
>>> a
[1, 3, 4, 9, 10]

Если элементы списка сами представляют собой списки, т. е. являются вложенными списками (подразумеваем матрицу), то сортировка будет происходить по первым элементам вложенных списков (по первому столбцу матрицы):

>>> a = [[12,101],[2,200],[18,99]]
>>> a.sort()
>>> a
[[2, 200], [12, 101], [18, 99]]

Но что делать, если надо отсортировать не по первому столбцу? На этот случай функция (а точнее метод) sort() принимает необязательный аргумент key, в котором передается другая функция. Этой другой функции передается очередной элемент списка. Она может сделать с ним что угодно и вернуть что угодно. По этому "что угодно" и происходит сортировка.

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

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

a = [['петя',10,130,35], ['вася',11,135,39],['женя',9,140,33],['дима',10,128,30]]
 
n = input('Сортировать по имени (1), возрасту (2), росту (3), весу (4): ')
n = int(n)-1
 
def sort_col(i):
    return i[n]
 
a.sort(key=sort_col)
 
for i in a:
    print("%7s %3d %4d %3d" % (i[0],i[1],i[2],i[3]))

Здесь пользователь вводит номер поля. Число приводится к типу integer, и из него вычитается единица (т.к. индексация списка с нуля).

Далее определяется функция sort_col(). Ей передается аргумент i, а она возвращает n-ый элемент этого аргумента. Так, если этой функции передать список, то она вернет его n-й элемент. В данном случае тот, который хотел пользователь.

В функции sort() указывается пользовательская функция. Когда sort() извлекает очередной элемент списка, который сортирует, то передает этой функции. Получается, что элемент списка подменяется на то, что возвращает пользовательская функция.

В данном случае если пользователь заказывает сортировку по второму столбцу, то вывод будет таким:

Сортировать по имени (1), возрасту (2), росту (3), весу (4): 2
   женя   9  140  33
   петя  10  130  35
   дима  10  128  30
   вася  11  135  39

Можно не определять пользовательскую функцию, а использовать lambda-функцию:

a = [['петя',10,130,35], ['вася',11,135,39],['женя',9,140,33],['дима',10,128,30]]
 
n = input('Сортировать по имени (1), возрасту (2), росту (3), весу (4): ')
n = int(n)-1
 
a.sort(key=lambda i: i[n])
 
for i in a:
    print("%7s %3d %4d %3d" % (i[0],i[1],i[2],i[3]))

Кроме того, метод sort() имеет еще один необязательный параметр по ключевому слову - reverse. По умолчанию он равен False. Это значит, что сортировка происходит по возрастанию. Однако если у reverse будет значение True, то сортировка будет обратной, т. е. по убыванию. В измененной программе ниже реализована возможность выбора типа сортировки:

a = [['петя',10,130,35], ['вася',11,135,39],['женя',9,140,33],['дима',10,128,30]]
 
n = input('Сортировать по имени (1), возрасту (2), росту (3), весу (4): ')
n = int(n)-1
t = input('По возрастанию (0), по убыванию (1): ')
t = int(t)
 
a.sort(key=lambda i: i[n], reverse=t)
 
for i in a:
    print("%7s %3d %4d %3d" % (i[0],i[1],i[2],i[3]))

При сортировки по весу по убыванию получим:

Сортировать по имени (1), возрасту (2), росту (3), весу (4): 4
По возрастанию (0), по убыванию (1): 1
   вася  11  135  39
   петя  10  130  35
   женя   9  140  33
   дима  10  128  30

Так ли неизменяем кортеж?

Кортеж — это один из типов данных языка программирования Python. Наряду со списками и строками, он относится к последовательностям (элементы можно извлекать по индексу, брать срезы). Отличается кортеж от списка тем, что он, как и строка, неизменяем (нельзя изменить, добавить, удалить отдельный элемент кортежа). От строк же кортеж отличается тем, что он, как и список, содержит отдельные элементы, часто разных типов.

Так если мы имеем список

>>> a = [1, 2]

то можем изменить его

>>> a[0] = 2 
>>> a 
[2, 2]

Если же мы имеем кортеж (определяется круглыми скобками)

>>> a = (1, 2)

то попытка его изменения приведет к ошибке:

>>> a[0] = 2 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: 'tuple' object does not support item assignment

Мы можем только получать значение элементов кортежа:

>>> a[0] 
1

Однако, если элементом кортежа является изменяемый объект. Например, список или словарь, то этот элемент можно изменять:

>>> a = (1, [2, 3], {'a': 10, 'b': 20}) 
>>> a[1][0] = 5 
>>> a[2]['c'] = 30 
>>> a 
(1, [5, 3], {'c': 30, 'b': 20, 'a': 10})

В данном примере кортеж содержит три элемента: число, список и словарь. Первый элемент мы изменить не можем, т. к. числа — неизменяемые объекты. А вот второй и третий элемент изменить можно. В примере первый индекс указывает на элемент кортежа, второй — на элемент списка или ключ словаря.

Почему же такое изменение кортежа возможно? Дело в том, что в кортеже сохраняются не сами значения элементов списка (или словаря и т. д.), а ссылка на список. Поэтому хотя в кортеже ссылку изменить нельзя (то есть нельзя поменять список на другой), но вот что там делается с объектом по этой ссылке кортеж «не волнует».

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

>>> a = [1, 2] 
>>> b = {1: 'i', 2: 'j'} 
>>> c = (a, b) 
>>> c 
([1, 2], {1: 'i', 2: 'j'}) 
>>> d = [3, 4, 5, 6] 
>>> c[0] = d 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: 'tuple' object does not support item assignment

Казалось бы, выход в данной ситуации — это присвоить переменной a:

>>> a = d
>>> c
([1, 2], {1: 'i', 2: 'j'})

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

>>> c[0].append(4)
>>> c
([1, 2, 4], {1: 'i', 2: 'j'})

Если бы переменная a не была «перезаписана», то есть не стала ссылаться на другой список, то кортеж можно было бы менять через нее:

>>> a = [1, 2]
>>> b = {1: 'i', 2: 'j'}
>>> c = (a, b)
>>> a[0] = 3
>>> c
([3, 2], {1: 'i', 2: 'j'})

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

Фильтрация последовательностей (встроенная функция filter())

В языке программирования Python есть встроенная функция filter(), которая принимает два параметра и возвращает объект-итератор. Первый аргумент этой функции - какая-либо другая функция, а второй - последовательность (к которым относятся строки, списки и кортежи), итератор или объект, поддерживающий итерацию. Далее мы ограничимся тем, что второй аргумент - всегда последовательность.

То, что возвращает filter(), есть итератор, состоящий из тех элементов последовательности, для которых переданная в качестве первого аргумента функция вернула "правду" (true).

В примере ниже создается функция func(), которая возвращает 1, если ей передан аргумент больше нуля, и 0 во всех остальных случаях. Когда эту функцию применяют для списка a, то получается объект-итератор из положительных элементов a.

>>> a = [1, -4, 6, 8, -10]
>>> def func(x):
...     if x > 0:
...             return 1
...     else:
...             return 0
... 
>>> b = filter(func, a)
>>> b = list(b)
>>> b
[1, 6, 8]

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

>>> b = filter(func, a)
>>> for i in b:
...     print(i)
... 
1
6
8
>>> for i in b:
...     print(i)
... 
>>> 

Как видно из примера, повторное обращение обнаруживает, что в объекте ничего уже нет.

Теперь посмотрим на такой вариант:

>>> a = [-1,0,1,0,0,1,0,-1]
>>> b = list(filter(None,a))
>>> b
[-1, 1, 1, -1]

Если вместо функции в качестве первого аргумента filter() передается значение None, то в отфильтрованном объекте окажутся те значения, которые сами по себе являются true. Аналогичный пример со строками:

>>> s = ['a','','d','cc',' ']
>>> ss = list(filter(None, s))
>>> ss
['a', 'd', 'cc', ' ']

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

Еще один пример - из строки фильтруются числа:

>>> def numbs(x):
...     if '0' <= x <= '9':
...             return 1
...     else:
...             return 0
... 
>>> s = "5a 3 k 99 d00"
>>> for i in filter(numbs,s):
...     print(i)
... 
5
3
9
9
0
0

Функции min и max в Python

В языке программирования Python есть встроенные функции поиска минимума и максимума. Им можно передавать как один объект (список или другой объект-последовательность или итерируемый объект), так и непосредственно множество однотипных объектов.

Если передается один список, то в нем находится минимум или максимум, который возвращается.

>>> a = [11,8,12,0] 
>>> min(a) 
0 
>>> max(a) 
12

Если передается несколько списков, то возвращается целый список. При этом сравнение происходит поэлементно: сначала сравниваются первые элементы списков. Если они не равны, то функция min вернет тот список, первый элемент которого меньше (max наоборот). Если первые элементы равны, то будут сравниваться вторые и т. д.

>>> a = [11,8,12,0] 
>>> b = [11,7,123,9] 
>>> m = min(a,b) 
>>> print(m, type(m)) 
[11, 7, 123, 9] <class 'list'> 
>>> c = [11,8,12] 
>>> min(a,c) 
[11, 8, 12]

Функциям min и max можно непосредственно передавать множество чисел:

>>> max(3,8,-3,12,9) 
12 

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

>>> s = ['a','d',1] 
>>> min(s) 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: unorderable types: int() < str()

Но:

>>> s = ['a','d','ee'] 
>>> min(s) 
'a'

В функциях min и max можно указать необязательный именной параметр key. Ему присваивается одноаргументная функция, которая выполняет какое-то предварительное действие над элементами, например, списка.

>>> a = [8,-11,4,2,-5] 
>>> max(a) 
8 
>>> max(a,key=abs) 
-11

Здесь во втором случае находится максимум среди абсолютных значений чисел. То есть к каждому элементу списка применяется функция abs. Однако применить ее к целым спискам нельзя:

>>> max(a,b,key=abs) 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: bad operand type for abs(): 'list'