​​🗓 Календарь

Когда под рукой нет календаря, но есть Python:

import calendar; calendar.TextCalendar().pryear(2019)

Или из командной строки:

python -c 'import calendar; calendar.TextCalendar().pryear(2019)'
Календарь

Хотите по-русски (если вдруг еще не)?

import locale
locale.setlocale(locale.LC_ALL, 'ru_RU')
import calendar 
calendar.TextCalendar().pryear(2019)

А еще можно узнать, високосный ли год:

>>> calendar.isleap(2019)
False
>>> calendar.isleap(2020)
True

Или какой сегодня день недели?

>>> calendar.day_name[calendar.weekday(2019, 2, 19)]
'вторник'

Больше функций календаря ищите в документации к модулю calendar.

Специально для канала @pyway.

👀 global и nonlocal

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

def foo():
    print('x is', x)

x = 5
foo()  # напечает x is 5

Однако если в функции есть присваиваниие x после использования переменной x, то возникнет ошибка:

def foo():
    print('x is', x)
    x = 10

x = 5
foo()  # UnboundLocalError: local variable 'x' referenced before assignment

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

x = …
x += …, x -= … и т.п.
• for x in …:
• with … as x:

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

def foo():
    global x  # <-- тут
    print('x is', x)
    x = 10
    print('x is now', x)

x = 5
foo()  # ошибок не будет

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

def make_inc():  # внешняя ф-ция
    total = 0     # счетчик
    def helper():  # внутр. ф-ция 
        total += 1  # тут присваивание переменной
        return total 
    return helper  

f = make_inc()

print(f())  # UnboundLocalError: local variable 'total' referenced before assignment

Тут нужно другое ключевое слово – nonlocal, которое укажет, что нужно искать переменную во внешней области видимости. Такой пример будет работать, как задумано:

def make_inc():
    total = 0
    def helper():
        nonlocal total  # <- тут
        total += 1
        return total
    return helper

f = make_inc()
print(f())

Почему мы редко видим global и nonlocal?
nonlocal – специфичная вещь, обычно вместо нее создают класс.
global потакает плохим практикам программирования. Менять глобальные переменные внутри функций – плохая практика.

📎 Пример.

def foo():
    global x
    print('x is', x)
    for x in range(2):
        ...
x = 5
foo()  # x is 5
foo()  # x is 1 (испортили из-за for)

Нет ошибок выполнения, но есть логическая ошибка! После первого вызова foo() мы испортили глобальную переменную x, она стала 1 (последним значением в цикле). Надо было просто называть переменные разными именами, и global не понадобится!

Специально для канала @pyway.

@ Оператор умножения матриц

А вы знали, что помимо обыденных операторов +, -, *, / и прочих, есть еще операторы @ и @=? Нет, это не про декораторы. Задуманы эти операторы были для умножения матриц и появились в версии Python 3.5. Однако встроенного типа «матрица» в Python нет, и ни один из встроенных типов эти операторы не реализует. Поэтому, быть может, о нем и не рассказывают на курсах.

Однако оператор @ рекомендуется для умножения матриц в библиотеке numpy:

>>> import numpy as np
>>> a = np.array( [ [1, 2], [-2, 3] ] )
>>> b = np.array( [ [3, 0], [1, -3] ] )
>>> a @ b
array([[ 5, -6],
       [-3, -9]])
>>> np.matmul(a, b)
array([[ 5, -6],
       [-3, -9]])

⚠️ Обратите внимание, что это именно np.matmul, а не np.dot!

Также вы можете написать реализацию операторов @ и @= для своих классов. Для этого вам понадобятся магические методы matmul__, __imatmul__, __rmatmul__ . Смотрите пример по ссылке.

Специально для канала @pyway.

🔐 Храним секреты правильно

Наверное, каждый когда-то писал в своем коде:

DB_HOST = 'localhost'
DB_USER = 'root'
DB_PASSWORD = 'l33thAxor666'

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

Для хранения секретов и паролей придет на помощь библиотека keyring.

В зависимости от ОС и среды она использует:
• macOS Keychain
• Freedesktop Secret Service
• KDE4 & KDE5 KWallet
• Windows Credential Locker
• и другие бэкенды…

Мы храним в скрипте или конфиге только название системы и логин (можете использовать произвольные):

>>> import keyring
>>> keyring.set_password("my_system", "my_username", "password")
>>> keyring.get_password("my_system", "my_username")
'password'

Другие пользователи системы не смогут прочитать эти данные. Но от вашего имени можно получить доступ к ним даже из терминала:

$ keyring set my_system my_username
Password for 'my_username' in 'my_system':
$ keyring get my_system my_username
qwerty

Считать пароль безопасно с клавиатуры можно с помощью модуля getpass (он строен в Python). Вводимые символы не будут видны на экране:

>>> import getpass
>>> password = getpass.getpass(prompt="Enter super password:")
Enter super password:
>>> password
'qwerty'

Специально для канала @pyway.

🔢 Приоритет операций

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

2 * 2 + 2 = 6

Рассмотрим таблицу приоритета операций в языке Python. Сверху таблицы самые приоритетные операции, снизу – операции с низким приоритетом.

ОперацияОписание
( )Скобки
**Экспонента (возведение в степень)
+x, -x, ~xУнарные плюс, минус и битовое отрицание
*, /, //, %Умножение, деления, взятие остатка
+, —Сложение и вычитание
<<, >>Битовые сдвиги
&Битовое И
^Битовое исключающее ИЛИ (XOR)
|Битовое ИЛИ
==, !=, >, >=, <, <=,
is, is not,
in, not in
Сравнение, проверка идентичности,
проверка вхождения
notЛогическое НЕ
andЛогическое И
orЛогическое ИЛИ

Как видно, скобки самые главные. Скобки решают все.

Если в одном выражении идут операторы одинакового приоритета, то вычисления выполняются слева направо.

Исключение составляет оператор **. Он право-ассоциативный. Т.е. в цепочке из двух ** сначала выполнится правый, а потом левый.

>>> 3 ** 4 ** 2
43046721
>>> 3 ** (4 ** 2)
43046721
>>> (3 ** 4) ** 2
6561

Обратите внимание на приоритеты not, and и or.

not a or b and c   ===   (not a) or (b and c)

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

В случае с операторами сравнения, помните про цепочки сравнений!

         x < y < z
это ни   (x < y) < z,
ни       x < (y < z),
а        x < y and y < z

Специально для канала @pyway.

⛓ Цепочки сравнений

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

if x >= 5 and x < 20:

Однако Python предоставляет нам синтаксическое удобство, которое выглядит более «математичным». Такая запись и короче, и понятнее:

if 5 <= x < 20:

В качестве операторов сравнения могут быть любые из списка в любых сочетаниях:

">", "<", "==", ">=", "<=", "!=", "is" ["not"], ["not"] "in"

Т.е. запись вида a < b > c вполне законна, хоть и трудна для понимания.

Формально, если мы имеем N операций OP1…OPN и N + 1 выражений (a, b … y, z), то запись вида:

a OP1 b OP2 c … y OPN z 

Это эквивалентно записи:

a OP1 b and b OP2 c and … and y OPN z

📎 Примеры:

x = 5
print(1 < x < 10)
print(x < 10 < x*10 < 100)
print(10 > x <= 9)
print(5 == x > 4)
a, b, c, d, e, f = 0, 5, 12, 0, 15, 15
print(a <= b < c > d is not e is f)

Специально для канала @pyway.

Итераторы и генераторы

В чем разница между итератором и генератором? Этот вопрос можно часто услышать на собеседованиях.

Итератор – более общая концепция, чем генератор.

Итератор – это интерфейс доступа к элементам коллекций и потоков данных. Он требует реализации единственного метода – «дай мне следующий элемент». Если вы пишите свой итератор на Python 3 вам нужно реализовать в классе метод __next__. Если элементы исчерпаны итератор возбудит исключение StopIteration.

📎 Пример. Итератор счетчик – выдает числа от low до high:

class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high
    def __iter__(self):
        return self
    def __next__(self): 
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

Генератор – это итератор

Генератор – это итератор, но не наоборот. Не любой итератор является генератором.

Есть два способа получить генератор:

📎 1. Генераторное выражение (что-то типа list comprehension, но возвращает генератор, а не список). Используются круглые скобки:

