Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrapping objects to extend/add functionality while working around isinstance

In Python, I've seen the recommendation to use holding or wrapping to extend the functionality of an object or class, rather than inheritance. In particular, I think that Alex Martelli spoke about this in his Python Design Patterns talk. I've seen this pattern used in libraries for dependency injection, like pycontainer.

One problem that I've run into is that when I have to interface with code that uses the isinstance anti-pattern, this pattern fails because the holding/wrapping object fails the isinstance test. How can I set up the holding/wrapping object to get around unnecessary type checking? Can this be done generically? In some sense, I need something for class instances analogous to signature-preserving function decorators (e.g., simple_decorator or Michele Simionato's decorator).

A qualification: I'm not asserting that all isinstance usage is inappropriate; several answers make good points about this. That said, it should be recognized that isinstance usage poses significant limitations on object interactions---it forces inheritance to be the source of polymorphism, rather than behavior.

There seems to be some confusion about exactly how/why this is a problem, so let me provide a simple example (broadly lifted from pycontainer). Let's say we have a class Foo, as well as a FooFactory. For the sake of the example, assume we want to be able to instantiate Foo objects that log every function call, or don't---think AOP. Further, we want to do this without modifying the Foo class/source in any way (e.g., we may actually be implementing a generic factory that can add logging ability to any class instance on the fly). A first stab at this might be:

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!

There are may reasons why you might want to do things exactly this way (use decorators instead, etc.) but keep in mind:

  • We don't want to touch the Foo class. Assume we're writing framework code that could be used by clients we don't know about yet.
  • The point is to return an object that is essentially a Foo, but with added functionality. It should appear to be a Foo---as much as possible---to any other client code expecting a Foo. Hence the desire to work around isinstance.
  • Yes, I know that I don't need the factory class (preemptively defending myself here).
like image 864
zweiterlinde Avatar asked Feb 23 '09 22:02

zweiterlinde


People also ask

What should the function below be called to set up a new object?

A function like Turtle or Point that creates a new object instance is called a constructor, and every class automatically provides a constructor function which is named the same as the class.

What is __ class __ in Python?

__class__ is an attribute on the object that refers to the class from which the object was created. a. __class__ # Output: <class 'int'> b. __class__ # Output: <class 'float'> After simple data types, let's now understand the type function and __class__ attribute with the help of a user-defined class, Human .

Which module is used to store data into Python objects with their structure?

Whatever the aim of the program is, you'll need data structures like lists and dictionaries to store them. You will always want to save the data that users enter before they close your program. The simplest way to do this is to use the JSON module to store your data.

What is Isinstance Python?

Python isinstance() Function The isinstance() function returns True if the specified object is of the specified type, otherwise False . If the type parameter is a tuple, this function will return True if the object is one of the types in the tuple.


1 Answers

If the library code you depend on uses isinstance and relies on inheritance why not follow this route? If you cannot change the library then it is probably best to stay consistend with it.

I also think that there are legitimate uses for isinstance, and with the introduction of abstract base classes in 2.6 this has been officially acknowledged. There are situations where isinstance really is the right solution, as opposed to duck typing with hasattr or using exceptions.

Some dirty options if for some reason you really don't want to use inheritance:

  • You could modify only the class instances by using instance methods. With new.instancemethod you create the wrapper methods for your instance, which then calls the original method defined in the original class. This seems to be the only option which neither modifies the original class nor defines new classes.

If you can modify the class at runtime there are many options:

  • Use a runtime mixin, i.e. just add a class to the __base__ attribute of your class. But this is more used for adding specific functionality, not for indiscriminate wrapping where you don't know what need to be wrapped.

  • The options in Dave's answer (class decorators in Python >= 2.6 or Metaclasses).

Edit: For your specific example I guess only the first option works. But I would still consider the alternative of creating a LogFoo or chosing an altogether different solution for something specific like logging.

like image 145
nikow Avatar answered Nov 06 '22 11:11

nikow