A python2 example of meta programming and self-registering subclasses (with lazy loading)
Want to know which Pokemon types are weakest against a specific Pokemon type?
In your software engineering efforts, you may come across a scenario which could benefit from plug-and-play classes. This is a concise example of self-registering classes which can be added or removed easily without affecting the main functionality that uses
them.
Pokemon is a game of fighting creatures that have a type hierarchy. From what I understand, all Pokemon types are either effective, weak or neutral against other specific Pokemon types... which essentially ends up making it a very complicated version of rock/paper/scissors.
This project allows the developer to create new Pokemon classes which will increase the usefulness of the code's output.
This link contains a chart documenting Pokemon types and which types they are strong or weak against. This code attempts to provide a user with the Weak Against values quickly, given any Pokemon type.
The find_weak_types_to_fight()
function, which is called from main()
, accepts one dictionary with one key
"type" and one string value of a Pokemon type
Why did I choose a dictionary as the data type to pass into
find_weak_types_to_fight()
? Because a dictionary is a common data type passed in as a payload to a REST API, and that dictionary is also passed to each Pokemon Type class' validate_fighting_type() methods, which can act on that payload as needed
Inside find_weak_types_to_fight()
there is a call to PokemonInterface.registry
, expecting it to be of a type dictionary. But how can that be? The code doesnt instantiate any Pokemon type classes, and it certainly doesnt instantiate an interface...
The answer is Meta Programming. Unbenkownst to most developers, the import statements are doing more than anyone thinks. Step through with a debugger and see what happens
When PokemonInterface
is imported inside weak_pokemon.py
it makes a call to its __metaclass__
constructor. Since there is no class attribute named registry, it creates one, assigning it the value defaultdict(list)
.
This is used so the registry
dict has a string key (Pokemon type) and a list value (classes of Pokemon types that are weak against it), because we will be pushing additional values as each Pokemon type class is evaluated.
One Annoyance... Each Pokemon Type class has to be manually imported in
weak_pokemon.py
, which means you need a code change anytime a new class is added
from pokemon_types.bug import Bug
from pokemon_types.dragon import Dragon
from pokemon_types.electric import Electric
from pokemon_types.flying import Flying
from pokemon_types.grass import Grass
from pokemon_types.ground import Ground
from pokemon_types.ice import Ice
from pokemon_types.rock import Rock
from pokemon_types.steel import Steel
from pokemon_types.water import Water
As each Pokemon type class is imported, since it implements the PokemonInterface interface, the __metaclass__
constructor is called... and this time the registry class attribute exists, allowing values to be appended to the handler.
For each string in the Pokemon type's weak class attribute (which is a list), the registry dictionary receives another entry (if the key doesnt exist) or has a value appended (if the key exists).
Once all Pokemon type classes are imported then the PokemonInterface.registry
dictionary is fully populated
New Pokemon characters are periodically introduced into the game. I have no idea if new Pokemon Types are introduced as well, but I'll assume they could be.
Since I'm all about the plug-and-play aspect, I want to be able to create a new class as needed and add it to the pokemon_types directory and NOT have to modify the weak_pokemon.py file
This can be accomplished by the lazy importing of all files in that directory, by adding code to the traditionally empty __init__.py file
from os.path import dirname, basename, isfile
import glob
modules = glob.glob(dirname(__file__)+"/*.py")
__all__ = [basename(f)[:-3] for f in modules if isfile(f) is True and not f.startswith('_')]`
Then the plethora of Pokemon Type class import statements in the weak_pokemon.py file can be replaced by this statement
from pokemon_types import *
This solution was discovered by searching python import all files in directory which led me to this StackOverflow post.
New Pokemon Type classes can now be added to the pokemon_types directory and be auto-magically imported without any changes to existing code :)
Given an input of: {'type': 'steel'}
The output will be:
Finding weak Pokemon types to fight against type: STEEL...
Bug is weak fighting STEEL
Dragon is weak fighting STEEL
Flying is weak fighting STEEL
Grass is weak fighting STEEL
Ice is weak fighting STEEL
Rock is weak fighting STEEL
Steel is weak fighting STEEL
- Create the remaining Pokemon type classes from this chart
- Add functionality to print which Pokemon types the Pokemon type in question is strong fighting against it.
- Convert this code into an API using Flask-RESTPlus
A Primer On Python Metaclasses