用于 ORM 目的的 python 枚举类

发布于 2024-09-16 07:55:23 字数 2735 浏览 11 评论 0原文

编辑问题

我正在尝试创建一个类工厂,它可以生成具有以下属性的类似枚举的类:

  1. 类是从列表中初始化的 允许的值(即,它是 自动生成!)。
  2. 类创建其自身的一个实例 对于每个允许值。
  3. 类不允许创建 任何其他实例一旦 上述步骤已完成(任何尝试 这样做会导致异常)。
  4. 类实例提供方法 给定一个值,返回一个 参考对应的 实例。
  5. 类实例只有两个 属性:id 和值。这 属性 id 自动递增 每个新实例;属性 value 是实例的值 代表。
  6. 类是可迭代的。我更愿意 使用 接受的 回答另一个SO问题 (具体来说,通过利用类 注册表并定义一个iter 我的元类中的方法 枚举类被实例化)。

这就是我正在寻找的一切。请考虑原文(如下)只是问题的背景。抱歉从一开始就没有说清楚。

更新的答案

我对 aaronasterling 的非常有用的答案做了一些修改。我想我应该在这里展示它,以便其他人受益,并且如果我做错了什么,我会收到更多评论:)

我所做的修改是:

(0) 移植到 p3k (iteritems --> items, metaclass --> 'metaclass =',无需指定对象作为基类)

(1)将实例方法更改为@classmethod(现在我不需要对象来调用它,只需类)

(2) 我不是一次性填充 _registry,而是在每次构造新元素时更新它。这意味着我可以使用它的长度来设置 id,因此我摆脱了 _next_id 属性。它也将更好地用于我计划的扩展(见下文)。

(3) 从 enum() 中删除了类名参数。毕竟,该类名将是本地名称;无论如何,全局名称必须单独设置。所以我使用虚拟的“XXX”作为本地类名。我有点担心第二次调用该函数时会发生什么,但它似乎有效。如果有人知道为什么,请告诉我。如果这是一个坏主意,我当然可以在每次调用时自动生成一个新的本地类名。

(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等)本质上都是枚举类型;例如,类型只能是“科幻”、“浪漫”、“喜剧”、“科学”;覆盖只能是‘硬’、‘软’;等等。 Book 和其他每个类之间存在多对一的关系。

我想半自动生成每个枚举样式的类。请注意,SQLAlchemy 要求将“scifi”表示为 Genre 类的实例;换句话说,简单地定义 Genre.scifi = 0、Genre.romance = 1 等是行不通的。

我尝试编写一个元类枚举,它接受类名称和允许值列表作为参数。我希望

Genre = enum('Genre', ['scifi', 'romance', 'comic', 'science'])

能够创建一个允许这些特定值的类,并且还可以创建我需要的每个对象:流派(“科幻”)、流派(“浪漫”)等。

但我被困住了。一个特殊的问题是,我无法创建 Genre('scifi'),直到 ORM 意识到这个类;另一方面,当 ORM 了解 Genre 时,我们就不再位于类构造函数中了。

另外,我不确定我的方法一开始是否合适。

任何建议将不胜感激。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

初见 2024-09-23 07:55:23

基于更新的新答案

我认为这满足您所有指定的要求。如果没有,我们可以添加您需要的任何内容。

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) 

type(self).__dict__[attr] = type(self)(attr)

所有子类也会创建子类的实例以响应请求。如果您希望子类创建 Genre 的实例,请将 type(self)(attr) 替换为 Genre(attr)

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)

怀念你的温柔 2024-09-23 07:55:23

您可以使用 type 即时创建新类内置:

类型(名称、基础、字典)

返回一个新类型的对象。这是
本质上是一种动态形式
类声明。名称字符串是
类名并成为
__name__ 属性;基元组
逐项列出基类并变为
__bases__ 属性;和字典
字典是包含的命名空间
类体的定义变成
__dict__ 属性。例如,
下面两条语句创建
相同类型的对象:

<前><代码>>>> X 类(对象):
...a = 1
...
>>>>> X = type('X', (object,), dict(a=1))

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。)您是否询问如何创建 Genre 的新子类,或者如何创建新实例?前者在直觉上似乎是正确的,但后者是您所要求的。这很简单:

list( map( Genre, [ 'scifi', ... ] ) )

将为您列出以下内容:

[ Genre( 'scifi' ), ... ]

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' ), ... ]
生死何惧 2024-09-23 07:55:23

也许 Verse Quiz 程序中的这个枚举函数对您有用:诗歌测验

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文