使用Python globals() 动态类的最佳方法

6 投票
3 回答
3697 浏览
提问于 2025-04-11 09:35

我正在开发一个网页应用程序,它会根据用户的输入返回不同的模块。每个模块都是一个Python类,这个类有一个构造函数,接受一个参数,并且有一个'.html'属性,用来存放输出内容。

从全局命名空间动态获取这个类是可行的:

result = globals()[classname](param).html

而且这种方式比下面这种写法要简洁得多:

if classname == 'Foo':
    result = Foo(param).html
elif classname == 'Bar':
    ...

那么,从风格上来说,写这个的最佳方式是什么呢?使用全局命名空间有什么风险或者不建议的理由吗?

3 个回答

0

另一种建立类名和类之间关系的方法:

在定义类的时候,可以给任何想要放入查找表的类添加一个属性,比如:

class Foo:
    lookup = True
    def __init__(self, params):
        # and so on

完成这个步骤后,建立查找表的方法是:

class_lookup = zip([(c, globals()[c]) for c in dir() if hasattr(globals()[c], "lookup")])
4

首先,听起来你可能有点重复造轮子了……大多数Python的网页框架(比如CherryPy和TurboGears)已经有办法根据网址的内容或者用户输入来把请求分发到特定的类了。

其实你现在的做法并没有什么错误,但是根据我的经验,这通常意味着你的程序里缺少某种“抽象”。你基本上是依赖Python解释器来存储你可能需要的对象列表,而不是自己来管理这些对象。

所以,作为第一步,你可以考虑先做一个字典,把你可能想要调用的所有类列出来:

dispatch = {'Foo': Foo, 'Bar': Bar, 'Bizbaz': Bizbaz}

一开始这样做可能不会有什么明显的变化。但随着你的网页应用不断发展,你会发现几个好处:(a) 你不会遇到命名空间冲突,(b) 使用globals()可能会有安全问题,如果攻击者能找到办法把任意的classname注入到你的程序里,他们就能访问你程序中的任何全局符号,(c) 如果你想让classname变成其他的东西,而不是确切的类名,使用你自己的字典会更灵活,(d) 如果你发现需要的话,你可以把dispatch字典替换成一个更灵活的用户自定义类,用来做数据库访问或者其他事情。

安全问题对于网页应用来说尤其重要。使用globals()[variable],而variable是来自网页表单的输入,这简直就是在自找麻烦

6

这种方法的一个缺陷是,它可能让用户拥有超出你想要的权限。他们只需要提供名称,就能调用那个命名空间中的任何单参数函数。你可以通过一些检查来防范这种情况(比如使用isinstance(SomeBaseClass, theClass)),但最好还是避免使用这种方法。另一个缺点是,它限制了你类的放置位置。如果你有很多这样的类,决定把它们分组到模块中,你的查找代码就会失效。

你有几种替代方案:

  1. 创建一个明确的映射:

     class_lookup = {'Class1' : Class1, ... }
     ...
     result = class_lookup[className](param).html
    

    不过这样有个缺点,就是你必须重新列出所有的类。

  2. 将类嵌套在一个外部作用域中。例如,可以在自己的模块内定义它们,或者在一个外部类中定义:

    class Namespace(object):
        class Class1(object):
            ...
        class Class2(object):
            ...
    ...
    result = getattr(Namespace, className)(param).html
    

    不过这样你会无意中暴露出几个额外的类变量(比如__bases__,__getattribute__等)——可能不会被利用,但也不是完美的。

  3. 从子类树构建一个查找字典。让所有的类都继承自一个单一的基类。当所有类创建完成后,检查所有基类并从中填充一个字典。这样做的好处是,你可以在任何地方定义你的类(例如,在不同的模块中),只要在所有类创建后再创建注册表,你就能找到它们。

    def register_subclasses(base):
        d={}
        for cls in base.__subclasses__():
            d[cls.__name__] = cls
            d.update(register_subclasses(cls))
        return d
    
    class_lookup = register_subclasses(MyBaseClass)
    

    一个更高级的变体是使用自注册类——创建一个元类,自动将任何创建的类注册到一个字典中。这在这个情况下可能有些过于复杂,但在某些“用户插件”的场景中是很有用的。

撰写回答