Skip to content

Instantly share code, notes, and snippets.

@buchanae
Last active December 30, 2015 20:39
Show Gist options
  • Save buchanae/7882317 to your computer and use it in GitHub Desktop.
Save buchanae/7882317 to your computer and use it in GitHub Desktop.
Experimental alternate initializer pattern in Python
from collections import namedtuple
import functools
import unittest
# The following example is contrived and over-simplified.
class Vehicle(object):
def __init__(self, name, passengers):
self.name = name
self.passengers = passengers
# This is how it's normally recommended to do alternate constructors.
@classmethod
def from_blueprint(cls, blueprint):
'''
Given a Blueprint instance, return a Vehicle instance.
This is an alternate constructor. It ta
'''
name = blueprint.name
# Pretend that getting "passengers" from "blueprint" is non-trivial
# and it's code you would really like to be reused.
passengers = blueprint.passengers
# and/or pretend that there are 10 other attributes you need
# to get from "blueprint" and pass to Vehicle.__init__
return cls(name, passengers)
# Car is a Vehicle that adds two properties: "type" and "doors"
class Car(Vehicle):
# Typical __init__ for a subclass. Nothing new here.
def __init__(self, name, passengers, type, doors):
# "type" could be "sports car" or "sedan"
# "doors" could be "2" or "4"
# Here's your typical call to the parent class __init__
super(Car, self).__init__(name, passengers)
self.type = type
self.doors = doors
@classmethod
def from_blueprint(cls, blueprint):
type = blueprint.type
doors = blueprint.doors
# This is where you run into issues.
# Call the parent constructor so that you can reuse its code.
car = super(Car, cls).from_blueprint(blueprint)
# OOPS! That super call will call Vehicle.from_blueprint(Car, blueprint)
# which will end up calling Car.__init__.
# But we've changed Car.__init__'s arguments,
# and Vehicle.from_blueprint doesn't know that,
# so Vehicle.from_something becomes unusable.
# This is a pattern I'm experimenting with.
# The previous example is an alternate constructor,
# while this is more of an alternate _initializer_.
# This is the "meat" of this code.
#
# This code is not fully implemented, it's just enough to convey
# the idea of the pattern.
class alt_init(object):
'''
Make "function" an alternate initializer.
This is a descriptor that wraps "function".
class C(object):
@alt_init
def from_foo(self, arg1, arg2, ...):
...
When C.from_foo(...) is called, this descriptor will create
an instance of C using C.__new__ and pass it to C.from_foo,
which can then initialize the instance, as it would normally
in __init__.
'''
def __init__(self, function):
self.function = function
def __get__(self, instance, owner):
# If this is being called on a class (not an instance)
# create an instance of the class.
if not instance:
instance = owner.__new__(owner)
# Return a wrapped function that passes the instance to self.function
# as the first argument.
@functools.wraps(self.function)
def wrapped(*args, **kwargs):
self.function(instance, *args, **kwargs)
return instance
return wrapped
# This is a Vehicle that uses alt_init instead of classmethod.
class AltVehicle(object):
def __init__(self, name, passengers):
self.name = name
self.passengers = passengers
# The important thing to notice here is the method signature:
# this receives an instance as the first argument, instead of a class.
@alt_init
def from_blueprint(self, blueprint):
'''
Given a Blueprint instance, initialize this AltVehicle instance.
'''
name = blueprint.name
# Pretend that getting "passengers" from "blueprint" is non-trivial
# and it's code you would really like to be reused.
passengers = blueprint.passengers
# and/or pretend that there are 10 other attributes you need
# to get from "blueprint" and pass to AltVehicle.__init__.
# I'm leaving that out for brevity.
# Notice that here we're not calling cls(arg1, arg2, ...)
# and we're not returning anything.
AltVehicle.__init__(self, name, passengers)
class AltCar(AltVehicle):
# Again, typical __init__, nothing new
def __init__(self, name, passengers, type, doors):
super(AltCar, self).__init__(name, passengers)
self.type = type
self.doors = doors
@alt_init
def from_blueprint(self, blueprint):
self.type = blueprint.type
self.doors = blueprint.doors
# In this pattern, it's easier to call the parent's
# alternate initializer, AltVehicle.from_blueprint
super(AltCar, self).from_blueprint(blueprint)
# Downsides:
# - there's more duplication between __init__ and alternate initializers
class TestCase(unittest.TestCase):
def setUp(self):
self.Blueprint = namedtuple('Blueprint', 'name passengers type doors')
self.blueprint = self.Blueprint('name', 2, 'car', 4)
def test_Vehicle(self):
# All is well so far
a = Vehicle('name', 2)
self.assertEqual(a.name, 'name')
self.assertEqual(a.passengers, 2)
b = Vehicle.from_blueprint(self.blueprint)
self.assertEqual(b.name, 'name')
self.assertEqual(b.passengers, 2)
def test_Car(self):
a = Car('name', 2, 'car', 4)
self.assertEqual(a.name, 'name')
self.assertEqual(a.passengers, 2)
self.assertEqual(a.type, 'car')
self.assertEqual(a.doors, 4)
with self.assertRaises(TypeError):
Car.from_blueprint(self.blueprint)
def test_AltVehicle(self):
a = AltVehicle('name', 2)
self.assertEqual(a.name, 'name')
self.assertEqual(a.passengers, 2)
b = AltVehicle.from_blueprint(self.blueprint)
self.assertEqual(b.name, 'name')
self.assertEqual(b.passengers, 2)
def test_AltCar(self):
a = AltCar('name', 2, 'car', 4)
self.assertEqual(a.name, 'name')
self.assertEqual(a.passengers, 2)
self.assertEqual(a.type, 'car')
self.assertEqual(a.doors, 4)
b = AltCar.from_blueprint(self.blueprint)
self.assertEqual(b.name, 'name')
self.assertEqual(b.passengers, 2)
self.assertEqual(b.type, 'car')
self.assertEqual(b.doors, 4)
if __name__ == '__main__':
unittest.main()
@buchanae
Copy link
Author

A downside is that subclasses must also import and use the alt_init decorator when overriding an alt_init from the parent class. It would be nice if this wasn't necessary. Possibly a metaclass would be better than a decorator.

@buchanae
Copy link
Author

Or a class decorator even?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment