EDITED QUESTION
I'm trying to create a class factory that can generate enumeration-like classes with the following properties:
- Class is initialized from the list of allowed values (i.e., it's automatically generated!).
- Class creates one instance of itself for each of the allowed value.
- Class does not allow the creation of any additional instances once the above step is complete (any attempt to do so results in an exception).
- Class instances provide a method that, given a value, returns a reference to the corresponding instance.
- Class instances have just two attributes: id and value. The attribute id auto-increments for each new instance; the attribute value is the value the instance represents.
- Class is iterable. I'd prefer to implement this using the accepted answer to another SO question (specifically, by utilizing class registry and defining an iter method in the metaclass from which my enumeration classes are instanced).
This is all I'm looking for. Please consider the original text (below) just a background to the question. Sorry for not being clear from the start.
UPDATED ANSWER
I made slight modifications to the very helpful answer by aaronasterling. I thought I'd show it here so that others can benefit, and so that I receive more comments if I did something wrong :)
The modifications I made are:
(0) Ported to p3k (iteritems --> items, metaclass --> 'metaclass =', no need to specify object as base class)
(1) Changed instance method into @classmethod (now I don't need the object to call it, just the class)
(2) Instead of populating _registry in one swoop, I update it every time a new element is constructed. This means I can use its length to set id, and so I got rid of _next_id attribute. It will also work better for the extension I plan (see below).
(3) Removed classname parameter from enum(). After all, that classname is going to be a local name; the global name would have to be set separately anyway. So I used a dummy 'XXX' as the local classname. I'm a bit worried about what happens when I call the function for the second time, but it seems to work. If anyone knows why, let me know. If it's a bad idea, I can of course auto-generate a new local classname at every invocation.
(4) Extended this class to allow an option whereby new enum elements can be added by the user. Specifically, if instance() is called with a non-existent value, the corresponding object is created and then returned by the method. This is useful if I grab a large number of enum values from parsing a file.
def enum(values):
class EnumType(metaclass = IterRegistry):
_registry = {}
def __init__(self, value):
self.value = value
self.id = len(type(self)._registry)
type(self)._registry[value] = self
def __repr__(self):
return self.value
@classmethod
def instance(cls, value):
return cls._registry[value]
cls = type('XXX', (EnumType, ), {})
for value in values:
cls(value)
def __new__(cls, value):
if value in cls._registry:
return cls._registry[value]
else:
if cls.frozen:
raise TypeError('No more instances allowed')
else:
return object.__new__(cls)
cls.__new__ = staticmethod(__new__)
return cls
ORIGINAL TEXT
I am using SQLAlchemy as the object-relational mapping tool. It allows me to map classes into tables in a SQL database.
I have several classes. One class (Book) is your typical class with some instance data. The others (Genre, Type, Cover, etc.) are all essentially enumeration type; e.g., Genre can only be 'scifi', 'romance', 'comic', 'science'; Cover can only be 'hard', 'soft'; and so on. There is many-to-one relationship between Book and each of the other classes.
I would like to semi-automatically generate each of the enumeration-style classes. Note that SQLAlchemy requires that 'scifi' is represented as an instance of class Genre; in other words, it wouldn't work to simply define Genre.scifi = 0, Genre.romance = 1, etc.
I tried to write a metaclass enum that accepts as arguments the name of the class and the list of allowed values. I was hoping that
Genre = enum('Genre', ['scifi', 'romance', 'comic', 'science'])
would create a class that allows these particular values, and also goes around and creates each of the objects that I need: Genre('scifi'), Genre('romance'), etc.
But I am stuck. One particular problem is that I can't create Genre('scifi') until ORM is aware of this class; on the other hand, by the time ORM knows about Genre, we're no longer in the class constructor.
Also, I'm not sure my approach is good to begin with.
Any advice would be appreciated.