Last active
December 30, 2015 20:39
-
-
Save buchanae/7882317 to your computer and use it in GitHub Desktop.
Experimental alternate initializer pattern in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Or a class decorator even?