company_banner

Подборка @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()

    Mail.Ru Group

    778,83

    Строим Интернет

    Поделиться публикацией

    Похожие публикации

    Комментарии 4
      0
      А можно, пожалуйста, поподробнее про 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

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

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое