Подборка @pythonetc, сентябрь 2018

79urugmgbgzjhlpozccip51e5_s.png

Это четвёртая подборка советов про 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()

© Habrahabr.ru