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


ссылка на оригинал статьи https://habr.com/post/425125/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *