Это восьмая подборка советов про 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() # <class '__main__.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()
<MagicMock name='mock.x()' id='139769776427752'>
Очевидно, что этот код будет работать с последовательным обращением к атрибутам на любую глубину. При этом аргументы методов игнорируются:
>>> m.a.b.c.d
<MagicMock name='mock.a.b.c.d' id='139769776473480'>
>>> m.a.b.c.d
<MagicMock name='mock.a.b.c.d' id='139769776473480'>
>>> m.x().y().z()
<MagicMock name='mock.x().y().z()' id='139769776450024'>
>>> m.x(1).y(1).z(1)
<MagicMock name='mock.x().y().z()' id='139769776450024'>
А если вы зададите какому-нибудь атрибуту значение, то заглушка больше не будет возвращаться:
>>> 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]
<MagicMock name='mock.__getitem__().__getitem__()' id='139769776049848'>
>>> m.__getitem__.return_value.__getitem__.return_value = 50
>>> m[1][2]
50