python enumeration class for ORM purposes
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 cl开发者_运维技巧s
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.
new answer based on updates
I think that this satisfies all of your specified requirements. If not, we can probably add whatever you need.
def enum(classname, values):
class EnumMeta(type):
def __iter__(cls):
return cls._instances.itervalues()
class EnumType(object):
__metaclass__ = EnumMeta
_instances = {}
_next_id = 0
def __init__(self, value):
self.value = value
self.id = type(self)._next_id
type(self)._next_id += 1
def instance(self, value):
return type(self)._instances[value]
cls = type(classname, (EnumType, ), {})
instances = dict((value, cls(value)) for value in values)
cls._instances = instances
def __new__(cls, value):
raise TypeError('No more instances allowed')
cls.__new__ = staticmethod(__new__)
return cls
Genre = enum('Genre', ['scifi', 'comic', 'science'])
for item in Genre:
print item, item.value, item.id
assert(item is Genre(item.value))
assert(item is item.instance(item.value))
Genre('romance')
old answer
In response to your comment on Noctis Skytower's answer wherein you say that you want Genre.comic = Genre('comic')
(untested):
class Genre(GenreBase):
genres = ['comic', 'scifi', ... ]
def __getattr__(self, attr):
if attr in type(self).genres:
self.__dict__[attr] = type(self)(attr)
return self.__dict__[attr]
This creates an instance of genre in response to an attempt to access it and attaches it to the instance on which it is requested. If you want it attached to the entire class, replace the line
self.__dict__[attr] == type(self)(attr)
with
type(self).__dict__[attr] = type(self)(attr)
this has all subclasses create instances of the subclass in response to requests as well. If you want subclasses to create instances of Genre
, replace type(self)(attr)
with Genre(attr)
You can create new classes on-the-fly with the type
builtin:
type(name, bases, dict)
Return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the
__name__
attribute; the bases tuple itemizes the base classes and becomes the__bases__
attribute; and the dict dictionary is the namespace containing definitions for class body and becomes the__dict__
attribute. For example, the following two statements create identical type objects:>>> class X(object): ... a = 1 ... >>> X = type('X', (object,), dict(a=1)) New in version 2.2.
In this case:
genre_mapping = { }
for genre in { 'scifi', 'romance', 'comic', 'science' }:
genre_mapping[ 'genre' ] = type( genre, ( Genre, ), { } )
or in Python 2.7+:
genre_mapping = { genre: type( genre, ( Genre, ), { } ) for genre in genres }
If you are doing this a lot, you can abstract away the pattern.
>>> def enum( cls, subs ):
... return { sub: type( sub, ( cls, ), { } ) for sub in subs }
...
>>> enum( Genre, [ 'scifi', 'romance', 'comic', 'science' ] )
{'romance': <class '__main__.romance'>, 'science': <class '__main__.science'>,
'comic': <class '__main__.comic'>, 'scifi': <class '__main__.scifi'>}
EDIT: Have I missed the point? (I've not used SQLAlchemy before.) Are you asking how to create new subclasses of Genre
, or how to create new instances? The former seems intuitively right, but the latter is what you've asked for. It's easy:
list( map( Genre, [ 'scifi', ... ] ) )
will make you a list of:
[ Genre( 'scifi' ), ... ]
Maybe this enumeration function from the Verse Quiz program could be of some use to you: Verse Quiz
精彩评论