通用元类以跟踪子类?

11

我正在尝试编写一个通用的元类来跟踪子类。

由于我希望这是通用的,所以我不想在这个元类中硬编码任何类名,因此我想出了一个函数来生成正确的元类,例如:

def make_subtracker(root):
    class SubclassTracker(type):
        def __init__(cls, name, bases, dct):
            print('registering %s' % (name,))
            root._registry.append(cls)
            super(SubclassTracker, cls).__init__(name, bases, dct)
    return SubclassTracker

通过这种方式,我可以使用以下代码为特定的类生成元类:

__metaclass__ = make_subtracker(Root)

这就是我遇到问题的地方。我无法做到这一点:

class Root(object):
   _registry = []
   __metaclass__ = make_subtracker(Root)

...因为当我使用 make_subtracker(Root) 时,Root 还没有被定义。我尝试稍后添加 __metaclass__ 属性,以便至少可以应用于子类:

class Root(object):
   _registry = []

Root.__metaclass__ = make_subtracker(Root)

...但这并不起作用。当类定义被读取时,__metaclass__有特殊的处理方式,详见自定义类创建

我正在寻求建议以实现此目标(无论是在运行时更改类的元类使其应用于其子类,还是采用其他替代方案)。


1
请不要这样做。之后的人会将其删除,因为它太复杂了。请使用一个工厂函数来创建适当子类的对象。 - S.Lott
3个回答

11

我认为你想要类似于这样的东西(未经测试):

class SubclassTracker(type):
    def __init__(cls, name, bases, dct):
        if not hasattr(cls, '_registry'):
            cls._registry = []
        print('registering %s' % (name,))
        cls._registry.append(cls)
        super(SubclassTracker, cls).__init__(name, bases, dct)

然后,针对 Python 2,你可以这样调用它:

class Root(object):
    __metaclass__ = SubclassTracker

适用于 Python 3

class Root(object, metaclass=SubclassTracker):

请注意,你不需要将_registry属性添加到类中,因为这样的东西是元类的作用。既然你已经有了一个这样的元类(指现有的metaclass),那就可以直接使用。

同时,请注意将注册代码移到else子句中,以防止该类注册自身为子类。


实际上,由于您的SubclassTracker类不包括对root类型的任何硬编码引用,因此无需创建函数,因此我直接定义了一个SubclassTracker类,并忘记了make_subtracker。一切正常,谢谢! - Carles Barrobés
@Carles,你在最后一次评论时使用的版本将在多个类中出现问题,如果它们没有定义“_registry”。不过我已经修复了这个问题,所以看看新版本吧。它更加简洁。实际上,我甚至没有想到要放弃工厂函数。 - aaronasterling
就目前而言,“Root”会将自己添加到其“_registry”列表中,即使从技术上讲它并不是自己的子类。这可能是需要的,也可能不是。 - martineau
此外,如果您从Root派生一个类,它的_registry列表中不仅会有自己,还会有Root,即使它们都不是子类--这似乎非常可能是不想要的。 - martineau

11

Python自动为新式类执行此操作,如在这个答案中提到的那样,用于类名查找其所有子类的问题,请参考此处


1
虽然这在技术上确实可以跟踪,但根据您的使用情况,它可能不是非常高效。它返回一个列表,因此如果您正在寻找具有特定属性的类,则必须遍历整个列表。使用元类允许您以任何您想要的方式高效地索引子类。 - Cerin
@Cerin:也许……如果高效率是一个关注点或问题——这不能从问题中确定,因为它没有指定信息将如何使用。无论如何,我很想看到你的想法的具体实现,因为所有其他答案目前也都是基于列表的——所以请随意添加你自己的答案。 - martineau
@martinaeu,我的实现与aaronasterling的相同,只是注册表是一个字典而不是列表,并且密钥是您想要为每个类使用的任何哈希表示。我目前将其用作Django中register()模式的替代方法,以将ModelForm类与唯一的slug关联起来,以便快速URL调度。 - Cerin
@Cerin:aaronasterling的回答存在一些问题,而且对于我们许多人来说,与Django相关的参考意义不大,因此,请发布一些代码或编辑现有的答案。 - martineau
你过于防御了。我并没有批评的意思。我只是提出了一个我认为别人可能想考虑的观点。 - Cerin
@Cerin:如果有人对于搜索列表的效率不满意,可以看一下我自己的答案,它返回一个将所有子类名称映射到子类的字典--这可以被修改以返回不同类型的映射,因为它在定义每个子类时都可以访问。 - martineau

2

我一直在尝试的东西(已经可行)如下:

def sublass_registry():
    ''' Create a metaclass to register subclasses '''

    class SublassRegistryMeta(type):
        def __init__(cls, name, bases, classdict):
            if classdict.get('__metaclass__') is SublassRegistryMeta:
                SublassRegistryMeta.lineage = [cls] # put root class at head of a list
            else:
                # sublclasses won't have __metaclass__ explicitly set to this class
                # we know they're subclassees because this ctor is being called for them
                SublassRegistryMeta.lineage.append(cls) # add subclass to list
            type.__init__(cls, name, bases, classdict)

    return SublassRegistryMeta

def subclasses(cls):
    ''' Return a list containing base and subclasses '''

    try:
        if cls.__metaclass__.lineage[0] is cls: # only valid for a root class
            return cls.__metaclass__.lineage
    except AttributeError:
        pass
    return None

class Car(object): # root class
    __metaclass__ = sublass_registry()

class Audi(Car): # inherits __metaclass__
    pass

class Ford(Car): # inherits __metaclass__
    pass

class Audi2(Audi): # sub-subclass also inherits __metaclass__
    pass

print subclasses(Car)
# [<class '__main__.Car'>, <class '__main__.Audi'>, <class '__main__.Ford'>, <class '__main__.Audi2'>]
print subclasses(Audi)
# None

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接