There are things you cannot do with classes.
Metaclasses let you do these things!
Inherited docstrings aren't particularly informative:
class A(object):
def my_func(self):
"""Do some stuff for class A."""
pass
class B(A):
pass
print A().my_func.__doc__
print B().my_func.__doc__ # this is doing stuff for class B!
Do some stuff for class A. Do some stuff for class A.
The nose
testing framework will print out the docstrings of test methods as it runs them.
Unfortunately, if you have a test suite class that inherits from another class, you won't be able to tell when it's running methods from the parent class vs. the subclass.
Just manually include information in the docstrings:
class A(object):
def my_func(self):
"""A: Do some stuff."""
pass
class B(A):
def my_func(self):
"""B: Do some stuff."""
super(B, self).my_func()
print A().my_func.__doc__
print B().my_func.__doc__
A: Do some stuff. B: Do some stuff.
But, that's a lot of work if you have many subclasses and/or many methods.
"Aha!", one might say. "I will just edit the docstrings in the __init__
of the superclass!"
class A(object):
def __init__(self):
old_doc = self.my_func.__doc__
cls_name = type(self).__name__
self.my_func.__doc__ = "%s: %s" % (cls_name, old_doc)
def my_func(self):
"""Do some stuff."""
class B(A):
pass
Unfortunately, method docstrings aren't writable:
print A().my_func.__doc__
print B().my_func.__doc__
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-4-ddf68abe1a40> in <module>() ----> 1 print A().my_func.__doc__ 2 print B().my_func.__doc__ <ipython-input-3-d4d1b624ac26> in __init__(self) 3 old_doc = self.my_func.__doc__ 4 cls_name = type(self).__name__ ----> 5 self.my_func.__doc__ = "%s: %s" % (cls_name, old_doc) 6 7 def my_func(self): AttributeError: attribute '__doc__' of 'instancemethod' objects is not writable
Note: Function docstrings, in general, are writable -- it's just method docstrings that aren't.
So, is there any way to change the function's docstring before it becomes a method?
A class is a special kind of object which creates new objects called instances.
A class is kind of like a form (e.g., tax form 1040).
An instance is kind of like your specific copy of the form.
type
will tell us the class of an instance:
class A(object):
def my_func(self):
"""Do some stuff."""
pass
a_inst = A()
print "Instance `a_inst` has type:", type(a_inst).__name__
Instance `a_inst` has type: A
Remember: everything in Python is an object!
So, classes have types, too:
print "Class `A` has type:", type(A).__name__
Class `A` has type: type
In other words, classes are generated by a special type called type
.
(Yes, the terminology is a bit confusing.)
type
The type
object actually does a few different things:
This is the type of class declaration you're used to:
class A(object):
def my_func(self):
"""Do some stuff."""
pass
But we can also use the type
type to create new classes on demand:
def my_func(self):
"""Do some stuff."""
pass
A_name = 'A'
A_parents = (object,)
A_methods = {'my_func': my_func}
A = type(A_name, A_parents, A_methods)
Let's try creating our new class programmatically.
This way, we can modify the function's docstring before it becomes a method:
def my_func(self):
"""Do some stuff."""
pass
def make_class(name, parents, methods):
"""Create a new class and prefix its method's docstrings to
include the class name."""
for f in methods:
methods[f].__doc__ = "%s: %s" % (name, methods[f].__doc__)
cls = type(name, parents, methods)
return cls
A = make_class('A', (object,), {'my_func': my_func})
print A().my_func.__doc__
B = make_class('B', (A,), {'my_func': my_func})
print B().my_func.__doc__
A: Do some stuff. B: A: Do some stuff.
Oops, that wasn't what we wanted! What happened?
What happened was that we modified the docstring of the same object (function) in memory.
Rather than having two separate functions in A
and B
, they point to the same function:
print A.my_func.__func__ is B.my_func.__func__
print my_func.__doc__
True B: A: Do some stuff.
Luckily, we can programmatically create functions using the function
type, too!
def my_func(self):
"""Do some stuff."""
pass
def copy_function(f):
"""Create a new function in memory that is a duplicate of `f`."""
func_type = type(f)
new_func = func_type(
f.func_code, # bytecode
f.func_globals, # global namespace
f.func_name, # function name
f.func_defaults, # default keyword argument values
f.func_closure) # closure variables
new_func.__doc__ = f.__doc__
return new_func
my_new_func = copy_function(my_func)
my_new_func.__doc__ = "modified: %s" % my_func.__doc__
print my_func.__doc__
print my_new_func.__doc__
Do some stuff. modified: Do some stuff.
Let's update our make_class
function to copy the methods before changing their docstrings:
def my_func(self):
"""Do some stuff."""
pass
def make_class(name, parents, methods):
"""Create a new class and prefix its method's docstrings to
include the class name."""
for f in methods:
# copy the function, overwrite the docstring, and replace the old method
new_func = copy_function(methods[f])
new_func.__doc__ = "%s: %s" % (name, methods[f].__doc__)
methods[f] = new_func
cls = type(name, parents, methods)
return cls
# Now it works!
A = make_class('A', (object,), {'my_func': my_func})
B = make_class('B', (A,), {'my_func': my_func})
print A().my_func.__doc__
print B().my_func.__doc__
A: Do some stuff. B: Do some stuff.
Actually, we were! A metaclass is any callable that takes parameters for:
The type
type we were using before is just the default metaclass.
The function make_class
is technically a metaclass, too!
However, Python creates classes in a slightly more complex way than we were creating classes.
We need to modify our make_class
function to ignore other class attributes (e.g. non-functions):
def make_class(name, parents, attrs):
"""Create a new class and prefix its method's docstrings to
include the class name."""
for a in attrs:
# skip special methods and non-functions
if a.startswith("__") or not hasattr(attrs[a], "__call__"):
continue
# copy the function, overwrite the docstring, and replace the old method
new_func = copy_function(attrs[a])
new_func.__doc__ = "%s: %s" % (name, attrs[a].__doc__)
attrs[a] = new_func
cls = type(name, parents, attrs)
return cls
Now, all we need is a little special "syntactic sugar" in our class definition, and it works!
class A(object):
__metaclass__ = make_class
def my_func(self):
"""Do some stuff."""
pass
print A().my_func.__doc__
A: Do some stuff.
Note that this __metaclass__
syntax applies to Python 2.7. The syntax is slightly different for Python 3.
Metaclasses intervene on class (not instance) creation.
This gives us an opportunity to modify the class's methods before the class is actually created:
Subclasses still won't actually rewrite the docstring correctly:
class A(object):
__metaclass__ = make_class
def my_func(self):
"""Do some stuff."""
pass
class B(A):
pass
print A().my_func.__doc__
print B().my_func.__doc__
A: Do some stuff. A: Do some stuff.
This is because my_func
is not passed in as an attribute of B
(it is already an attribute of A
).
To really make this work, you have to go through all the attributes of all the parent classes and copy them, too.
My blog post (link) goes into this in more detail and includes the full code.
Django uses metaclasses to simplify its interface:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
p = Person(name='Jess', age='24')
print(p.age) # this gives an int, not an IntegerField!
(Source: Classes as objects on StackOverflow)
Metaclasses can make code incredibly difficult to understand.
Only use them when you really need them!
In the words of Tim Peters:
Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).
... unless you're like me, and you enjoy learning about obscure parts of Python.
But really, if you're writing code for anything anyone else will ever use, this is good advice.
Details are available on my website, http://www.jesshamrick.com/ (see also the first reference below). I'll be posting these slides for reference, too.
This presentation was created with the lovely IPython Notebook, using the ipython nbconvert
subcommand to convert the notebook into reveal.js slides.