Python元类和import *

3

主要目标:通过一个字符串自动将类注册到工厂中,在运行时使用该字符串动态创建类,这些类可以在它们自己的文件中而不是分组在一个文件中。

我有几个类都继承自相同的基类,并且它们定义了一个字符串作为它们的类型。

用户想要获取其中一个类的实例,但只知道该类型在运行时。

因此,我有一个工厂来创建给定类型的实例。我不想硬编码"if then语句",所以我有一个元类来注册所有基类的子类:

class MetaRegister(type):
    # we use __init__ rather than __new__ here because we want
    # to modify attributes of the class *after* they have been
    # created
    def __init__(cls, name, bases, dct):
        if not hasattr(cls, 'registry'):
            # this is the base class.  Create an empty registry
            cls.registry = {}
        else:
            # this is a derived class.  Add cls to the registry
            interface_id = cls().get_model_type()
            cls.registry[interface_id] = cls

        super(MetaRegister, cls).__init__(name, bases, dct)

问题在于,为了使工厂正常运作,必须导入所有的子类(从而运行元类)。为了解决这个问题,您可以使用from X import *。但是,要使此方法生效,您需要在包的__init__.py文件中定义一个__all__变量,以包含所有子类。
我不想硬编码子类,因为这会破坏使用元类的目的。
我可以使用以下方法来检查包中的文件:
import glob

from os.path import dirname, basename, isfile

modules = glob.glob(dirname(__file__) + "/*.py")
__all__ = [basename(f)[:-3] for f in modules if isfile(f)]

这个方法很好,但是这个项目需要编译成单一的.so文件,这样就无法使用文件系统了。
那么我如何在运行时实现创建实例而不硬编码类型的主要目标呢?
是否有一种方法可以在运行时填充__all__变量而不用触碰文件系统?
在Java中,我可能会用注释来装饰类,然后在运行时获取所有带有该注释的类,Python中是否有类似的东西?
我知道Python中有装饰器,但我不确定我能否以这种方式使用它们。
编辑1: 每个子类必须在一个文件中。
- Models
-- __init__.py
-- ModelFactory.py
-- Regression
--- __init__.py
--- Base.py
--- Subclass1.py
--- Subclass2ExtendsSubclass1.py

编辑2:一些代码来说明问题:
+ main.py
|__ Models
    |__ __init__.py
    |__ ModelFactory.py
    |__ Regression
        |__ init__.py
        |__ Base.py
        |__ SubClass.py
        |__ ModelRegister.py

main.py

from models.ModelFactory import ModelFactory

if __name__ == '__main__':
    ModelFactory()


ModelFactory.py

from models.regression.Base import registry
import models.regression

class ModelFactory(object):
    def get(self, some_type):
        return registry[some_type]


ModelRegister.py
class ModelRegister(type):
    # we use __init__ rather than __new__ here because we want
    # to modify attributes of the class *after* they have been
    # created
    def __init__(cls, name, bases, dct):
        print cls.__name__
        if not hasattr(cls, 'registry'):
            # this is the base class.  Create an empty registry
            cls.registry = {}
        else:
            # this is a derived class.  Add cls to the registry
            interface_id = cls().get_model_type()
            cls.registry[interface_id] = cls

        super(ModelRegister, cls).__init__(name, bases, dct)

Base.py

from models.regression.ModelRegister import ModelRegister

class Base(object):
    __metaclass__ = ModelRegister

    def get_type(self):
        return "BASE"

SubClass.py

from models.regression.Base import Base


class SubClass(Base):
    def get_type(self):
        return "SUB_CLASS"

运行它,你只能看到打印出 "Base"。使用装饰器会得到相同的结果。


非常好的问题,我很想看到答案,因为我遇到过这个问题很多次,但据我所知,除非解决硬编码类或全局自动导入其他类文件,否则不可能实现。 - Seb D.
当您说该项目需要编译成单个.so文件时,您是什么意思?您如何“编译”该项目?您能否修改编译过程以在包的__init__.py文件中生成__all__ - jme
@jme 我们目前使用Nuitka将整个源代码编译成单个 .so 文件。我们可以更改构建过程以重新创建“init.py”文件,但我希望避免这样做,因为它更加复杂,如果软件包被移动或重命名,则很容易出错,并且它会扩大开发机器和构建机器之间的差距。 - TheGuyWhoCodes
@jme 我实际上尝试使用了pkgutil,但它没有起作用。 - TheGuyWhoCodes
没错,在编译的扩展模块上它不应该起作用,但是如果在编译之前将包安装为普通的Python包,则pkgutiliter_modules应该可以工作。因此,想法是编写一个快速脚本,将纯Python包安装到虚拟环境中,导入它,通过pkgutil检查它,然后生成一个__init__.py文件。我将通过编辑我的答案来演示我的意思。 - jme
显示剩余2条评论
3个回答

5

将类注册为运行时的简单方法是使用装饰器:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class Foo(object):
    pass

@register
class Bar(object):
    pass

