Это восьмая подборка советов про 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

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