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

Комментарии (4)


  1. TyVik
    03.10.2018 21:51

    А можно, пожалуйста, поподробнее про overload? У меня конструкция

    @overload
    def a(b: None) -> None: return None
    @overload
    def a(b: int) -> str: return 'int'
    @overload
    def a(b: float) -> str: return 'float'
    def a(b) -> str: return 'all other'
    
    print(a(1.0))
    

    всегда выводит 'all other'. Если её убрать, то возникает NotImplementedError: You should not call an overloaded function. A series of overload-decorated functions outside a stub module should always be followed by an implementation that is not overload-ed.


    1. Zada
      03.10.2018 22:55
      +1

      Ибо overload декоратор используется только тайпчекером.


      The overload-decorated definitions are for the benefit of the type checker only, since they will be overwritten by the non-@overload-decorated definition, while the latter is used at runtime but should be ignored by a type checker.


    1. pushtaev Автор
      03.10.2018 23:35

      Zada верно подсказывает. Использовать надо именно так, как в примере: есть функция, которая содержит все реализации через if, а есть набор сигнатур для тайпчекера. Там "..." в теле — не условность; это реальный питонячий код.


      1. Johan
        04.10.2018 01:33

        В качестве альтернативы if можно посмотреть на functools.singledispatch.