Обтекание объектов для расширения / добавления функциональности при работе с isinstance - PullRequest
5 голосов
/ 24 февраля 2009

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

Одна проблема, с которой я столкнулся, заключается в том, что когда мне приходится взаимодействовать с кодом, который использует isinstance anti-pattern , этот шаблон завершается ошибкой, поскольку объект удержания / переноса не проходит тест isinstance. Как настроить объект удержания / обтекания, чтобы обойти ненужную проверку типов? Можно ли сделать это в общем? В некотором смысле мне нужно что-то для экземпляров классов, аналогичное декораторам с сохранением сигнатур (например, simple_decorator или декоратор Микеле Симионато ).

Квалификация: я не утверждаю, что все isinstance использование неуместно; несколько ответов дают хорошие замечания по этому поводу. Тем не менее, следует признать, что использование isinstance накладывает существенные ограничения на взаимодействие объектов - оно заставляет наследование быть источником полиморфизма, а не поведения .

Кажется, есть некоторая путаница относительно того, как именно / почему это проблема, поэтому позвольте мне привести простой пример (в широком смысле взято из pycontainer ). Допустим, у нас есть класс Foo, а также FooFactory. Для примера предположим, что мы хотим иметь возможность создавать экземпляры объектов Foo, которые регистрируют каждый вызов функции, или не думают об AOP. Кроме того, мы хотим сделать это без какого-либо изменения класса / источника Foo (например, на самом деле мы можем реализовывать универсальную фабрику, которая может добавить возможность регистрации в любой экземпляр класса на лету). Первый удар в этом может быть:

class Foo(object):
    def bar():
        print 'We\'re out of Red Leicester.'

class LogWrapped(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
    def __getattr__(self, name):
        attr = getattr(self.wrapped, name)
        if not callable(attr):
            return attr
        else:
            def fun(*args, **kwargs):
                print 'Calling ', name
                attr(*args, **kwargs)
                print 'Called ', name
            return fun

class FooFactory(object):
    def get_foo(with_logging = False):
        if not with_logging:
            return Foo()
        else:
            return LogWrapped(Foo())

foo_fact = FooFactory()
my_foo = foo_fact.get_foo(True)
isinstance(my_foo, Foo) # False!

Могут быть причины, по которым вы можете захотеть делать все именно так (использовать декораторы и т. Д.), Но имейте в виду:

  • Мы не хотим трогать класс Foo. Предположим, мы пишем фреймворковый код, который может использоваться клиентами, о которых мы пока не знаем.
  • Смысл в том, чтобы вернуть объект, который по сути является Foo, но с добавленной функциональностью. Это должно выглядеть как Foo - насколько это возможно - для любого другого клиентского кода, ожидающего Foo. Отсюда и желание обойти isinstance.
  • Да, я знаю, что мне не нужен фабричный класс (упреждающая защита здесь).

Ответы [ 5 ]

2 голосов
/ 24 февраля 2009

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

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

Некоторые грязные опции, если по какой-то причине вы действительно не хотите использовать наследование:

  • Вы можете изменять только экземпляры классов, используя методы экземпляров. С new.instancemethod вы создаете методы-оболочки для вашего экземпляра, который затем вызывает оригинальный метод, определенный в исходном классе. Кажется, это единственная опция, которая не изменяет исходный класс и не определяет новые классы.

Если вы можете изменить класс во время выполнения, есть много опций:

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

  • Параметры в ответе Дейва (декораторы классов в Python> = 2.6 или метаклассы).

Edit: для вашего конкретного примера, я думаю, работает только первый вариант. Но я все же рассмотрел бы альтернативу создания LogFoo или выбора совершенно другого решения для чего-то конкретного, например ведения журнала.

1 голос
/ 24 февраля 2009

Следует помнить, что вам не обязательно использовать что-либо в базовом классе, если вы идете по пути наследования. Вы можете создать класс-заглушку для наследования, который не добавляет никакой конкретной реализации. Я делал что-то подобное несколько раз:

class Message(object):
     pass

class ClassToBeWrapped(object):
    #...

class MessageWithConcreteImplementation(Message):
    def __init__(self):
        self.x = ClassToBeWrapped()
    #... add concrete implementation here

x = MessageWithConcreteImplementation()
isinstance(x, Message)

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

Одна проблема, с которой я столкнулся, заключается в том, что когда мне приходится взаимодействовать с кодом, который использует анти-шаблон isinstance

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

0 голосов
/ 24 февраля 2009

Если вы пишете фреймворк, который должен принимать какие-то входные данные от ваших пользователей API, то я не вижу причин использовать isinstance. Ужасно, но я всегда проверяю, действительно ли он предоставляет интерфейс, который я собираюсь использовать:

def foo(bar):
    if hasattr(bar, "baz") and hasattr(bar, "quux"):
        twiddle(bar.baz, bar.quux())
    elif hasattr(bar, "quuux"):
        etc...

И я также часто предоставляю хороший класс для наследования функциональности по умолчанию, если пользователь API хочет использовать его:

class Bar:
    def baz(self):
        return self.quux("glu")

    def quux(self):
        raise NotImplemented
0 голосов
/ 24 февраля 2009

Моим первым побуждением было бы попытаться исправить код, который использует isinstance. В противном случае вы просто распространяете ошибки своего дизайна в свой собственный дизайн. По какой-то причине вы не можете его изменить?

Edit: Итак, вы оправдываете, что вы пишете фреймворк / библиотечный код, который вы хотите, чтобы люди могли использовать во всех случаях, даже если они хотят использовать isinstance?

Я думаю, что с этим несколько вещей не так:

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

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

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