>>> g = (2 * i for i in range(5))
>>> type(g)
<class 'generator'>
>>> next(g)
0
>>> next(g)
2

📎 2. Генераторные функции. Это функции, где есть хотя бы одно выражение yield. Когда мы запускаем генератор, функция выполняет до первого выражения yield. То, что мы передали в yield будет возвращено наружу. Генератор при этом встанет «на паузу» до следующей итерации. При следующей итерации выполнение генератора продолжится до очередного yield.

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

Пример. Генератор чисел Фибоначчи (бесконечный):

def fib():
    a, b = 0, 1
    while 1:
        yield a
        a, b = b, a + b

>>> fib_g = fib()
>>> next(fib_g)
0
>>> next(fib_g)
1
>>> next(fib_g)
1
>>> next(fib_g)
2
>>> next(fib_g)
3
>>> next(fib_g)
5

Вызвав генераторную функцию fib() мы получили генератор. Затем мы итерируем этот генератор функцией next().

Передача данных в генератор

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

send() – отправить данные в генератор. Переданное значение вернется из той конструкции yield, на которой возникла последняя пауза генератора. При этом генератор будет прокручен на один шаг, как если бы мы вызвали next:

val = yield i  # генератор вернет i, но внутри получит val из аргумента метода send

Пример. Этот генератор просто выдает числа от 0 и далее, при этом печатает в поток вывода все, что мы ему отправляем.

def my_gen():
    i = 0
    while True:
        val = yield i
        print('Got inside generator:', val)
        i += 1

>>> g = my_gen()
>>> next(g)
0
>>> g.send("hello")
Got inside generator: hello
1
>>> g.send("world")
Got inside generator: world
2

Обратите внимание, что первый раз нельзя посылать в генератор данные, пока мы не прокрутили его до первого yield. Нужно либо взывать next(g) или g.send(None) – это одно и тоже.

Не будет ошибкой отправлять данные генератору, который не получает их (нет использования значения конструкции yield). Например, нашему генератору fib() можно отравить все, что угодно, он просто проигнорирует.

throw() – бросить исключение внутри генератора. Исключение будет возбуждено из того выражение yield, где генератор последний раз остановился.

>>> g = my_gen()   # my_gen из прошлого примера

>>> g.throw(TypeError, 'my error')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in my_gen
TypeError: my error

close() – закрыть генератор. Бросает внутри генератора особое исключение GeneratorExit. Это исключение, даже если оно не обработано, не распространится в код, вызвавший close(). Но, если мы поймали это исключение внутри генератора, то после закрытия генератора нельзя уже делать yield, рискуя получить RuntimeError. Остальные виды исключений будут распространяться из генератора в код, его вызывающий. Попытка итерировать закрытый итератор приведет к исключению StopIteration (закрытый генератор – пустой итератор).

>>> g = my_gen()
>>> next(g)
0
>>> next(g)
Got inside generator: None
1
>>> g.close()
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Бонус

Как взять из итератора (в том числе из генератора) N первых значений?

Можно, конечно, написать свою функцию. Но зачем, если она уже есть в стандартном модуле itertools. Этот модуль содержит множество вспомогательных функций для работы с итераторами. Нам понадобится itertools.islice. Первый аргумент – итератор (ну или генератор), остальные три – как в range.

>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

>>> list(itertools.islice(fib(), 10, 20, 2))
[55, 144, 377, 987, 2584]

В первом примере мы передаем в функцию itertools.islice наш генератор чисел Фибоначчи и число чисел, которые надо вычислить (в нашем случае – 10).

Мы также применяем функцию list, чтобы посмотреть список значений, потому что itertools.islice возвращает не спикок, а именно новый итератор, в котором будут только интересные нам значений из исходного итератора.

Во втором примеры аргументов 4 штуки. В этом случае второй аргумент – начальный номер = 10, третий – конечный номер = 20 – (не включительно), и четвертый – шаг = 2. (Очень похоже на range, не так ли?)

Специально для канала @pyway.

Python: is

Новички часто путаются в конструкциях is и ==. Давайте разберемся, что к чему.

Сразу к сути: == (и его антагонист !=) применяются для проверки равенства (неравенства) значения двух объектов. Значение, это непосредственно то, что лежит в переменной. Значение числа 323235 – собственно число 323235. Тавтология. Но на примерах станет яснее.

Оператор is (и его антагонист is not) применяются проверки равенства (неравенства) ссылок на объект. Сразу отметим то, что на значение (допустим 323235) может быть копировано и храниться в разных местах (в разных объектах в памяти).

>>> x = 323235
>>> y = 323235
>>> x == y
True
>>> x is y
False

Видите, значение переменных равны по значению, но они ссылаются на разные объекты. Я не случайно взял большое число 323235. Дело в том, что в целях оптимизации интерпретатор Python при старте создает некоторые количество часто-используемых констант (от -5 до 256 включительно).

Следите внимательно за ловкостью рук:

>>> x = 256
>>> y = 256
>>> x is y
True
>>> x = 257
>>> y = 257
>>> x is y
False
>>> x = -5
>>> y = -5
>>> x is y
True
>>> x = -6
>>> y = -6
>>> x is y
False 

Поэтому новички часто совершают ошибку, считая, что писать == – это как-то не Python-way, а is – Python-way. Это ошибочное предположение может быть раскрыто не сразу.

Python старается кэшировать и переиспользовать строковые значения. Поэтому весьма вероятно, что переменные, содержащие одинаковые строки, будут содержать ссылки на одинаковые объекты. Но это не факт! Смотрите последний пример:

>>> x = "hello"
>>> y = "hello"
>>> x is y
True
>>> x = "hel" + "lo"
>>> y = "hello"
>>> x is y
True
>>> a = "hel"
>>> b = "lo"
>>> x = a + b
>>> y = "hello"
>>> x == y
True
>>> x is y
False

Мы составили строку из двух частей и она попала в другой объект. Python не догадался (и правильно) поискать ее в существующих строках.

Суть is (id)

В Python есть встроенная функция id. Она возвращает идентификатор объекта – некоторое число. Гарантируется, что оно будет различно для различных объектах в пределах одного интерпретатора. В реализации CPython – это просто адрес объекта в памяти интерпретатора.

Так вот:

a is b

Это тоже самое, что:

id(a) == id(b)

И все! Пример для проверки:

>>> x = 10.40
>>> y = 10.40
>>> x is y
False
>>> x == y
True

>>> id(x)
4453475504
>>> id(y)
4453475600
>>> id(x) == id(y)
False

>>> x = y
>>> x is y
True
>>> id(x)
4453475600
>>> id(y)
4453475600

Значения переменных равны, но их id – разные, и is выдает False. Как только мы к x привязали y, то ссылки стали совпадать.

Для чего можно применять is?

Если мы точно знаем уверены, что хотим проверять именно равенство ссылок на объекты (один ли это объект в памяти или разные).

Еще можно применять is для сравнения с None. None – это встроенная константа и двух None быть не может.

>>> x is None
False
>>> x = None
>>> x is None
True

Также для Ellipsis:

>>> ... is Ellipsis
True
>>> x = ...
>>> y = ...
>>> x is y
True

Я не рекомендую применять is для True и False.

Потому что короче писать if x:, чем if x is True:.

Можно применять is для сравнения типов с осторожностью (без учета наследования, т. е. проверка на точное совпадение типов):

>>> x = 10.5
>>> type(x) is float
True

С наследованием может быть конфуз:

>>> class Foo: ...
...
>>> class Bar(Foo): ...
...
>>> f = Foo()
>>> b = Bar()
>>> type(f) is Foo
True
>>> type(b) is Bar
True
>>> type(b) is Foo
False
>>> isinstance(b, Foo)
True

Не смотря на то, что Bar – наследник Foo, типы переменных foo и bar не совпадают. Если нам важно учесть наcледование, то пишите isinstance.

Нюанс: is not против is (not)

Важно знать, что is not – это один целый оператор, аналогичный id(x) != id(y). А в конструкции x is (not y) – у нас сначала будет логическое отрицание y, а потом просто оператор is.

Пример уловки:

>>> x = 10
>>> x is not None
True
>>> x is (not None)
False

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

Далее речь пойдет об обычных == и !=. Можно определить магический метод __eq__, который обеспечит поведение при сравнении классов. Если он не реализован, то объекты будет сравниваться по ссылкам (как при is).

