What are the measures to call a Python code a policy-based design?
Asked Answered
C

1

4

Description

I wonder if the code I am showing can be considered as an example of policy-based design in Python. Also, I would like to know if have you seen python modules using something like this example so I can learn from them?

I wrote more details and examples about this approach in a post.

Recently I needed something like policy-based design for a python module I was working on.

I found a similar question in this forum, but it was closed, and I was not able to add a comment.

Let me give a summary of what I expect from this approach in Python.

  • Module classes are divided between policy and host classes.
  • Policy classes implement modifications to the behavior or interface of the host classes by inheritance.
  • Users instantiate a new class from a host class by providing a set of policy classes.

Here it is my toy example:

class PrintOutput:
    """Implement print output policy."""
    def _output(self, message):
        print(message)

class SaveOutput:
    """Implement save output policy."""
    def set_filename(self, filename):
        self.filename = filename
    def _output(self, message):
        with open(self.filename, 'w') as file:
            file.write(message)

def HelloWorld(Output=PrintOutput):
    """Creates a host class."""
    class _(Output):
        """Implements class affected by the policy."""
        def run(self):
            """Print message."""
            self._output('Hello world!')
    return _

PrintHelloWorld = HelloWorld()
hw = PrintHelloWorld()
hw.run() # print "Hello World!"

SaveHelloWorld = HelloWorld(
    Output=SaveOutput
)
hw = SaveHelloWorld()
hw.set_filename('output.txt')
hw.run() # save "Hello World!" in output.txt

In the example, I expect that all the classes are already defined in a module. The user only needs to instantiate the HelloWorld class with the output policy that fits his or her requirements.

Design ingredients

The basic design ingredients are

  • Multiple inheritances: that exists in both C++ and Python.

  • Postpone inheritance: postponing the definition of the inheritance between host and policy classes until user instantiation of the host class

// in C++ using templates
template<typename Policy1, typename Policy2, ...>
class HostClass: public Policy1, public Policy2, ... {
    // Implement host class.
};
# in Python with a factory with arguments
def HostClass(Policy1, Policy2=Default2, ...):
    class _(Policy1, Policy2, ...):
        """Implements class affected by the policies."""
    return _
  • Class instantiation
// in C++
typedef HostClass<Policy1Class, Policy2Class, ...> NewClass;
# in Python
NewClass = HostClass(Policy1Class, Policy2Class, ...)

Using mixins as alternative

I just learned from one of the comments, that it is possible to use Python mixins as an alternative approach to creating new classes. Under this approach module's code is divided between based and mixin classes. You can then create new classes from the base classes, and using mixin classes to implement modifications to the behavior or interface provided by the base classes.

Following this answer, I was able to write my first example using mixins.

class PrintOutput:
    """Implement print output mixin."""
    def _output(self, message):
        print(message)

class SaveOutput:
    """Implement save output mixin."""
    def set_filename(self, filename):
        self.filename = filename
    def _output(self, message):
        with open(self.filename, 'w') as file:
            file.write(message)

class HelloWorld:
    """Creates a host class."""
    def run(self):
        """Print message."""
        self._output('Hello world!')

class PrintHelloWorld(PrintOutput, HelloWorld):
    pass

hw = PrintHelloWorld()
hw.run() # print "Hello World!"

class SaveHelloWorld(SaveOutput, HelloWorld):
    pass

hw = SaveHelloWorld()
hw.set_filename('output.txt')
hw.run() # save "Hello World!" in output.txt

The difference between approaches

The main difference between mixin and my previous example is the inheritance hierarchy between classes. Neither mixin nor based classes can infer which one is in the role of mixin or based class. This is because they are all parents of the new class like PrintHelloWorld or SaveHelloWorld.

In my attempt to do a policy-based design, it is always possible for a host class to know which classes are their policy because they are its parents. This feature allows me to exploit the Python's Method Resolution Order (MRO) to create compositions between host and policy classes. These compositions are the result of using instantiated host classes as a policy to instantiate another host class, see the example below.

class InputMessage:
    """Generate the message."""
    def run(self):
        return 'hello world'

def AddPrefix(Input):
    """Add a prefix."""
    class _(Input):
        def set_prefix(self, prefix):
            self._prefix = prefix
        def run(self):
            return self._prefix + super().run()
    return _

def AddSuffix(Input):
    """Add a suffix."""
    class _(Input):
        def set_suffix(self, suffix):
            self._suffix = suffix
        def run(self):
            return super().run() + self._suffix
    return _

def PrintOutput(Input):
    """Print message."""
    class _(Input):
        def run(self):
            print(super().run())
    return _

PrintPrefixSuffixMessage = PrintOutput(
    AddSuffix(AddPrefix(InputMessage))
)
message = PrintPrefixSuffixMessage()
message.set_prefix('Victor says: ')
message.set_suffix(' and goodbye!')
message.run()

I am not sure if this difference has any practical implications between these two approaches. At the moment I am just trying to learn what can be expressed with them.

Going down the rabbit hole of recursion

I would like to point out that it is possible to add recursion to the mix. This is reminiscent of some metaprogramming techniques used in C++ that enable computation at compile-time. This has in principle no application in Python that is an interpreted language. Please bear with me for a moment the next impractical example below.

class Identity:
    def run(self, z):
        return z

def MandelbrotSeq(Base, n):
    def Mandelbrot(Base, n):
        if n == 0:
            return Base
        class _(Base):
            def run(self, z):
                z = super().run(z)
                return z**2 + self.c
        return Mandelbrot(_, n-1)
    return Mandelbrot(Base, n).__mro__

M = MandelbrotSeq(Identity, 5)

m = M[0]()
m.c = 1
# print 677 the value at 5th iteration for c=1
print(m.run(0))

m = M[1]()
m.c = 1
# print 26 the value at 4th iteration for c=1
print(m.run(0))

m = M[2]()
m.c = 1
# print 5 the value at 3th iteration for c=1
print(m.run(0))

The factory MandelbrotSeq maps recursively the Mandelbrot sequence as a list of hierarchically connected classes. The first element of M refers to the fifth-generation descendant of the Identity class. This means a call to the run(.) member function of an instance of M[0] will return the 5th value of the sequence. In the same way, I call to run(.) member function of an instance of M[1] will return the 4th value of the sequence.

This is just an extreme example of how to exploit the Python MRO as shown in the earlier section. It is only a toy example, and I was not able to come up with a better idea than something like a fractal just because a recursion was involved. Please do not do this at home!

