用于删除 class 定义重复的元类

MetaClasses to remove duplication of class definitions

我有几个 类 定义如下 Python:

class Item:
    def __init__(self, name):
        self.name = name

class Group:
    def __init__(self, name):
        self.name = name
        self.items = {}

    def __getitem__(self, name):
        return self.items[name]

    def __setitem__(self, name, item):
        self.items[name] = item

class Section:
    def __init__(self, name):
        self.name = name
        self.groups = {}

    def __getitem__(self, name):
        return self.groups[name]

    def __setitem__(self, name, group):
        self.groups[name] = group

class List:
    def __init__(self, name):
        self.name = name
        self.sections = {}

    def __getitem__(self, name):
        return self.sections[name]

    def __setitem__(self, name, section):
        self.sections[name] = section

GroupSection List的模式相似。 Python 有没有办法使用 MetaClasses 重构它以避免代码重复?

您可以使用继承:

class Item:
    def __init__(self, name):
        self.name = name

class Group(Item):
    def __init__(self, name):
        super().__init__(name)
        self._dict = {}
        self.items = self._dict

    def __getitem__(self, name):
        return self._dict[name]

    def __setitem__(self, name, item):
        self._dict[name] = item

class Section(Group):
    def __init__(self, name):
        super().__init__(name)
        self.groups = self._dict


class List(Group):
    def __init__(self, name):
        super().__init__(name)
        self.sections = self._dict

是的 - 我也会使用继承来做到这一点,但不是在 __init__ 中定义特定的属性名称,而是将其设置为 class 属性。基础甚至可以声明为抽象的。

class GroupBase():
    collection_name = "items"
    
    def __init__(self, name):
        self.name = name
        setattr(self.collection_name, {})

    def __getitem__(self, name):
        return getattr(self, self.collection_name)[name]


    def __setitem__(self, name, item):
        getattr(self, self.collection_name)[name] = item
    
class Section(GroupBase):
    collection_name = "groups"

class List(GroupBase):
    collection_name = "sections"

请注意,在 运行 时可以使用更多 class 属性,例如 为每个集合指定项目类型,并在需要时强制在 __setitem__ 内输入。

或者,如您所问,可以字面地使用 string-template 系统并仅在 metaclass 中使用“exec”语句创建新的 classes。 那将更接近于“模板”。 class 代码本身将存在于字符串中,并且模式可以使用带有 .format() 的普通字符串替换。与 C++ 模板的主要区别在于语言 运行time 本身将在 运行time 进行替换 - 而不是编译(到字节码)时间。 exec 函数实际上会在此时编译文本模板 - 是的,它比 pre-compiled 代码慢,但由于它只是 运行 一次,在导入时,确实如此没有区别:

group_class_template = """\
class {name}:
    def __init__(self, name):
        self.name = name
        self.{collection_name} = {{}}

    def __getitem__(self, name):
        return self.{collection_name}[name]

    def __setitem__(self, name, item):
        self.{collection_name}[name] = item
"""

class TemplateMeta(type):
    def __new__(mcls, name, bases, cls_namespace, template):
        # It would be possible to run the template with the module namespace
        # where the stub is defined, so that expressions
        # in the variables can access the namespace there
        # just set the global dictionary where the template
        # will be exec-ed to be the same as the stub's globals:
        # modulespace = sys._getframe().f_back.f_globals

        # Othrwise, keeping it simple, just use an empty dict:
        modulespace = {}

        cls_namespace["name"] = name

        exec(template.format(**cls_namespace), modulespace)
        # The class is execed actually with no custom metaclass - type is used.
        # just return the created class. It will be added to the modulenamespace,
        # but special attributes like "__qualname__" and "__file__" won't be set correctly.
        # they can be set here with plain assignemnts, if it matters that they are correct.
        return modulespace[name]


class Item:
    def __init__(self, name):
        self.name = name


class Group(metaclass=TemplateMeta, template=group_class_template):
    collection_name = "items"


class Section(metaclass=TemplateMeta, template=group_class_template):
    collection_name = "groups"
    

class List(metaclass=TemplateMeta, template=group_class_template):
    collection_name = "sections"

并将其粘贴到 REPL 中,我可以只使用创建的 classes:

In [66]: a = Group("bla")

In [67]: a.items
Out[67]: {}

In [68]: a["x"] = 23

In [69]: a["x"]
Out[69]: 23


In [70]: a.items
Out[70]: {'x': 23}

这样做的主要缺点是模板本身看起来只是一个字符串,而诸如 linters、静态类型检查器、auto-complete 基于 IDE 中静态扫描的工具不会为模板化的 classes 工作。这个想法可以发展,这样模板将是有效的 Python 代码,在“.py”文件中 - 它们可以在导入时像任何其他文件一样读取 - 只需要指定一个模板系统而不是使用built-in str.format 以便模板可以是有效代码。例如,如果定义以单个下划线为前缀和结尾的名称是将在模板中替换的名称,则可以使用正则表达式代替 .format.

的 name-replacement

另一个更类似于模板方法的选项可能是使用 type 动态生成您的对象:

def factory(cls_name, collection_name='_data'):
    
    def __init__(self, name):
        self.name = name
        
    def __getitem__(self, key):
        return eval(f'self.{collection_name}[key]')

    def __setitem__(self, key, value):
        exec(f'self.{collection_name}[key] = value')
    
    attrs = {
        '__setitem__': __setitem__,
        '__getitem__': __getitem__,
        '__init__': __init__,
        collection_name: {}
    }
    
    exec(f'{cls_name} = type(cls_name, (), attrs)')
    
    return eval(cls_name)


Item = factory('Item')
Group = factory('Group', 'items')
Section = factory('Section', 'groups')
List = factory('List', 'sections')

g = Group('groupA')
s = Section('section_one')
l = List('list_alpha')

g[1] = 10
s['g'] = g

print(g.items, s.groups, l.sections)

{1: 10} {'g': <main.Group object at 0x7fd87bdfecd0>} {}