Подборка @pythonetc, январь 2019

Это восьмая подборка советов про Python и программирование из моего авторского канала @pythonetc.
Предыдущие подборки:
Два неявных метода классов
Для создания метода класса нужно использовать декоратор @classmethod. Потом этот метод можно вызывать напрямую из класса, а не из его экземпляров, и он будет принимать класс в качестве первого аргумента (его обычно вызывают cls, а не self).
Однако в модели данных Python есть два неявных метода класса: __new__ и __init_subclass__. Они работают так, словно тоже декорированы с помощью @classmethod, хотя это и не так (__new__ создаёт новые экземпляры класса, а __init_subclass__ является хуком, который вызывается при создании производного класса).
class Foo:
def __new__(cls, *args, **kwargs):
print(cls)
return super().__new__(
cls, *args, **kwargs
)
Foo() #
Асинхронные менеджеры контекста
Если вы хотите, чтобы менеджер контекста приостанавливал корутину при входе или выходе из контекста, то пользуйтесь асинхронными менеджерами. Тогда вместо вызова m.__enter__() и m.__exit__() Python будет делать await на m.__aenter__() и m.__aexit__() соответственно.
Асинхронные менеджеры контекста нужно применять с синтаксисом async with:
import asyncio
class Slow:
def __init__(self, delay):
self._delay = delay
async def __aenter__(self):
await asyncio.sleep(self._delay / 2)
async def __aexit__(self, *exception):
await asyncio.sleep(self._delay / 2)
async def main():
async with Slow(1):
print('slow')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Определяем асинхронный менеджер контекста
Начиная с Python 3.7 contextlib предоставляет декоратор asynccontextmanager, который позволяет определять асинхронный менеджер контекста таким же образом, как это делает contextmanager:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def slow(delay):
half = delay / 2
await asyncio.sleep(half)
yield
await asyncio.sleep(half)
async def main():
async with slow(1):
print('slow')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
В более старых версиях языка вы можете использовать @asyncio_extras.async_contextmanager.
Унарный оператор «плюс»
В Python нет оператора ++, вместо него используется x += 1. Но при этом синтаксис ++x является валидным (а x++ — уже нет).
Хитрость в том, что в Python есть унарный оператор «плюс», и ++x на самом деле является x.__pos__().__pos__(). Этим можно злоупотребить и заставить ++ работать как инкрементирование (но я бы не рекомендовал так делать):
class Number:
def __init__(self, value):
self._value = value
def __pos__(self):
return self._Incrementer(self)
def inc(self):
self._value += 1
def __str__(self):
return str(self._value)
class _Incrementer:
def __init__(self, number):
self._number = number
def __pos__(self):
self._number.inc()
x = Number(4)
print(x) # 4
++x
print(x) # 5
Объект MagicMock
Объект MagicMock позволяет брать у себя любой атрибут и вызывать любой метод. При таком способе доступа возвращается новая заглушка (mock). Более того, вы получаете такой же объект-заглушку, если обращаетесь к тому же атрибуту (или вызываете тот же метод):
>>> from unittest.mock import MagicMock
>>> m = MagicMock()
>>> a = m.a
>>> b = m.b
>>> a is m.a
True
>>> m.x() is m.x()
True
>>> m.x()
Очевидно, что этот код будет работать с последовательным обращением к атрибутам на любую глубину. При этом аргументы методов игнорируются:
>>> m.a.b.c.d
>>> m.a.b.c.d
>>> m.x().y().z()
>>> m.x(1).y(1).z(1)
А если вы зададите какому-нибудь атрибуту значение, то заглушка больше не будет возвращаться:
>>> m.a.b.c.d = 42
>>> m.a.b.c.d
42
>>> m.x.return_value.y.return_value = 13
>>> m.x().y()
13
Однако это не работает с m[1][2]. Дело в том, что MagicMock не обрабатывает обращение к элементу, это просто вызов метода:
>>> m[1][2] = 3
>>> m[1][2]
>>> m.__getitem__.return_value.__getitem__.return_value = 50
>>> m[1][2]
50
