View on GitHub

Methodics of Python Intro

Practical materials for learning Python

В данной главе мы рассмотрим отдельный класс объектов — последовательности. Также обсудим работу цикла for в контексте последовательностей.

Быстрый поиск


Операции над объектами как совокупность методов

Перед тем, как непосредственно начать разбираться с циклом for и последовательностями, необходимо немного забежать вперёд и разобраться, что из себя представляют объекты в Python. Все объекты в Python представляют некоторые экземпляры соответствующих классов-типов этих объектов. Как и у любого класса, у класса в Python есть атрибуты: поля — атрибуты-значения — и методы — атрибуты-функции.

>>> a = 123  
>>> dir(a)  
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']  
>>> a.real  
123  
>>>

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

Если говорить точнее, то операции являются синтаксическим сахаром над методами конкретных объектов или классов-типов объектов:

>>> a = 123  
>>> a+1  
124  
>>> a.__add__(1)  
124  
>>>
>>> int  
<class 'int'>  
>>> int.__add__(123, 1)  
124  
>>> (123).__add__(1)  
124

Наряду с операциями возникает понятие протокола — некоторого свода правил, которому должны удовлетворять объекты этого протокола. В Python протокол требует наличия у объектов реализации списка методов, совокупность которых описывает объекты протокола.

В качестве примера рассмотрим числовой протокол. Все встроенные типы Python, удовлетворяющие списку описанных в изначальном Си-протоколе методов, считаются с точки зрения утиной типизации Python числами. Как следствие, реализация в своих объектах числового протокола формально делает объекты «числами».

Аналогично числовому протоколу последовательности описываются своим протоколом последовательности. Однако, вернёмся к уточнению этого протокола мы дальше по главе ☺

Цикл for

Несмотря на привычное слово «цикл» в названии for, от классического формата циклов он довольно далеко. Этот оператор работает с последовательностями: в правой части стоит последовательность, в левой — имя ( или имена, если элементы последовательности — тоже последовательности), которое связывается с элементами последовательности. Имя последовательно связывается с элементами последовательности и внутри цикла выполняет над ними операции.

По окончании выполнения for связанное имя продолжает существовать и указывать на последний обработанный объект:

>>> for a in (1, 2, 4):  
...     print(a)
...
1  
2  
4  
>>>
>>> a  
4  
>>>

Вариант с именами:

>>> for a, b in [[1, 2], [3, 4]]:  
...     print(a, b)
...
1 2  
3 4  
>>>

Аналогично классическим циклам, for поддерживает работу break и continue, а также, аналогично while, имеет собственную клаузу else:

>>> for i in (1, 2, 3, 4, 5):  
...     if i == 3:
...         print("TREEE")
...         break
... else:  
...     print("No 3")
...
TREEE  
>>> for i in (1, 2, 12, 4, 5):  
...     if i == 3:
...         print("TREEE")
...         break
... else:  
...     print("No 3")
...
No 3  
>>>

Условие выполнения for, при котором срабатывает клауза else, временно назовём абстрактным «Последовательность кончилась». Подробнее этот аспект будет рассказан в будущих главах.

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

>>> for i in "QWER":  
...     print(i)
...
Q  
W  
E  
R  
>>> list("QWE")  
['Q', 'W', 'E']  
>>>

Та же логика пошагового выполнения операций над элементами последовательности лежит в специальных функциях множественного сравнения: all — конъюнкция логических выражений-элементов последовательности, any — дизъюнкция этих элементов.

>>> all((1, "qwe", [1, 2, 3]))  
True  
>>> all((1, "qwe", []))  
False  
>>> any((1, "qwe", []))  
True  
>>> any((0, "", []))  
False  
>>>

Последовательности

Что же отличает последовательности от других объектов Python? Протокол последовательности, вообще говоря, требует реализации лишь одного метода: __getitem__ — метода, реализующего оператор [].

Последовательности глобально делятся на константные (с реализацией только __getitem__; при модификации создаётся новый объект) и модифицируемые (для них добавляется ещё один метод — __setitem__; модификация не меняет объект).

>>> s = 1, 2, 3
>>> s  
(1, 2, 3)  
>>> id(s)  
140601713136512  
>>> s +=4, 5  
>>> s  
(1, 2, 3, 4, 5)  
>>> id(s)  
140601712995408  
>>>
>>> s = [1, 2, 3]  
>>> id(s)  
140601713134592  
>>> s += [4, 5]  
>>> s  
[1, 2, 3, 4, 5]  
>>> id(s)  
140601713134592  

При этом модификация может проводиться любыми объектами, операции над которыми возможны:

>>> s += "QWE"  
>>> s  
[1, 2, 3, 4, 5, 'Q', 'W', 'E']  
>>>

Непосредственно, операции над последовательностями

Над всеми последовательностями можно проводить различные логические и арифметические операции:

>>> s = [1, 2, 3]  
>>> 1 in s  
True  
>>> 'qwe' not in s  
True  
>>>
>>> s + [4, 5]  
[1, 2, 3, 4, 5]  
>>> s * 8  
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]  
>>> 8 * s  
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]  
>>>
>>> (8).__mul__(s)  
NotImplemented  
>>> s.__rmul__(8)  
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]  
>>>
>>> s[2]  
3  
>>>
>>> a = (12, 124, 12, 125, 12, 234, 46, 124, 347, 5, 8, 0)  
>>> a[1:3]  
(124, 12)  
>>> a[-5:-1:-1]  
()  
>>> a[-5:1:-1]  
(124, 46, 234, 12, 125, 12)  
>>> a[7:]  
(124, 347, 5, 8, 0)  
>>>

Здесь хочется обратить внимание на секцию вида [:], формально означающую взятие секции из всей последовательности. Для константных последовательностей получаемый объект будет тем же для отсутствия лишнего копирования, в то время как модифицируемая последовательность будет создаваться новой копией:

>>> s  
[1, 2, 3]  
>>> id(s), id(s[:])  
(140601713258560, 140601713211136)  
>>>
>>> a = s  
>>> b = s[:]  
>>> a is s  
True  
>>> b is s  
False  
>>> s = (1, 2, 3)  
>>> id(s), id(s[:])  
(140601713204096, 140601713204096)  
>>>

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

>>> s = "qwertyuiop"  
>>> s[1:3:4]  
'w'  
>>> s[1:3:2]  
'w'  
>>> s[1:30:2]  
'wryip'  
>>> s[5]  
'y'  
>>> type(s[5])  
<class 'str'>  
>>> s[5][0]  
'y'  
>>> s[5][0][0]  
'y'  
>>> s[5][0][0][0]  
'y'  
>>>

Последовательности внутри

Подробнее изучим, как работает __getitem__на примере собственного класса-последовательности:

>>> class C:  
...     def __getitem__(self, arg):
...         return arg
...
>>> c = C()  
>>> res = c[123:1:1000]  
>>> res  
slice(123, 1, 1000)  
>>>

Оказывается, для работы __getitem__ существует специальный объект slice, представляющий из себя структуру с тремя полями: start, stop и step. При этом значения этих полей не обязаны быть числовыми:

>>> res.start  
123  
>>> res.stop  
1  
>>> res.step  
1000  
>>> 
>>> res = c["QWA":"":[1,2, 3]]  
>>> res.start  
'QWA'  
>>> res.stop  
''  
>>> res.step  
[1, 2, 3]  
>>>

Секционирование, таким образом, представляет из себя правила вычисления элементов на основе параметров slice.

Списки в секционировании, сложность модификации

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

>>> s  
[1, 2, 3, 4, 5, 'Q', 'W', 'E']  

>>> s[3:8:2] = [666, 13, 228, 1337]  
Traceback (most recent call last):  
 File "<python-input-99>", line 1, in <module>
   s[3:8:2] = [666, 13, 228, 1337]
   ~^^^^^^^
ValueError: attempt to assign sequence of size 4 to extended slice of size 3  
>>> s[3:8:2] = [666, 13, 228]  
>>> s  
[1, 2, 3, 666, 5, 13, 'W', 228]  

>>> s[1:3] = ['q', 'q', 'q', 'q', 'q', 'q', ]  
>>> s  
[1, 'q', 'q', 'q', 'q', 'q', 'q', 666, 5, 13, 'W', 228]  
>>>

Хочется также обсудить сложность операций модификации списка. Список представляет из себя классический динамический массив Си (с оговорками, что работа с памятью не ложится на разработчика совсем). Его реализация подразумевает геометрическое масштабирование при заполнении с копированием всех элементов в новое место. Однако поскольку копирование происходит не чаще, чем раз в N операций (N — длина списка), а в остальное время добавление элементов выполняется за константное время, среднее время модификации конца списка становится константным. При этом модификация начала списка всегда занимает O(N). Решением этой проблемы является использование специальных структур, таких как deque или простая очередь.

Возможности списков

Список, как самостоятельный объект, обладает множеством методов, наделяющих его особыми свойствами.

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

>>> a = [[1, 2, 3],[4, 5, 6]]  
>>> a  
[[1, 2, 3], [4, 5, 6]]  
>>> a[0][2]  
3  
>>>
>>> a  
[[1, 2, 3], [4, 5, 6]]  
>>> a[0]  
[1, 2, 3]  
>>> id(a[0])  
140601713143040  
>>> a[0].append(100500)  
>>> a  
[[1, 2, 3, 100500], [4, 5, 6]]  
>>> a[0]  
[1, 2, 3, 100500]  
>>> id(a[0])  
140601713143040  
>>>

