用于ORM的Python枚举类
编辑后的问题
我想创建一个类工厂,这个工厂可以生成类似枚举的类,具备以下特性:
- 类是从允许的值列表中初始化的(也就是说,它是自动生成的!)。
- 类为每个允许的值创建一个自身的实例。
- 一旦完成上述步骤,类不允许再创建任何额外的实例(任何尝试这样做都会导致异常)。
- 类的实例提供一个方法,给定一个值,可以返回对应实例的引用。
- 类的实例只有两个属性:id 和 value。id 属性会随着每个新实例自动增加;value 属性是实例所代表的值。
- 类是可迭代的。我希望使用另一个问题的接受答案来实现这一点(具体来说,通过利用类注册表并在我的枚举类的元类中定义一个iter方法)。
这就是我想要的。请把下面的原始文本仅作为问题的背景。抱歉一开始没有说清楚。
更新的回答
我对aaronasterling的非常有帮助的回答做了一些小修改。我想在这里展示一下,以便其他人也能受益,如果我做错了什么,也能收到更多的评论 :)
我做的修改包括:
(0) 移植到p3k(iteritems --> items,metaclass --> 'metaclass =',不需要指定object作为基类)
(1) 将实例方法改为@classmethod(现在我不需要对象来调用它,只需要类即可)
(2) 不再一次性填充_registry,而是每次构造新元素时更新它。这意味着我可以用它的长度来设置id,因此我去掉了_next_id属性。这对我计划的扩展也会更好(见下文)。
(3) 从enum()中移除了classname参数。毕竟,那个classname将是一个局部名称;全局名称反正得单独设置。所以我用了一个虚拟的'XXX'作为局部classname。我有点担心第二次调用这个函数时会发生什么,但似乎可以正常工作。如果有人知道原因,请告诉我。如果这是个坏主意,我当然可以在每次调用时自动生成一个新的局部classname。
(4) 扩展了这个类,允许用户添加新的枚举元素。具体来说,如果用一个不存在的值调用instance(),则会创建相应的对象并由该方法返回。这在我从解析文件中获取大量枚举值时非常有用。
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
原始文本
我正在使用SQLAlchemy作为对象关系映射工具。它允许我将类映射到SQL数据库中的表。
我有几个类。其中一个类(Book)是典型的类,包含一些实例数据。其他类(Genre、Type、Cover等)本质上都是枚举类型;例如,Genre只能是'scifi'、'romance'、'comic'、'science';Cover只能是'hard'、'soft';等等。Book与其他每个类之间都有多对一的关系。
我想半自动生成每个枚举风格的类。请注意,SQLAlchemy要求'scifi'作为Genre类的一个实例来表示;换句话说,简单地定义Genre.scifi = 0,Genre.romance = 1等是行不通的。
我尝试编写一个接受类名和允许值列表作为参数的元类enum。我希望
Genre = enum('Genre', ['scifi', 'romance', 'comic', 'science'])
能创建一个允许这些特定值的类,并且还会自动创建我需要的每个对象:Genre('scifi')、Genre('romance')等。
但我卡住了。一个特别的问题是,在ORM知道这个类之前,我无法创建Genre('scifi');另一方面,当ORM知道Genre时,我们已经不在类构造函数中了。
此外,我也不确定我的方法是否一开始就是好的。
任何建议都将不胜感激。
3 个回答
也许这个来自Verse Quiz程序的枚举函数对你会有一些帮助:Verse Quiz
你可以使用内置的 type
函数动态创建新的类:
type(name, bases, dict)
这个函数会返回一个新的类型对象。简单来说,它是一种动态创建类的方式。这里的 name 是类的名字,最终会变成
__name__
属性;bases 是一个元组,列出了基类,最终会变成__bases__
属性;dict 是一个字典,里面包含了类的定义,最终会变成__dict__
属性。比如,下面这两条语句创建的类型对象是一样的:>>> class X(object): ... a = 1 ... >>> X = type('X', (object,), dict(a=1)) New in version 2.2.
在这个例子中:
genre_mapping = { }
for genre in { 'scifi', 'romance', 'comic', 'science' }:
genre_mapping[ 'genre' ] = type( genre, ( Genre, ), { } )
或者在 Python 2.7 及以上版本中:
genre_mapping = { genre: type( genre, ( Genre, ), { } ) for genre in genres }
如果你经常这样做,可以把这个模式抽象出来。
>>> 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'>}
编辑:我是不是理解错了?(我之前没用过 SQLAlchemy。)你是在问怎么创建新的 子类 吗,还是怎么创建新的 实例?前者听起来直观,但后者才是你问的内容。其实很简单:
list( map( Genre, [ 'scifi', ... ] ) )
这会给你一个列表:
[ Genre( 'scifi' ), ... ]
基于更新的新回答
我觉得这个回答满足了你所有的要求。如果不满足,我们可以再加上你需要的内容。
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')
旧回答
针对你在Noctis Skytower的回答中提到的评论,你说你想要 Genre.comic = Genre('comic')
(这个还没测试过):
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]
这段代码的意思是,当你试图访问某个类型时,它会创建一个这个类型的实例,并把它附加到你请求的那个实例上。如果你想让它附加到整个类上,可以把这一行替换成
self.__dict__[attr] == type(self)(attr)
这样所有的子类在被请求时也会创建自己的实例。如果你希望子类创建 Genre
的实例,可以把 type(self)(attr)
替换成 Genre(attr)
。
type(self).__dict__[attr] = type(self)(attr)