Quick Summary
I’ve got a simple Python helper class, in the talljosh package, that lets you write things that behave as functions but can be extended by subclassing.
But Why?
Consider the following Python function, found in json/encoder.py in the standard library:
1 2 3 4 5 6 7 |
def encode_basestring(s): """Return a JSON representation of a Python string """ def replace(match): return ESCAPE_DCT[match.group(0)] return '"' + ESCAPE.sub(replace, s) + '"' |
Nested functions can be very useful for simple examples like this, but they don’t allow much flexibility. I can’t come along and say “I want to do exactly the same, but with a different dictionary for escaping. If the code author wanted to allow that, he’d have to do something like this:
1 2 3 4 5 6 7 |
def encode_basestring(s, escape_dct=ESCAPE_DCT, escape=ESCAPE): """Return a JSON representation of a Python string """ def replace(match): return escape_dct[match.group(0)] return '"' + escape.sub(replace, s) + '"' |
But you can’t expect coders to parameterise every function in this way. It makes the code noisy and makes it hard to understand what’s typical usage.
One of the principles of Python programming is that those using your code are consenting adults. They don’t need to be treated like children. And therefore in Python you never worry about controlling access to methods or functions—you assume your user is grown-up enough to know what they’re doing, even if it’s not what you intended them to do.
Unfortunately, nested functions are hard to introspect, and very hard to extend.
What Then?
I’ve written a base class called Function which helps alleviate this problem.
1 2 3 4 5 6 7 8 9 10 11 |
import talljosh class encode_basestring(talljosh.Function): ESCAPE = json.encoder.ESCAPE ESCAPE_DCT = json.encoder.ESCAPE_DCT def run(self, s): return '"' + self.ESCAPE.sub(self.replace, s) + '"' def replace(self, match): return self.ESCAPE_DCT[match.group(0)] |
To the casual user, this function behaves exactly the same as the original: you call it in the same way. Calling it will create an instance, and call the run()
method. But it has the advantage of being extensible. I can subclass it and override ESCAPE
, or ESCAPE_DCT
, or even override replace()
.
How Does it Work?
The Function base class is very simple. (I stripped out the 22 lines of docstring for this post.)
1 2 3 4 |
class Function(object): __metaclass__ = FunctionMeta def __new__(cls, *args, **kwargs): return object.__new__(cls).run(*args, **kwargs) |
By defining the __new__ method in this way, Function
subclasses behave like normal functions when called. This means that users don’t have to type encode_basestring().run(s)
, but can simply call encode_basestring(s)
and get the result.
The metaclass definition is only necessary so that you can use Function
subclasses as methods. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MyClass(object): def eggs(self, x): return self.ham(x) + 3 class ham(talljosh.Function): # This Function behaves as a method. def run(self, my_class, x): # self is a ham instance # my_class is a MyClass instance return self.process(x + 1) def process(self, my_class, y): return 2 * y |
Notice that the methods inside the Function
receive the running Function
as self
, and the enclosing MyClass
instance as their second parameter.
In order for this to work, the metaclass looks like this:
1 2 3 4 5 6 7 |
import functools class FunctionMeta(type): def __get__(self, instance, class_): if instance is None: return self return functools.partial(self, instance) |
This is descriptor magic, and is a topic for another day.
Where can I get it?
The talljosh package is licensed under version 2 of the GPL. If you accept the license, you can get it from the Python Package Index.
One Response to Subclass ’em Functions