>>> class Baz: ...
...
>>> x = Baz()
>>> y = Baz()
>>> x == y
False
>>> x = y
>>> x == y
True

Если он реализован, то будет вызван метод __eq__ для левого операнда.

class Foo:
 def __init__(self, x):
  self.x = x
 def __eq__(self, other):
  print('Foo __eq__ {} and {}'.format(self, other))
  return self.x == other.x

>>> x = Foo(5)
>>> y = Foo(5)
>>> x == y
Foo __eq__ <__main__.Foo object at 0x109e9c048> and <__main__.Foo object at 0x109e8a5c0>
True

Метод __ne__ отвечает за реализацию !=. По умолчанию он вызывает not x.__eq__(y). Но рекомендуется реализовывать их оба вручную, чтобы поведение сравнения было согласовано и явно.

Вопрос к размышлению: что будет если мы сравним объекты разных классов, причем оба класса реализуют __eq__?

Что будет, если мы реализуем __ne__, но не реализуем __eq__?

А еще есть метод __cmp__. Это уже выходит за рамки статьи про is. Почитайте самостоятельно…

Специально для канала @pyway.

🖨 Все о print

print – одна из первый функций, с которой знакомятся новички, изучающие Python.

>>> print("Hello world!")
Hello world!

Однако, print умеет больше, чем просто печатать через пробел. Взглянем на сигнатуру:

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

Именованные параметры управляют поведением функции.

Во-первых, обратим внимание на параметр sep – это строка-разделитель, она будет вставлена между каждым из неименованных параметров переданным в print (по умолчанию – одиночный пробел).

>>> print(1, 2, 3, 4, sep=" and ")
1 and 2 and 3 and 4

Если передать sep=’\n’, то каждый параметр будет выведен с новой строки.

Во-вторых, параметр end – будет выведен после печати последнего аргумента. По умолчанию, перевод строки. Например, иногда требуется печатать в одну строку: тогда передим end=»:

>>> print("Same", end=''); print("Line")
SameLine

Параметр file указывает файл или поток, в который будет выводится информация, по умолчанию, это стандартный вывод sys.stdout. Можно печатать в файл:

>>> f = open('1.txt', 'w')
>>> print("hello", file=f)
>>> f.close()

Параметр flush=True заставляет систему немедленно сбросить содержимое буфера в поток вывода. Изредка данные застревают в буфере, и этим мы их проталкиваем. Например, при end=», «1» появится только через 5 секунд сразу с «2».

>>> print('1', end=''); time.sleep(5); print('2')

Исправим это:

>>> print('1', end='', flush=True); time.sleep(5); print('2')

Наконец, не забудем, что бывает удобно печатать коллекции, передав их в print со звездочкой.

>>> arr = ["Apple", "pear", "orange"]
>>> print(*arr, sep=", ")
Apple, pear, orange

Индексирование в Python

Положительные и отрицательные индексы

Допустим у нас есть список или кортеж.

x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
t = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Без потери общности будем работать только со списком х (с кортежем t – тоже самое).

Легко получить i-тый элемент этого списка по индексу.

Внимание! Индексы в Python считаются с нуля (0), как в С++ и Java.

>>> x[0]
0
>>> x[7]
7
>>> x[11]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

В последней строке мы вылезли за пределы (у нас в списке последний индекс – 10) и получили исключение IndexError.

Но что будет, если мы обратимся к элементу с отрицательным индексом? В С++ такой операцией вы бы прострелили себе ногу. А в Python? IndexError? Нет!

>>> x[-1]
10
>>> x[-2]
9
>>> x[-10]
1
>>> x[-11]
0

Это совершенно легально. Мы просто получаем элементы не с начала списка, а с конца (-i-тый элемент).
x[-1] – последний элемент.
x[-2] – предпоследний элемент.

Это аналогично конструкции x[len(x)-i]:

>>> x[len(x)-1]
10

Обратите внимание, что начальный (слева) элемент в отрицательной нотации имеет индекс -11.

Срезы

Срезы, они же slices, позволяют вам получить какую-то часть списка или кортежа.

Форма x[start:end] даст элементы от индекса start (включительно) до end (не включая end). Если не указать start – мы начнем с 0-го элемента, если не указать end – то закончим последним элементом (включительно). Соотвественно, x[:] это тоже самое, что и просто x.