Classmate answered 5/8, 2019 at 19:46 Comment(13)
I feel like this would be a better question for a different StackExchange, maybe Software Engineering?Paco
Python allows for multiple base classes, this would give something similar to this approach using mixinsSimms
Python is dynamic, "Postponing inheritance" doesn't make much sense when classes can be created dynamicallySimms
@GreenCloakGuy thanks for recommendation, I will take a look.Classmate
@IainShelvington both are good points. I will try to create an example using only mixins and see if there is any difference. Related to postponing the inheritance, I meant that the inheritance relationship is not predefined in the module, but it is postponed until the user instantiates the host class with policies.Classmate
The fact that a class statement is executed like any other statement (it's not a static definition) means you can simply wait until you know which base classes to use to define the class. It's the class definition itself that you can postpone until you are ready to define it. (In fact, that's why you can have a class statement in the body of a function; you wait until the function is called to define, then return a reference to, the class.)Aguiar
@Aguiar you are right I am postponing the definition of the class itself. However, I am able to provide in advance a "template" of how the class definition should look like. I will check the question to clarify this point and avoid any future confusion. Thanks!Classmate
keep in mind there is often pushback about Design Patterns in the python community. Sometimes it is unjustified: Command or Composite or Strategy remain useful in concept. But a lot of the creational patterns are of more limited value because the language is so dynamic by nature. As someone said of the Chandler project : Python is not Java.Fried
Put another way: many design patterns exist to work around limitations of a particular language. Patterns that make sense in Java (which doesn't have first-class functions) may make no sense in Python (which does have first-class functions.) Focus on solving the problem, not on porting a solution from another language, i.e., focus on what the solution does, not how it does it.Aguiar
@JLPeyret thanks for the advice.Classmate
@Aguiar all are good points and I agree with them. That is why I defined my design requirements in the question. For the most part, mixins fit the bill. It is just I did not understand until now what the community meant with them. Also, the implementation I am showing seems to do the job, whatever it is the proper name for it: Parametrized Class Factory or Silly Trick 8-)? Thanks!Classmate
IMHO you shouldn't use inheritance for this at all. Pass as argument a "file-like" object that is used for output, then implemente the different "policies" as different types of "file-like" objects. Composition is usually much more flexible than inheritance. Anyway, note that instead of using a class declaration you can also just use type: type('MyClass', (Base1, Base2), {'run': lambda self: self._output('Hello World!')}) works fine (and is basically what happens when executing a class declaration.Fortyfive
@GiacomoAlzetta you are right about using a file object is the right way for this particular example. I intended to give a toy example to illustrate the approach. I also agree with you that you should try first if object composition is right for your requirements. You are also right about you could also use type() to implement this. For example, I use it when coding the decorator approach I describe in the accompanying blog I supporting my answer below. Thx!Classmate
C
0

The answer is the product of what I learn based on my research and some comments from StackOverflow.

I wrote a detail blog about these approaches called Mixin and policy-based design in python. In that post, I also discuss several extensions like implementations using decorators, adding default classes with mixins, and more ...

Design requirements and implementations

Requirement

Design a Python module such users can create classes with new behavior and interfaces by inheriting from a predefined set of classes. This approach provides a large number of classes available to the users based on all possible ways they can be combined into new ones.

This requirement can be implemented using mixin and policy-based implementations.

Mixin implementation

Mixin implementation is defined as follow:

  • Module classes are divided between mixin and base classes.
  • Mixin classes implement modifications to the behavior or interface of a base class.
  • Users create a new class by combining one or more mixin(s) with base classes by inheritance.
class Mixin1Class:
    """Implementation of some Mixin1 class."""
class Mixin2Class:
    """Implementation of some Mixin2 class."""
...
class BasedClass:
    """Implementation of based class."""

# End user creating new class
class NewClass(Mixin1Class, [Mixin2Class, ...], BasedClass):
    pass

References:

Policy-based implementation

Policy-based implementation is defined as follow:

  • Module classes are divided between policy and host classes.
  • Policy classes implement modifications to the behavior or interface of the host.
  • Host classes are defined withing class factories i.e., a function that returns type objects.
  • Users invoke the creation of new class using class factories.
  • Policy classes are passed as arguments to the class factory.
  • Withing the class factory, the host class is created with all the policy classes as its parents.
class Policy1Class:
    """Implementation of some Policy1 class."""
class Policy2Class:
    """Implementation of some Policy2 class."""
...
def HostClassFactory(Policy1, Policy2=Policy2Class, ...):
    """Create a HostClass and return it."""
    class HostClass(Policy1, Policy2, ...):
        """Concrete implementation of the host class."""
    return HostClass

# End user invoking new class
NewClass = HostClassFactory(Policy1Class, [Policy2Class,...])

References:

Comparison between implementations

Here a list of the differences between approaches:

  • The relationship between classes is not the same affecting the Python's Method Resolution Order or MRO, see figure below.
  • The mixin's base classes are defined or instantiated when the user creates the new class.
  • However, policy-based host class definition is delayed until the user calls the factory function.
  • It is possible to provide default policy classes, but you cannot have mixin default classes.
Classmate answered 22/10, 2019 at 12:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.