Motivation for Metaclasses
In this chapter of our tutorial we want to provide some incentives or motivation for the use of metaclasses. To demonstrate some design problems, which can be solved by metaclasses, we will introduce and design a bunch of philosopher classes. Each philosopher class (Philosopher1, Philosopher2, and so on) need the same "set" of methods (in our example just one, i.e. "the_answer") as the basics for his or her pondering and brooding. A stupid way to implement the classes consists in having the same code in every philospher class:
class Philosopher1:def the_answer(self, *args):return 42class Philosopher2:def the_answer(self, *args):return 42class Philosopher3:def the_answer(self, *args):return 42plato = Philosopher1()print(plato.the_answer())kant = Philosopher2()# let's see what Kant has to say :-)print(kant.the_answer())
4242
We can see that we have multiple copies of the method "the_answer". This is error prone and tedious to maintain, of course.
From what we know so far, the easiest way to accomplish our goal without creating redundant code consists in designing a base, which contains "the_answer" as a method. Each Philosopher class inherits now from this base class:
class Answers:def the_answer(self, *args):return 42class Philosopher1(Answers):passclass Philosopher2(Answers):passclass Philosopher3(Answers):passplato = Philosopher1()print(plato.the_answer())kant = Philosopher2()# let's see what Kant has to say :-)print(kant.the_answer())
4242
The way we have designed our classes, each Philosopher class will always have a method "the_answer". Let's assume, we don't know a priori if we want or need this method. Let's assume that the decision, if the classes have to be augmented, can only be made at runtime. This decision might depend on configuration files, user input or some calculations.
# the following variable would be set as the result of a runtime calculation:x = input("Do you need the answer? (y/n): ")if x=="y":required = Trueelse:required = Falsedef the_answer(self, *args):return 42class Philosopher1:passif required:Philosopher1.the_answer = the_answerclass Philosopher2:passif required:Philosopher2.the_answer = the_answerclass Philosopher3:passif required:Philosopher3.the_answer = the_answerplato = Philosopher1()kant = Philosopher2()# let's see what Plato and Kant have to say :-)if required:print(kant.the_answer())print(plato.the_answer())else:print("The silence of the philosphers")
Do you need the answer? (y/n): y4242
Even though this is another solution to our problem, there are still some serious drawbacks. It's error-prone, because we have to add the same code to every class and it seems likely that we might forget it. Besides this it's getting hardly manageable and maybe even confusing, if there are many methods we want to add.
We can improve our approach by defining a manager function and avoiding redundant code this way. The manager function will be used to augment the classes conditionally.
# the following variable would be set as the result of a runtime calculation:x = input("Do you need the answer? (y/n): ")if x=="y":required = Trueelse:required = Falsedef the_answer(self, *args):return 42# manager functiondef augment_answer(cls):if required:cls.the_answer = the_answerclass Philosopher1:passaugment_answer(Philosopher1)class Philosopher2:passaugment_answer(Philosopher2)class Philosopher3:passaugment_answer(Philosopher3)plato = Philosopher1()kant = Philosopher2()# let's see what Plato and Kant have to say :-)if required:print(kant.the_answer())print(plato.the_answer())else:print("The silence of the philosphers")
Do you need the answer? (y/n): y4242
This is again useful to solve our problem, but we, i.e. the class designers, must be careful not to forget to call the manager function "augment_answer". The code should be executed automatically. We need a way to make sure that "some" code might be executed automatically after the end of a class definition.
# the following variable would be set as the result of a runtime calculation:x = input("Do you need the answer? (y/n): ")if x=="y":required = Trueelse:required = Falsedef the_answer(self, *args):return 42def augment_answer(cls):if required:cls.the_answer = the_answer# we have to return the class now:return cls@augment_answerclass Philosopher1:pass@augment_answerclass Philosopher2:pass@augment_answerclass Philosopher3:passplato = Philosopher1()kant = Philosopher2()# let's see what Plato and Kant have to say :-)if required:print(kant.the_answer())print(plato.the_answer())else:print("The silence of the philosphers")
Do you need the answer? (y/n): y4242
Metaclasses can also be used for this purpose as we will learn in the next chapter.
No comments:
Post a Comment