Python: расширение класса / объекта, возвращенного из стороннего пакета, который использует фабричный метод - PullRequest
0 голосов
/ 03 марта 2020

Я пытаюсь определить наилучший подход, чтобы обернуть сторонний класс в другой класс, чтобы я мог обеспечить согласованный внутренний API. Сторонний класс создается с использованием довольно сложного фабричного метода и поэтому никогда не создается напрямую. Обертываемые другие классы создаются напрямую.

В обычных случаях я просто наследую от класса, а затем расширяю при необходимости ie.

class MyCustomClassWithStandardAPI(StandardThirdPartyClass):
    def custom_method_that_defines_api(self):
        return super().third_party_method_being_wrapped()

Но в этом случае, потому что объект должен быть инициализирован с помощью стороннего фабричного метода, я не могу обернуть класс, если не считать дублирования самого фабричного метода (который является длинным и поэтому представляет риск, если фабрика изменится в будущем).

class MyCustomClassWithStandardAPI(ThirdPartyClassThatIsCreatedByFactoryMethod):
    def custom_method_that_defines_api(self):
        ....

def complicated_third_party_factory_method():
    ...
    ...
    returns ThirdPartyClassThatIsCreatedByFactoryMethod(a, b, c, d, ...)

Поскольку класс создается методом фабрики, я никогда не смогу прервать создание экземпляра стороннего класса и заменить его собственным подклассом.

Любые мысли о том, как этого добиться в Python

Ответы [ 2 ]

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

Я бы использовал здесь duck, набрав , потому что Python облегчает задачу. Скрытый принцип заключается в том, что ваш собственный класс просто содержит подобъект обязательного и сложного класса и прозрачно передает все неизвестные ему запросы к этому объекту.

Единственная проблема заключается в том, что некоторый код использует isinstance или issubclass, он сломается.

Если этого не требуется, getattr может быть достаточно:

class MyCustomClassWithStandardAPI:

    def __init__(self, thirdParty):             # store the "inherited" object at initialization time
        self.inner = thirdParty

    # define additional methods
    def custom_method_that_defines_api(self):
        ....

    # delegate everything else to the "inherited" object
    def __getattr__(self, attr):
        return getattr(self.inner, attr)

Вы используете его таким образом:

obj = MyCustomClassWithStandardAPI(complicated_third_party_factory_method())
obj.custom_method_that_defines_api()     # Ok, uses the specialized method
obj.third_party_method()                 # Ok delegates to the inner object
1 голос
/ 03 марта 2020

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

Вы будете лучше создавая ассоциацию с одним экземпляром объекта в этой другой иерархии, и поддерживайте чистый и унифицированный интерфейс в соответствии с вашими потребностями.

То есть:


class MyCustomBase:
    def __init__(self, [enough parameters to instantiate the 3rdy party thing you need]):
          self.api = third_party_factory(parameters)
          ...
    ...
    def stantard_comunicate(self, data):
         if isinstance(self, api, TypeOne):
               data = transform_data_for_type_one(data)
               self.api.comunicate_one(data)    
         elif isinstance(self, api, TypeTwo)
               data = transform_data_for_type_two(data)
               self.api.comunicate_two(data) 
         ...

Если в 3-й партии lib есть много методов и атрибутов, большинство из которых вам не нужно настраивать, вам не нужно записывать, а затем записывать либо один за другим - вы можете настроить доступ к атрибуту в своем классе-обертке, чтобы напрямую переходить к атрибуту / методу с помощью простого метода __getattr__. Внутри вашего класса выше добавьте:

    def __getattr__(self, attr):
        return getattr(self.api, attr)

Без какого-либо наследования.

Если вам нужно наследование ...

Иногда вам понадобится ваш прокси-объект, чтобы "быть" из другой вид, например, при передаче ваших собственных экземпляров в методы или классы, предоставляемые другой библиотекой. Затем вы можете форсировать динамическое наследование c, не вмешиваясь в метод фабрики другой стороны, динамически создавая класс с гораздо более простым фабричным методом на вашей стороне:


def myfactory(...):

     instance = thirdy_party_factory(...)

     class MyClass(type(instance)):
         def __init__(self, ...):
             super().__init__(...)
             self.my_init_code()

         def my_init_code(self):
             # whatver you need to initialize the attributes
             #   you use go here
         ...
         # other methods you may want to customize can make use
         # of "super()" normally

     # Convert "other_instance" to be of type "my_instance":
     instance.__class__ = MyClass
     return instance

(Если в таком случае будет создано много таких экземпляров в долгоживущий процесс, тогда вы должны кэшировать динамически созданные классы. Более простой способ - просто перенести фабрику классов на еще более простой фабричный метод, который принимает родительский класс в качестве параметра, и использовать встроенный в Python lru_cache. «за это»:

from functools import lru_cache


@lru_cache()
der inner_factory(Base):

     class MyClass(type(instance)):
         def __init__(self, ...):
             super().__init__(...)
             self.my_init_code()

         def my_init_code(self):
             # whatver you need to initialize the attributes
             #   you use go here
         ...
         # other methods you may want to customize can make use
         # of "super()" normally
    return MyClass



def myfactory(...):

     instance = thirdy_party_factory(...)
     MyClass = inner_factory(type(instance))
     # Convert "other_instance" to be of type "my_instance":
     instance.__class__ = MyClass
     return instance

Patching Monkey

Приведенные выше примеры касаются «только вашей стороны» истории - но в зависимости от того, как вы используете вещи и от характера другой библиотеке, возможно, будет проще «обезьянить патч», то есть: заменить в пространстве имен метода фабрики 3-й стороны базовые классы, которые он намеревается использовать вашими производными классами, так, чтобы ваши отобранные классы были используется фабрикой библиотеки.

Исправление обезьян в Python это просто вопрос назначения объектов - но в стандартной библиотеке есть вызываемый элемент unittest.mock.patch, который предлагает утилиту, которая не только выполняет исправление, но и заботится об очистке, восстанавливая исходные значения после окончания использования исправления.

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

Но по существу:



from unittest.mock import patch
from thirdparty.bases import ThirdyBase
from thirdyparty.factories import factory

class MyStandardBase(ThirdyBase):
    def implement_standard_api(self, ...):
        ...


def mycode():
    # Bellow, the last item on the dotted path is the name
    # of the base class _inside_ the module that defines the
    # factory function.
    with patch("thirdyparty.factories.ThirdyBase", MyStandardBase):
        myinstance = factory(...)

...