>>> x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> x[2:8]
[2, 3, 4, 5, 6, 7]
>>> x[:8]
[0, 1, 2, 3, 4, 5, 6, 7]
>>> x[2:]
[2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> x[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Если end <= start, получим пустой список.

>>> x[5:3]
[]

Аналогично мы можем получать срезы с отчетом от конца списка с помощью отрицательных индексов.

>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a[-4:-2]
[7, 8]

В этом случае также start < end, иначе будет пустой список.

Форма x[start:end:step] даст элементы от индекса start (включительно) до end (не включая end), в шагом step. Если step равен 1, то эта форма аналогична предыдущей рассмотренной x[start:end].

>>> x[::2]
[0, 2, 4, 6, 8, 10]
>>> x[::3]
[0, 3, 6, 9]
>>> x[2:8:2]
[2, 4, 6]

x[::2] – каждый второй элемент, а x[::3] – каждый третий. 

Отрицательный шаг вернет нам элементы в обратном порядке:

>>> x[::-1]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# как если бы:
>>> list(reversed(x))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# в обратном порядке с шагом 2
>>> x[::-2]
[10, 8, 6, 4, 2, 0]

Запись в список по срезу

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

Если размеры равны (в примере два элемента в срезе и два элемента во втором списке) – происходит замена элементов.

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

Если они не равны по размеру, то в результате список расширяется или сжимается.

>>> a = [1, 2, 3, 4, 5]
# размер среза = 1 элемент, а вставляем два (массив расширился)
>>> a[2:3] = [0, 0]
>>> a
[1, 2, 0, 0, 4, 5]

# тут вообще пустой размер среза = вставка подсписка по индексу 1
>>> a[1:1] = [8, 9]
>>> a
[1, 8, 9, 2, 0, 0, 4, 5]

# начиная с элемента 1 и кончая предпоследним элементом мы уберем (присвоив пустой список)
>>> a[1:-1] = []
>>> a
[1, 5]

Именованные срезы

Можно заранее создавать срезы с какими-то параметрами без привязки к списку или кортежу встроенной функцией slice. А потом применить этот срез к какому-то списку.

>>> a = [0, 1, 2, 3, 4, 5]
>>> LASTTHREE = slice(-3, None)
>>> LASTTHREE
slice(-3, None, None)
>>> a[LASTTHREE]
[3, 4, 5]

Вместо пустых мест для start, end или step здесь мы пишем None.

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

Индексирование своих объектов

В конце концов, мы можете определить самостоятельно поведение оператор индексации [], определив для своего класса магические методы __getitem__, __setitem__ и __delitem__. Первый вызывается при получении значения по индекса (или индексам), второй – если мы попытаемся нашему объекту что-то присвоить по индексу. А третий – если мы будет пытаться делать del по индексу. Необязательно реализовывать их все. Можно только один, например:

# при чтении по индексу из этого класса, мы получим удвоенных индекс
class MyClass:
    def __getitem__(self, key):
       return key * 2

myobj = MyClass()
myobj[3]  # вернет 6
myobj["privet!"] # приколись, будет: 'privet!privet!'

В качестве ключей можно использовать не только целые числа, но и строки или любые другие значения, в том числе slice и Ellipsis. Как вы будете обрабатывать их – решать вам. Естественно, логика, описанная в предыдущих разделах, здесь будет только в том случае, если вы ее сами так реализуете.

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

class IntegerNumbers:
  def __getitem__(self, key):
    if isinstance(key, int):
      return key
    elif isinstance(key, slice):
      return list(range(key.start, key.stop, key.step))
    else:
      raise ValueError

ints = IntegerNumbers() 
print(ints[10])  # 10
print(ints[1:10:2]) # [1, 3, 5, 7, 9]
print(ints["wwdwd"]) # так нельзя

Можно иметь несколько индексов. Ниже мы суммируем все значения индексов.

class MultiIndex:
  def __getitem__(self, keys): 
    # все индексы (если их 2 и больше попадут) в keys с типом tuple
    return sum(keys)  # просуммируем их

prod = MultiIndex()
print(prod[10, 20])  # напечает 30

Удачи в программировании и жизни!

Специально для канала PyWay.