This workshop explores metaprogramming in a Python context, specifically metaclasses. Metaclasses are a powerful way to greatly reduce the amount of boilerplate code in object oriented programming. With careful use and documentation, programs become easier to understand and more maintainable.
Although this is broadly applicable to both Python 2 and Python 3, the specifics of syntax (and examples) are geared towards Python 3.
- Object Lifecycle -
__new__vs.__init__ - Classes as Callables -
__call__vs.class: - Class as Type Wrapper -
type()vs.class: - Class Templates -
metaclass=
Metaprogramming is a fancy word for code that generates code. On a basic level, functional programming is already a form of metaprogramming - for example, functions that return new functions. Programming languages with true macro systems (where arguments are unevaluated symbols - almost like passing in snippets of code as strings) are a more advanced form of the same.
All of this provides for much smaller code due to increased code reuse. Composability is key to increasing code reuse, and the ability to generate functions out of other functions (a core feature of functional programming) is key to that.
Python has functional programming features - limited lambdas, etc. However, the word metaprogramming in the context of Python is strongly correlated with a feature limited to a few languages - metaclasses.
Metaclasses are templates for classes. Just as objects are instances of classes, classes are instances of metaclasses.
Because we are dealing with classes, we are dealing with units of code that create objects. Therefore, we must understand object creation at a basic level.
When an object is created, __new__ is first called, followed by __init__.
__new__is called to actually allocate memory for the new object.__init__is called to initialize the memory - i.e. set the object up with initial attributes.
This means that you can override __new__ in custom classes, to control things such as:
- Memory allocation - return an existing object instead of allocating a new one at all.
- Immutability - set the attributes of an object where the class does not allow the attributes to change.
See the 01_object_lifecycle Python 3 file.
We are demonstrating the Flyweight object-oriented design pattern.
- When
__new__is run, it has noself. That object does not exist. (It's__new__'s job to create it.) __new__gets a reference to the class instead. This is to allowsuper(), which gets the base class, which you would call__new__on to create the object.__new__returns the object, as opposed to__init__which is not allowed to return anything.
Syntactically, classes are used as if they were functions, which internally they are! For example, objects are constructed like:
my_object = MyClass(init_arg_1, init_arg_2)
Note the parentheses after the class. That's just like a function call, except the function name is CamelCase!
See the 02_classes_as_callables Python 3 file.
- See
def Bookshelf(). It's a function that returns an object. - The function goes through the same sequence that
class:goes through:__new__and__init__- Make the
add_book()function accessible. (This is the syntactic sugar part.)
- The one difference is that
add_book()is a different copy for every object created by the function version. In the class, it's a single function that gets shared. - Note how JavaScript-y the function version is.
Underneath the hood, defining class is just calling type to instantiate the class. You can define a class in the same way by calling type() directly and defining the members of that class as arguments.
See the 03_class_vs_type_wrapper Python 3 file.
- Note how the object created with
BookshelfAsTyperesponds properly totype().
Just as classes are templates for objects, metaclasses are templates for classes. They can modify the definition of a class before it is allocated, as well as initialize the attributes of the class after.
See the 04_class_templates Python 3 file.
- Ultimately, the base class of all metaclasses is
type. Therefore, the arguments in theclassheader are the same as the arguments to thetype()function. - In general, use
__new__to define functions, and__init__to define class variables. - In Python 3, metaclasses are assigned as a keyword argument in the
classheader.