1

Say you have a bunch of factory functions, each of which does two things:

  1. Modify or add arguments to the initialization of a class
  2. Does something with the class instance afterwards

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?

2
  • Do you mean you want to create a "scary-police" dog? Then you can't without modifying these functions. I'd suggest using (multi)inheritance. So instead of functions, you'll have class PoliceDog(Dog), class ScaryDog(Dog) and class ScaryPoliceDog(ScaryDog, PoliceDog). Commented Aug 3, 2021 at 16:13
  • I'm aware of the necessity of rewriting the functions. Multiple inheritance is no solution because there are too many combinations Commented Aug 3, 2021 at 16:15

3 Answers 3

1

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

Sign up to request clarification or add additional context in comments.

1 Comment

In mod_kwargs, kwargs need to be returned. I don't know why a copy of the dictionary is created but it is so.
0

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()

Comments

0

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:

  1. The MRO for ScaryPoliceDog is PoliceDog, ScaryDog, Dog, object. The __init__ method for each will be called in turn.
  2. super() in PoliceDog does not refer to Dog; it refers to ScaryDog, because that's the class following PoliceDog in the MRO.
  3. 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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.