views:

94

answers:

3

EDITED QUESTION

I'm trying to create a class factory that can generate enumeration-like classes with the following properties:

  1. Class is initialized from the list of allowed values (i.e., it's automatically generated!).
  2. Class creates one instance of itself for each of the allowed value.
  3. 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).
  4. Class instances provide a method that, given a value, returns a reference to the corresponding instance.
  5. 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.
  6. 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.

A: 

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' ), ... ]
katrielalex
map might not be optimal here. Consider `dict((genre, Genre(genre)) for genre in genres)`. This way, you can access the classes by the name of the genre they represent. Just another possibility.
aaronasterling
Thank you and sorry for the confusion. See my new comment under the question. I'd like to create both the classes and the instances of those classes.
max
A: 

Maybe this enumeration function from the Verse Quiz program could be of some use to you: Verse Quiz

Noctis Skytower
Thank you. It's useful, but it's not quite what would work for me. The problem is that I can't just have Genre.comic = 1, Genre.scifi = 2, etc. I actually need Genre class instances to represent 'comic', 'scifi', etc. The reason is that SQLAlchemy would look for certain fields within those objects (such as 'id' and 'name', for example) to populate the tables.
max
Perhaps if I could just make Genre.comic = Genre('comic') somehow.. it would work then.
max
+1  A: 

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)

aaronasterling
Perfect, thank you. Apart from porting to Python 3, I just made one change to your code: used @classmethod decorator just above method instance(). Now I can call with just the class object, even if I don't have an instance object.
max
Why `return iter(cls._instances.itervalues())` and not just `return cls._instances.itervalues()`?
jchl
@jchl, quick coding. fixed. Thanks for pointing that out.
aaronasterling