Подборка @pythonetc, сентябрь 2018
Это четвёртая подборка советов про 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()