Какие же отличительные функции можно делать со списками:

>>> dir(a)  
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__','__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__','__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
>>> a  
[100500, 2, 3]  
>>> b = a.copy  
>>> b  
<built-in method copy of list object at 0x7f54cfdb6dc0>  
>>> b = a.copy()  
>>> b  
[100500, 2, 3]  
>>> b.sort()  
>>> b  
[2, 3, 100500]  
>>> a  
[100500, 2, 3]  
>>>

* Неглубокое копирование подразумевает копирование ссылок на объекты в новый список, но не клонирование объектов:

>>> a = [1, 2, 3]  
>>> b = [a, [123], a]  
>>> b  
[[1, 2, 3], [123], [1, 2, 3]]  
>>> b[0][0] = 100500  
>>> b  
[[100500, 2, 3], [123], [100500, 2, 3]]  
>>> b[0] is b[0]  
True  
>>> bb = b.copy()  
>>> bb  
[[100500, 2, 3], [123], [100500, 2, 3]]  
>>> b[0] is bb[0]  
True  
>>>
>>> a  
[100500, 2, 3]  
>>> id(a)  
140601713139904  
>>> a.reverse()  
>>> a  
[3, 2, 100500]  
>>> id(a)  
140601713139904  
>>>

* Для сортировки и разворота последовательностей существуют и внешние функции, но они работают своеобразно (как — будет рассказано в будущих главах):

>>> sorted(a)  
[2, 3, 100500]  
>>> reversed(a)  
<list_reverseiterator object at 0x7f54d0042bc0>  
>>> list(reversed(a))  
[3, 2, 100500]  
>>>
>>> 0 <'r'  
Traceback (most recent call last):  
 File "<python-input-143>", line 1, in <module>
   0 <'r'
TypeError: '<' not supported between instances of 'int' and 'str'  
>>> a == 'r'  
False  
>>> a, b = [1,"wer"], [3, 4]  
>>> a < b  
True  
>>> a, b = ["wer", 1], [3, 4]  
>>> a < b  
Traceback (most recent call last):  
 File "<python-input-148>", line 1, in <module>
   a < b
TypeError: '<' not supported between instances of 'str' and 'int'  
>>>

Дополнительные особенности последовательностей

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

>>> lst = [2*a + 1 for a in (1, 2, 3, 5)]  
>>> lst  
[3, 5, 7, 11]  
>>> [2*a + 1 for a in (1, 2, 3, 4, 5, 6, 7, 8, 9) if a//2 != 3]  
[3, 5, 7, 9, 11, 17, 19]  
>>> [2*a + 1 for a in (1, 2, 3, 4, 5, 6, 7, 8, 9)]  
[3, 5, 7, 9, 11, 13, 15, 17, 19]  
>>> [a * b for a in [2, 3, 4] for b in "QWE"]  
['QQ', 'WW', 'EE', 'QQQ', 'WWW', 'EEE', 'QQQQ', 'WWWW', 'EEEE']  
>>>

Такая методика «генерации» последовательности наводит на размышления о том, что последовательность, вообще говоря, не обязана храниться. Для её определения необходимо лишь задать __getitem__, который, вообще говоря, может выступать в качестве функции динамического расчёта последовательности. Такие последовательности называются вычислимыми. Ярким представителем вычислимых последовательностей является range, действия над которым по сути представляют алгебру над структурой, схожей со slice:

>>> range(10)  
range(0, 10)  
>>> type(range(10))  
<class 'range'>  
>>> range(10)[4]  
4  
>>> range(1, 20, 3)  
range(1, 20, 3)  
>>> list(range(1, 20, 3))  
[1, 4, 7, 10, 13, 16, 19]  
>>> range(1, 20, 3)[1:7:2]  
range(4, 22, 6)  
>>>

Ещё один пример вычислимой последовательности — enumerate. Эта последовательность вычисляется на основе переданной ей в качестве аргумента и возвращает пары вида «Индекс — значение», что резко улучшает вид кода и упрощает его:

>>> for i in range(len(s := "qwertyuiolk,mnbgfd")):  
...     print(i, s[i])
...
0 q  
1 w  
2 e  
3 r  
4 t  
5 y  
6 u  
7 i  
8 o  
9 l  
10 k  
11 ,  
12 m  
13 n  
14 b  
15 g  
16 f  
17 d  
>>>
>>> for i, k in enumerate("qwertyuiolk,mnbgfd"):  
...     print(i, k)
...
0 q  
1 w  
2 e  
3 r  
4 t  
5 y  
6 u  
7 i  
8 o  
9 l  
10 k  
11 ,  
12 m  
13 n  
14 b  
15 g  
16 f  
17 d  
>>>