Типы класса, перегружающие атрибуты другого класса - PullRequest
1 голос
/ 17 марта 2020

Я пытаюсь создать класс, который вызывает другие атрибуты класса, используя __getattr__, чтобы обернуть вызовы класса.

from aiohttp import ClientSession
from contextlib import asynccontextmanager


class SessionThrottler:

    def __init__(self, session: ClientSession,
                 time_period: int, max_tasks: int):
        self._obj = session
        self._bucket = AsyncLeakyBucket(max_tasks=max_tasks,
                                        time_period=time_period)

    def __getattr__(self, name):
        @asynccontextmanager
        async def _do(*args, **kwargs):
            async with self._bucket:
                res = await getattr(self._obj, name)(*args, **kwargs)
                yield res
        return _do

    async def close(self):
        await self._obj.close()

Итак, я могу сделать:

async def fetch(session: ClientSession):
    async with session.get('http://localhost:5051') as resp:
        _ = resp


session = ClientSession()
session_throttled = SessionThrottler(session, 4, 2)
await asyncio.gather(
    *[fetch(session_trottled) 
      for _ in range(10)]
)

Этот код работает нормально, но как я могу сделать так, чтобы session_throttled выводился как ClientSession вместо SessionThrottler (вроде functools.wraps)?

Ответы [ 2 ]

1 голос
/ 17 марта 2020

Я зависит от того, что вам нужно, с помощью "выводится как".

Создание экземпляров ThrotledSessions для ClientSessions

Естественный способ сделать это с помощью классов - через наследование - если ваш SessionThrotler наследуется от ClientSession, естественно, будет a ClientSession. «Небольшой недостаток» заключается в том, что тогда __getattr__ не будет работать должным образом, поскольку вызывается только для атрибутов, не найденных в экземпляре, а Python будет «видеть» исходные методы из ClientSession в вашем объекте ThrotledSession и вызывать те, вместо

