views:

276

answers:

5

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).
A: 
Dave Stenglein
A: 

My first impulse would be to try to fix the offending code which uses isinstance. Otherwise you're just propagating its design mistakes in to your own design. Any reason you can't modify it?

Edit: So your justification is that you're writing framework/library code that you want people to be able to use in all cases, even if they want to use isinstance?

I think there's several things wrong with this:

  • You're trying to support a broken paradigm
  • You're the one defining the library and its interfaces, it's up to the users to use it properly.
  • There's no way you can possibly anticipate all the bad programming your library users will do, so it's pretty much a futile effort to try to support bad programming practices

I think you're best off writing idiomatic, well designed code. Good code (and bad code) has a tendency to spread, so make yours an example. Hopefully it will lead to an overall increase in code quality. Going the other way will only continue the quality decline.

Kamil Kisiel
Thanks, I've edited the question accordingly.
zweiterlinde
The assumption is that isinstance is always a bad idea. There are valid times to use isinstance.
Jason Baker
+1  A: 

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.

nikow
+1  A: 

One thing to keep in mind is that you don't necessarily have to use anything in a base class if you go the inheritance route. You can make a stub class to inherit from that doesn't add any concrete implementation. I've done something like this several times:

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)

If you need to inherit from other things, I suppose you could run into some problems with multiple inheritance, but this should be fairly minimal if you don't provide any concrete implementation.

One problem that I've run into is that when I have to interface with code that uses the isinstance anti-pattern

I agree that isinstance is to be avoided if possible, but I'm not sure I'd call it an antipattern. There are some valid reasons to use isinstance. For instance, there are some message passing frameworks that use this to define messages. For example, if you get a class that inherits from Shutdown, it's time for a subsystem to shut down.

Jason Baker
A: 

If you're writing a framework that needs to accept some sort of inputs from your API users, then there's no reason I can think of to use isinstance. Ugly as it might be, I always just check to see if it actually provides the interface I mean to use:

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

And I also often provide a nice class to inherit default functionality, if the API user wants to use it:

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

    def quux(self):
        raise NotImplemented
TokenMacGuy