Extends: a collaborative open-classes library
To start using extends, you simply need to import it:
>>> import extends
But before we delve any further into Extends, answering a basic question might be in order:
What is an open class?
Several languages have so-called "open" classes (or variations thereof). Among those languages are Smalltalk or Ruby, and in some ways C#.
An open class is simply a class to which any user can add content (methods, fields, ...) in-place without having to edit the original source code. By in-place we mean that any user of the original class (within the scope of the opening) will see the new methods, fields and behaviors.
Why add open classes?
The main purposes here is to serve as the basics for a flexible plugins framework: while most systems with plugins only provide a specific set of extension points or behavior customization points (for existing objects of the system), using open classes lets any plugin creator alter any behavior of the base package.
Because extends is cooperative, it is not quite that flexible: only the classes "opted into" being open by the base package creators will be modifiable at will.
Still, by avoiding the creation of a complex plugin system (extending classes simply have to be loaded by Python), extends tries to simplify the work of both the original system's creators and plugin writers: both just have to write regular Python classes.
Creating an open class
The first task when using extends is to opt a class in extends's open class policy. This is done by simply inheriting from extends's Base class:
>>> class Example1(extends.Base): ... pass ... >>> isinstance(Example1(), Example1) True
The deed is done, from now onwards Example1 and all of its subclasses will be "open" as far as extends is concerned.
While extends tries to avoid changing the user's habit, and pretty much any normal Python usage (including multiple inheritance or adding a metaclass or two) should be possible, __new__ should only be overridden with great care: extends uses it to return arbitrary instances built on-the-fly. You probably want to avoid overriding __new__ if possible, just in case.
Re-opening the original class
"Re-opening" is a term often used for "extending a class in-place". Consider a class is a box with stuff in it, originally the class is created, methods and attributes are put in the box and the box is then closed to be used.
Extending the class in-place means re-opening the box and messing with the existing content of the class.
This is what we're going to do here.
Re-opening a class is almost as simple as opting a class in extends, the class writer simply needs to inherit from the class he wants to extend and from extend's Extend class:
>>> class BasicExtender(extends.Extend, Example1): ... def exists(self): ... return True
As you probably noted, Example1 didn't originally have a foo method:
>>> o1 = Example1() >>> isinstance(o1, Example1) True >>> o1.exists() True
At this point in time, extends.Extend has to precede the extended class or Python will not be able to create consistent resolution orders.
You will notice quite fast if you mix them up as Python will generate a TypeError.
It is also perfectly possible to inherit from third-party classes, this works as a mixin in that the resulting extension will be the combination of the inherited class and the extension one:
>>> class Arbitrary(object): ... def random_method(self): ... pass ... >>> class InherintingExtend(extends.Extend, Example1, Arbitrary): ... pass ... >>> o3 = Example1() >>> o3.random_method()
while the arbitrarily-inherited class can be at any position (technically), putting them after both the extension marker and the extended class is probably a good idea for readability and simplicity's sake.
While inheriting from arbitrary classes is possible, the warning about __new__ stays valid: it's a bad idea even if it's transitive.
Extending two different base classes at the same time is not supported, doing so will result in an undefined (but probably very bad) result.
That's basically it for the introductory usage, you are now able to use extends, and it should generally behave as you'd expect.
For more insight as to the precise behaviors of extends and its advanced usages, keep on reading.
Extending a light in the darkness
Hero: Extends Resolution Order
One of the important points of extends is to understand the order in which methods are resolved and executed between the base class and the various extends.
Overall, extends behaves as you would expect: on call, methods execute in reverse order from loading and the latest-loaded extends are executed first.
>>> class HERO(extends.Base): ... def shout(self): ... print 'Elder God' ... >>> class Heracles(extends.Extend, HERO): ... def shout(self): ... print 'First hero' ... super(Heracles, self).shout() ... >>> class Theseus(extends.Extend, HERO): ... def shout(self): ... print 'Second hero' ... super(Theseus, self).shout() ... >>> class Asclepius(extends.Extend, HERO): ... def shout(self): ... print 'Third hero' ... super(Asclepius, self).shout() ... >>> HERO().shout() Third hero Second hero First hero Elder God
As you can see, extends uses Python's own object system for its management. Therefore the various rules of cooperative inheritance in Python (including calling super) hold.
You may want to not call super if your goal is to completely replace a base class's method, but be aware that this can break third-party extensions, if not the base object itself.
Extends and regular inheritance
One of the patterns in object-oriented languages such as Python is the building of object hierarchies (via inheritance) as means of code reuse and sharing of interface and implementation.
It would of course be expected that extends deals correctly with this case, and that it behaves sensibly.
In an inheritance hierarchy involving extends-based objects, everything simply behaves as if the extends-based objects were replaced by their extended versions. Nothing more, nothing less, and the original MRO should still be valid (except it will have new components interspersed).
For instance if we define a basic hierarchy:
>>> class Root(extends.Base): ... def is_a(self): ... return False ... def is_b(self): ... return False >>> class A(Root): ... def is_a(self): ... return True >>> class B(Root): ... def is_b(self): ... return True >>> map(lambda v: v.is_a(), [Root(), A(), B()]) [False, True, False] >>> map(lambda v: v.is_b(), [Root(), A(), B()]) [False, False, True]
We can extend the leaves as normal:
>>> class BecomeB(extends.Extend, A): ... def is_b(self): ... return True >>> A().is_b() True
But we can also extend classes higher up the stack, and have its descendants behave as expected, getting their ancestor's extension:
>>> class RootIsRootChild(extends.Extend, Root): ... def is_root_child(self): ... return True >>> A().is_root_child() and B().is_root_child() True
The original classes are not modified at any point, so we can check their MRO:
>>> A.mro() == [A, Root, extends.Base, object] True
and that its orderings hold:
>>> mro = type(A()).mro() >>> mro.index(A) < mro.index(Root) True >>> mro.index(Root) < mro.index(extends.Base) True
Advanced topics in extends: Configuring extends behavior
By default, extends considers all the currently loaded classes for inclusions into the extension scheme. But whether you're the system's original writer or one of its user, it might not be what you want, and you could need to accept or reject extensions to original types based on entirely arbitrary criteria.
To that end, extends provides a powerful but simple filtering API: you can provide a callable to any subclass of extends.Base (including extends.Base itself), and for each extender candidate found that callable will be called with the class being extended and the potential extender. If it returns False then the candidate will be ignored .
>>> class Rejector(extends.Base): ... def accepted_extension(self): ... return False >>> Rejector.extension_filter( ... lambda *args: False) >>> class Rejecteee(extends.Extend, Rejector): ... def accepted_extension(self): ... return True >>> Rejector().accepted_extension() False
Limitations of Extends
Extends does not allow for the extension of the extends.Base class: not only would that generate some trouble performance-wise, but more importantly it has too high a chance of wreaking havoc on two different libraries using Extends in the same project.
|||In most situations in Python a falsy value (empty list, None, 0, ...) suffices, but this API specifically requires False to ignore an extender.|