Конечно, это также потребует от вас статического наследования вашего класса, и вы можете захотеть, чтобы он работал динамически. (Статически я имею в виду необходимость писать class SessionThrotler(ClientSession): - или, по крайней мере, если существует конечное число различных классов Session, которые вы хотите обернуть, напишите также для каждого подкласса, унаследованного от ThrotledClass:

class ThrotledClientSession(ThrotledSession, ClientSession):
    ...

Если это то, что вам подходит, тогда нужно исправить доступ к атрибутам, создав __getattribute__ вместо __getattr__. Разница между ними заключается в том, что __getattribte__ охватывает все поиск атрибута выполняется и вызывается в начале поиска, в то время как __getattr__ вызывается как часть нормального поиска (внутри стандартного алгоритма для __getattribute__), когда все остальное не удается.

class SessionThrottlerMixin:

    def __init__(self, session: ClientSession,
                 time_period: int, max_tasks: int):
        self._bucket = AsyncLeakyBucket(max_tasks=max_tasks,
                                        time_period=time_period)

    def __getattribute__(self, name):
        attr = super().__getattribute__(name)
        if not name.startswith("_") or not callable(attr):
             return attr
        @asynccontextmanager
        async def _do(*args, **kwargs):
            async with self._bucket:
                res = await attr(*args, **kwargs)
                yield res
        return _do

    class ThrotledClientSession(SessionThrottlerMixin, ClientSession):
        pass

Если вы получаете свои CLientSession экземпляры из другого кода и не хотите или не можете заменить базовый класс этим, вы можете сделать это для нужных экземпляров, присвоив атрибуту __class__ : он работает, если ClientSession является нормальным Python классом, не наследующим от специальных баз, таких как встроенные * 1095, не использующими __slots__ и некоторыми другими ограничениями - экземпляр "преобразуется" в ThrotledClientSession в полете (но вы должны выполнить наследование): session.__class__ = ThrottledClientSession.

Назначение класса таким образом не будет запускать новый класс __init__. Поскольку вам нужно создать _bucket, у вас может быть метод класса, который создаст корзину и произведет замену - поэтому в версии с __getattribute__ добавьте что-то вроде:


class SessionThrottler:
    ...
    @classmethod
    def _wrap(cls, instance, time_period: int, max_tasks: int):
       cls.__class__ = cls
       instance._bucket = AsyncLeakyBucket(max_tasks=max_tasks,
                                            time_period=time_period)
       return instance 

    ...

throtled_session = ThrotledClientSession._wrap(session, 4, 2)

Если у вас есть много родительских классов, которые вы хотите обернуть таким образом, и вы не хотите объявлять Throttled его версию, этот может быть создан динамически - но я бы только go таким образом, если бы это был единственный путь к go. Было бы предпочтительным объявить около 10 заглушек версий Thotled, по 3 строки в каждой.

Виртуальные подклассы

Если вы можете изменить код ваших ClientSession классов (и других, которые вы хотите обернуть), это наименее навязчивый способ - у

Python есть скрытая OOP функция, называемая Virtual Subclassing, в которой класс может быть зарегистрирован как подкласс другого без реального наследования. Однако класс, который должен быть «родительским», должен иметь abc.ABCMeta в качестве своего метакласса - иначе это действительно ненавязчиво.

Вот как это работает:


In [13]: from abc import ABC                                                                                                         

In [14]: class A(ABC): 
    ...:     pass 
    ...:                                                                                                                             

In [15]: class B:  # don't inherit
    ...:     pass

In [16]: A.register(B)                                                                                                               
Out[16]: __main__.B

In [17]: isinstance(B(), A)                                                                                                          
Out[17]: True

Так в исходном коде, если вы можете заставить ClientSession наследовать от abc.ABC (без каких-либо других изменений для всех ) - и затем сделать:

ClientSession.register(SessionThrottler), и это будет просто работайте (если вы подразумеваете, что «выводится как» имеет отношение к типу объекта).

Обратите внимание, что если ClientSession и другие имеют другой метакласс, добавление abc.ABC в качестве одной из его баз завершится с ошибкой конфликт метаклассов Если вы можете изменить их код, это все же лучший способ go: просто создайте совместный метакласс, который наследует от обоих метаклассов, и все настроено:


class Session(metaclass=IDontCare):
    ...


from abc import ABCMeta

class ColaborativeMeta(ABCMeta, Session.__class__):
    pass

class ClientSession(Session, metaclass=ColaborativeMeta):
    ...

Тип подсказки

Если вам не нужен "isinstance" для работы, и вам просто нужно быть одинаковым для системы ввода текста, тогда просто используйте typing.cast:

import typing as T
...
session = ClientSession()
session_throttled = T.cast(ClientSession, SessionThrottler(session, 4, 2))

Объект не затрагивается во время выполнения - точно такой же объект, но с этого момента такие инструменты, как mypy, будут считать его экземпляром ClientSession.

Последний, но не менее важный измените имя класса.

Таким образом, если под «выведено как» вы не подразумеваете, что обернутый класс должен рассматриваться как экземпляр, а просто заботитесь о правильном отображении имени класса в журналах и т. Д., Вы можете просто установить класс __name__ атрибут для любой строки, которую вы хотите:

class SessionThrottler:
    ...

SessionThrottelr.__name__ = ClientSession.__name__

или просто иметь соответствующий метод __repr__ в классе оболочки:

class SessionThrottler:
    ...
    def __repr__(self):
        return repr(self._obj)
0 голосов
/ 17 марта 2020

Это решение основано на исправлении методов объекта (вместо их переноса), заданных диспетчеру контекста.

import asyncio
import functools
import contextlib

class Counter:
    async def infinite(self):
        cnt = 0
        while True:
            yield cnt
            cnt += 1
            await asyncio.sleep(1)

def limited_infinite(f, limit):
    @functools.wraps(f)
    async def inner(*a, **kw):
        cnt = 0
        async for res in f(*a, **kw):
            yield res
            if cnt == limit:
                break
            cnt += 1
    return inner

@contextlib.contextmanager
def throttler(limit, counter):
    orig = counter.infinite
    counter.infinite = limited_infinite(counter.infinite, limit)
    yield counter
    counter.infinite = orig

async def main():
    with throttler(5, Counter()) as counter:
        async for x in counter.infinite():
            print('res: ', x)

if __name__ == "__main__":
    asyncio.run(main())

Для вашего случая это означает исправление всех соответствующих методов ClientSession (методы http только наверное). Не уверен, что лучше, хотя.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...