Reputation: 3625
Say you have a bunch of factory functions, each of which does two things:
E.g.
class Dog:
def __init__(self, **very_many_kwargs):
pass
def create_police_dog(department, **dog_kwargs):
dog_kwargs['race'] = 'pitbull_terrier'
dog = Dog(**dog_kwargs)
police_academy = PoliceAcademy()
police_academy.train(dog)
return dog
def create_scary_dog(**dog_kwargs):
dog_kwargs['teeth_size'] = 'MAX'
dog_kwargs['eye_color'] = fetch_angry_eye_colors('https://dogs.com')
dog = Dog(**dog_kwargs)
dog.experience_unhappy_childhood()
return dog
How to combine multiple of such functions in series?
Upvotes: 1
Views: 487
Reputation: 530990
Each function should accept an existing instance of Dog
and modify it, then return the modified instance. That way, you can simply compose the functions. (This is sort of a functional equivalent of the Builder pattern, though the example below is somewhat awkward and clunky.)
class Dog:
def __init__(self, **very_many_kwargs):
pass
def create_police_dog(dog, department):
dog.race = 'pitbull_terrier'
police_academy = PoliceAcademy()
police_academy.train(dog)
return dog
def create_scary_dog(dog):
dog.teeth_size = 'MAX'
dog.eye_color = fetch_angry_eye_colors('https://dogs.com')
dog.experience_unhappy_childhood()
return dog
scary_police_dog = create_scary_dog(
create_police_dog(
Dog(),
'vice'
)
)
Here is, I think, a more typical implementation of the Builder pattern.
class Dog:
...
class DogBuilder:
def __init__(self):
self.kwargs = {}
self._train = False
self._experience_unhappy_childhood = False
def build(self):
d = Dog(**self.kwargs)
if self._train:
PoliceAcademy().train(d)
if self._experience_unhappy_childhood:
d.experience_unhappy_childhood()
return d
def train(self):
self._train = True
def set_race(self, r):
self.kwargs['race'] = r
def experience_unhappy_childhood(self):
self._experience_unhappy_childhood = True
def make_police_dog(self, department):
self.kwargs['department'] = department
return self.set_race('pitbull_terrier').train()
def make_scary_dog(self):
return self.set_eye_color(fetch_angry_eye_colors('https://dogs.com')).
set_teeth_size('MAX').
experience_unhappy_childhood()
scary_police_dog = (DogBuilder()
.make_police_dog('vice')
.make_scary_dog()
.build()
Here's an approach using multiple inheritance (more specifically, cooperative multiple inheritance).
class Dog:
def __init__(self, *, race, teeth_size='Min', eye_color=None, **kwargs):
super().__init__(**kwargs)
self.race = race
self.eye_color = eye_color
self.teeth_size = teeth_size
class PoliceDog(Dog):
def __init__(self, *, department, **kwargs):
kwargs['race'] = 'pitbull_terrier'
super().__init__(**kwargs)
PoliceAcademy().train(self)
class ScaryDog(Dog):
def __init__(self, **kwargs):
kwargs['teeth_size'] = 'MAX'
kwargs['eye_color'] = fetch_angry_eye_colors('https://dogs.com')
super().__init__(**kwargs)
self.experience_unhappy_childhood()
class ScaryPoliceDog(PoliceDog, ScaryDog):
pass
d = ScaryPoliceDog(department="vice")
Unless there is something specific about a scary police dog that doesn't apply to an ordinary police dog or scary dog, nothing special needs to be done in ScaryPoliceDog
. It's all handled via delegation using super
.
In particular:
ScaryPoliceDog
is PoliceDog
, ScaryDog
, Dog
, object
. The __init__
method for each will be called in turn.super()
in PoliceDog
does not refer to Dog
; it refers to ScaryDog
, because that's the class following PoliceDog
in the MRO.ScaryPoliceDog
does not need to specify a race. Although that field is required by Dog
, it will be supplied by PoliceDog
, as PoliceDog.__init__
will be called first.Upvotes: 0
Reputation: 4449
Decorators almost work but since you want your modifications to occur both before and after instantiation, they won't chain properly. Instead define a custom system of generic modifiers that can be chained together at creation time:
from abc import ABC, abstractmethod
class DogModifier(ABC):
@abstractmethod
def mod_kwargs(self, **kwargs):
pass
@abstractmethod
def post_init(self, dog):
pass
class PoliceDog(DogModifier):
def __init__(self, department):
self._dept = department
def mod_kwargs(self, **kwargs):
kwargs['race'] = 'pitbull_terrier'
def post_init(self, dog):
PoliceAcademy(self._dept).train(dog)
class ScaryDog(DogModifier):
def mod_kwargs(self, **kwargs):
kwargs['teeth_size'] = 'MAX'
kwargs['eye_color'] = fetch_angry_eye_color('https://dogs.com')
def post_init(self, dog):
dog.experience_unhappy_childhood()
def create_dog(*modifiers, **dog_kwargs):
for m in modifiers:
m.mod_kwargs(**dog_kwargs)
dog = Dog(**dog_kwargs)
for m in modifiers:
m.post_init(dog)
return dog
# ...
police_dog = create_dog(PoliceDog('bomb squad'), kw1='a', kw2='b')
scary_dog = create_dog(ScaryDog(), kw1='x', kw2='y')
scary_police_dog = create_dog(PoliceDog('bomb squad'), ScaryDog(), kw1='z')
*code shown as example only - bugfixes left as an exercise for the reader
Upvotes: 1
Reputation: 12130
I don't think my solution is the best and would be very interested in any other solutions. But here is my idea:
class DogFactory:
def __init__(self):
self.dog_kwargs = {}
self.dog_functions = []
def scary(self):
self.dog_kwargs['teeth_size'] = 'MAX'
self.dog_kwargs['eye_color'] = fetch_angry_eye_colors('https://dogs.com')
self.dog_functions.append(Dog.experience_unhappy_childhood)
return self
def police(self):
self.dog_kwargs['race'] = 'pitbull_terrier'
police_academy = PoliceAcademy()
self.dog_functions.append(police_academy.train)
return self
def create(self):
dog = Dog(**self.dog_kwargs)
for f in self.dog_functions:
f(dog)
return dog
dog = DogFactory() \
.scary() \
.police() \
.create()
Upvotes: 0