How do I extend a class that I never instantiate myself?

Steven D'Aprano steve at pearwood.info
Sat Oct 10 18:54:29 EDT 2015


On Sun, 11 Oct 2015 04:02 am, speeze.pearson at gmail.com wrote:

> (This is a long post, but the question is simple. Most of this is
> just me enumerating what I've already tried.)
> 
> Someone wrote a library that creates and manipulates `Node`s.
> I would like to write another layer on top of this, to make
> trees that behave just like the library's trees, but whose nodes
> have some extra methods.
> 
> "Subclass!" is the classic OO answer, and my first instinct.
> Problem: I never instantiate `Node` myself. `Node`s are created
> somewhere deep inside the library, and there's no way to tell it
> "don't use `Node`, use this class instead."

But you have trees of nodes returned?

Here are a few solutions:


# Solution 1: inject a new method into each and every instance in the tree.
# Does not work for __dunder__ methods, but should work for any class that 
# includes a per-instance __dict__ (that's most classes apart from
# built-ins).

from types import MethodType

def foo(self):
    return self.spam or self.eggs

for node in the_tree:
    node.foo = MethodType(foo, node)

# Later...
for node in the_tree:
    print node.foo()


# Solution 2: hack the node type of each instance.
# Should work fine for adding new methods, even __dunder__ methods.
# Depending on the original class, may or may not work for overriding
# existing methods.

class MyNode(Node):
    def foo(self):
        return self.spam or self.eggs

for node in the_tree:
    node.__class__ = MyNode

# Later...
for node in the_tree:
    print node.foo()


Solution #2 is especially good to test your team's internal procedures and
code review practices. Any code review which doesn't flag it as a "What the
hell are you doing??? Is this even legal???" change is clearly deficient.

Yes, it is legal, in fact it is not just legal but a deliberately supported
feature of Python. But it is a bit tricky to get the details right. As
written above, it should work, but for me to really be comfortable with
using this in production, I would want to have written the original class
specifically with this in mind.

While both of the above have their uses, really the *right* way to do this
is to stop being such an OOP purist. This isn't Java, we have functions for
a reason.

# Solution 3: just use a damn function. Works for any class, whether pure
# Python or an extension class. Doesn't require any special support in the
# original class, and guaranteed not to give your code reviewers a 
# conniption fit.

def foo(node):
    return node.spam or node.eggs

# Later...
for node in the_tree:
    print foo(node)


> Does anyone know a programming pattern that solves this problem?
> 
> (That's it! That's the question. Everything after this is
> descriptions of solutions I'm not happy enough with.)
> 
> "Write a wrapper!" is my next instinct. Basically,
> 
>   class MyNode(object):
>     ...
>     def foo(self, *args, **kwargs):
>       return self.node.foo(*args, **kwargs)
>     def bar(self, *args, **kwargs):
>       return self.node.bar(*args, **kwargs)
>     ...
> 
> This would be tedious, but acceptable, if I knew how to make
> `MyNode.parent` and `MyNode.children` return `MyNode`s
> instead of `Node`s. 

class MyNode(Node):
    @property
    def parent(self):
        parent = super(MyNode, self).parent
        if parent is not None:
            parent = MyNode(parent)
        return parent
    # etc.

Still a lot of work, and you really need to understand the original Node
class very well. This is the problem with subclassing: it requires you to
be intimately familiar with the *implementation details* of the class you
subclass.


> My last idea is that my approach is *entirely* wrong: I shouldn't
> be trying to make the tree's nodes instances of my own class --
> I should just use the existing library's `Node` class, and write
> my library in a functional style. Don't define `MyNode.foo()`,
> instead define `mylibrary.foo(my_node)`.
> I've got nothing against functional programming, but mixing it
> so closely with OO would, I think, result in a confusing,
> schizophrenic interface.

Do you find len(alist) to be confusing and schizophrenic?


> Last possibly relevant fact: the library I want to extend is
> `xml.dom.minidom`. By `Node` I mean `Element`, and the specific
> methods I want to add will modify attributes used for CSS/JavaScript.

It may be relevant. Try it and see.


-- 
Steven




More information about the Python-list mailing list