
Это четвёртая подборка советов про Python и программирование из моего авторского канала @pythonetc.
Предыдущие подборки:
Переопределение и перегрузка
Существует две концепции, которые легко спутать: переопределение (overriding) и перегрузка (overloading).
Переопределение случается, когда дочерний класс определяет метод, уже предоставленный родительскими классами, и тем самым заменяет его. В каких-то языках требуется явным образом помечать переопределяющий метод (в C# применяется модификатор override), а в каких-то языках это делается по желанию (аннотация @Override в Java). Python не требует применять специальный модификатор и не предусматривает стандартной пометки таких методов (кто-то ради читабельности использует кастомный декоратор @override, который ничего не делает).
С перегрузкой другая история. Этим термином обозначается ситуация, когда есть несколько функций с одинаковым именем, но с разными сигнатурами. Перегрузка возможна в Java и C++, она часто используется для предоставления аргументов по умолчанию:
class Foo { public static void main(String[] args) { System.out.println(Hello()); } public static String Hello() { return Hello("world"); } public static String Hello(String name) { return "Hello, " + name; } }
Python не поддерживает поиск функций по сигнатуре, только по имени. Конечно, вы можете написать код, явным образом анализирующий типы и количество аргументов, но это будет выглядеть неуклюже, и такой практики лучше избегать:
def quadrilateral_area(*args): if len(args) == 4: quadrilateral = Quadrilateral(*args) elif len(args) == 1: quadrilateral = args[0] else: raise TypeError() return quadrilateral.area()
Если вам нужны type hints, воспользуйтесь модулем typing с декоратором @overload:
from typing import overload @overload def quadrilateral_area( q: Quadrilateral ) -> float: ... @overload def quadrilateral_area( p1: Point, p2: Point, p3: Point, p4: Point ) -> float: ...
Автовивификация
collections.defaultdict позволяет создать словарь, который возвращает значение по умолчанию, если запрошенный ключ отсутствует (вместо выбрасывания KeyError). Для создания defaultdictвам нужно предоставить не просто дефолтное значение, а фабрику таких значений.
Так вы можете создать словарь с виртуально бесконечным количеством вложенных словарей, что позволит использовать конструкции вроде d[a][b][c]...[z].
>>> def infinite_dict(): ... return defaultdict(infinite_dict) ... >>> d = infinite_dict() >>> d[1][2][3][4] = 10 >>> dict(d[1][2][3][5]) {}
Такое поведение называется «автовивификацией», этот термин пришёл из Perl.
Инстанцирование
Инстанцирование объектов включает в себя два важных шага. Сначала из класса вызывается метод __new__, который создаёт и возвращает новый объект. Затем из него Python вызывает метод __init__, который задаёт начальное состояние этого объекта.
Однако __init__ не будет вызван, если __new__ возвращает объект, не являющийся экземпляром исходного класса. В этом случае объект мог быть создан другим классом, и значит __init__ уже вызывался на объекте:
class Foo: def __new__(cls, x): return dict(x=x) def __init__(self, x): print(x) # Never called print(Foo(0))
Это также означает, что не следует создавать экземпляры того же класса в __new__ с помощью обычного конструктора (Foo(...)). Это может привести к повторному исполнению __init__, или даже к бесконечной рекурсии.
Бесконечная рекурсия:
class Foo: def __new__(cls, x): return Foo(-x) # Recursion
Двойное исполнение __init__:
class Foo: def __new__(cls, x): if x < 0: return Foo(-x) return super().__new__(cls) def __init__(self, x): print(x) self._x = x
Правильный способ:
class Foo: def __new__(cls, x): if x < 0: return cls.__new__(cls, -x) return super().__new__(cls) def __init__(self, x): print(x) self._x = x
Оператор [] и срезы
В Python можно переопределить оператор [], определив магический метод __getitem__. Так, например, можно создать объект, который виртуально содержит бесконечное количество повторяющихся элементов:
class Cycle: def __init__(self, lst): self._lst = lst def __getitem__(self, index): return self._lst[ index % len(self._lst) ] print(Cycle(['a', 'b', 'c'])[100]) # 'b'
Необычное здесь заключается в том, что оператор [] поддерживает уникальный синтаксис. С его помощью можно получить не только [2], но и [2:10], [2:10:2], [2::2] и даже [:]. Семантика оператора такая: [start:stop:step], однако вы можете использовать его любым иным образом для создания кастомных объектов.
Но если вызывать с помощью этого синтаксиса __getitem__, что он получит в качестве индексного параметра? Именно для этого существуют slice-объекты.
In : class Inspector: ...: def __getitem__(self, index): ...: print(index) ...: In : Inspector()[1] 1 In : Inspector()[1:2] slice(1, 2, None) In : Inspector()[1:2:3] slice(1, 2, 3) In : Inspector()[:] slice(None, None, None)
Можно даже объединить синтаксисы кортежей и слайсов:
In : Inspector()[:, 0, :] (slice(None, None, None), 0, slice(None, None, None))
slice ничего не делает, только хранит атрибуты start, stop и step.
In : s = slice(1, 2, 3) In : s.start Out: 1 In : s.stop Out: 2 In : s.step Out: 3
Прерывание корутины asyncio
Любую исполняемую корутину (coroutine) asyncio можно прервать с помощью метода cancel(). При этом в корутину будет отправлена CancelledError, в результате эта и все связанные с ней корутины будут прерваны, пока ошибка не будет поймана и подавлена.
CancelledError — подкласс Exception, а значит её можно случайно поймать с помощью комбинации try ... except Exception, предназначенной для ловли «любых ошибок». Чтобы безопасно для сопрограммы поймать ошибку, придётся делать так:
try: await action() except asyncio.CancelledError: raise except Exception: logging.exception('action failed')
Планирование исполнения
Для планирования исполнения какого-то кода в определённое время в asyncio обычно создают task, которая выполняет await asyncio.sleep(x):
import asyncio async def do(n=0): print(n) await asyncio.sleep(1) loop.create_task(do(n + 1)) loop.create_task(do(n + 1)) loop = asyncio.get_event_loop() loop.create_task(do()) loop.run_forever()
Но создание новоого таска может стоить дорого, да это и не обязательно делать, если вы не планируете выполнять асинхронные операции (вроде функции do в моём примере). Вместо этого можно использовать функции loop.call_later и loop.call_at, которые позволяют запланировать вызов асинхронного коллбека:
import asyncio def do(n=0): print(n) loop = asyncio.get_event_loop() loop.call_later(1, do, n+1) loop.call_later(1, do, n+1) loop = asyncio.get_event_loop() do() loop.run_forever()