如果您的所有类都在同一个模块中定义,并且该模块在运行时被导入,则此方法可行。但是,您的情况会使事情变得更加复杂。首先,您希望在不同的模块中定义您的类。这意味着我们必须能够在运行时动态确定包内存在哪些模块。使用Python的pkgutil模块可以轻松实现,但是,您还声明正在使用Nuitka将您的软件包编译为扩展模块。然而,pkgutil无法与此类扩展模块一起使用。
我无法找到任何记录的方法来确定从Python内部包含在Nuitka扩展模块中的模块。如果确实存在这样的方法,则上面的装饰器方法将在动态导入每个子模块后起作用。
因此,我认为最简单的解决方案是编写一个脚本,在编译之前生成__init__.py。假设我们有以下包结构:
.
├── __init__.py
├── plugins
│   ├── alpha.py
│   └── beta.py
└── register.py

“插件”存放在“plugins”目录中。文件内容如下:
# register.py
# -----------

registry = {}
def register(cls):
    registry[cls.__name__] = cls
    return cls

# __init__.py
# -----------

from . import plugins
from . import register


# ./plugins/alpha.py
# ------------------

from ..register import register

@register
class Alpha(object):
    pass


# ./plugins/beta.py
# ------------------

from ..register import register

@register
class Beta(object):
    pass

目前情况下,导入以上包不会注册任何类。这是因为类定义从未运行,因为包含它们的模块从未被导入。解决方法是自动生成plugins文件夹的__init__.py。下面是一个可以完成这个功能的脚本——这个脚本可以成为您的编译过程的一部分。
import pathlib


root = pathlib.Path('./mypkg/plugins')
exclude = {'__init__.py'}

def gen_modules(root):
    for entry in root.iterdir():
        if entry.suffix == '.py' and entry.name not in exclude:
            yield entry.stem

with (root / '__init__.py').open('w') as fh:
    for module in gen_modules(root):
        fh.write('from . import %s\n' % module)

将此脚本放置在您的包根目录的上一级(假设您的包名为 mypkg),并运行它,结果如下:
from . import alpha
from . import beta

现在开始进行测试:我们编译该软件包:
nuitka --module mypkg --recurse-to=mypkg

尝试导入它,并检查是否正确注册了所有类:

>>> import mypkg
>>> mypkg.register.registry
{'Beta': <class 'mypkg.plugins.beta.Beta'>, 
 'Alpha': <class 'mypkg.plugins.alpha.Alpha'>}

请注意,使用元类来注册插件类的方法也可以使用相同的方法,我只是更喜欢在这里使用装饰器。

如果所有的类都在同一个文件中,那么它肯定可以工作,但是我想每个文件只维护一个类。 - TheGuyWhoCodes
@TheGuyWhoCodes 所以将装饰器类导入到任何需要它的文件中... - Bob Dylan
2
@TheGuyWhoCodes 我建议你在问题中更明确地表达这个要求。 - jme
1
@BobDylan 在子类中导入装饰器,如果没有明确导入,则无法正常工作。 - TheGuyWhoCodes
еҪ“йңҖиҰҒжіЁеҶҢзҡ„зұ»зҡ„.pyж–Ү件жңӘиў«еҜје…Ҙж—¶пјҢиҝҷз§Қж–№жі•е°ұж— жі•е·ҘдҪңпјҢиҝҷжӯЈжҳҜOPй—®йўҳжүҖеңЁгҖӮ - Seb D.
2
@SébastienDeprez 我知道。我的回答是在那个要求之前给出的,当时还没有明确提出那个要求。 - jme

0

我会使用动态导入来实现这个。

models/regression/base.py:

class Base(object):
    def get_type(self):
        return "BASE"

models/regression/subclass.py:

from models.regression.base import Base

class SubClass(Base):
    def get_type(self):
        return "SUB_CLASS"

__myclass__ = SubClass

loader.py:

from importlib import import_module

class_name = "subclass"
module = import_module("models.regression.%s" % class_name)
model = module.__myclass__()
print(model.get_type())

models/models/regression/ 中,使用空的 __init__.py 文件。

具体如下:

nuitka --recurse-none --recurse-directory models --module loader.py

生成的 loader.so 包含 models/ 子目录下的所有模块。


0
如果反射的类正在使用您的元类,则无需使用from X import *来注册它们。只需要import X就足够了。一旦导入包含这些类的模块,这些类将被创建并在您的元类注册表中可用。

1
@TheGuyWhoCodes 也许我误解了你的问题,或者你做错了其他事情。简单的事实是,你不需要使用 from X import Yfrom X import * 导入每个类来使元类起作用。 - Pedro Werneck
您需要明确地导入每个类,以使元类运行。 - TheGuyWhoCodes
@TheGuyWhoCodes 不需要。导入模块就足够了。当导入模块时,类也会被创建。当你显式地导入一个类时,你只是在当前命名空间中创建一个引用。如果你认为你需要显式导入每个类,那么你肯定做错了什么。 - Pedro Werneck
我猜可能存在关于包和模块的误解。 - Liu Hao

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