你可以使用字符串来实例化一个类吗?

52

我在Python中使用了建造者模式来分离一堆不同的配置可能性。基本上,我有一堆以ID...(例如ID12345)命名的类,它们都继承自基础Builder类。在我的脚本中,每次运行此应用程序时,我需要实例化每个类的实例(大约50次)。因此,我正在尝试看看是否可以像这样做:

ProcessDirector = ProcessDirector()
ID12345 = ID12345()
ID01234 = ID01234()

ProcessDirector.construct(ID12345)
ProcessDirector.construct(ID01234)

ID12345.run()
ID01234.run()

我能不能像这样做(我知道这行不通):

IDS = ["ID12345", "ID01234"]

ProcessDirector = ProcessDirector()
for id in IDS:
  builder = id() #some how instantiate class from string
  ProcessDirector.construct(builder)
  builder.run()

这样,当我将来需要添加新的ID时,只需将其添加到IDS列表中,而不是在代码中到处添加新的ID。

编辑:

看起来基于数据来源有不同的意见。这些ID是在一个没有其他人访问权限的文件中输入的。我不是从命令行读取字符串,并且希望未来添加新ID时尽可能少地更改。


这些 ID 类是在同一个文件中与循环一起的,还是从其他地方导入的? - Justin Voss
Python有没有类似于Java的Class.forName()的等效方法? - S.Lott
17
这不是那个问题的副本,这是一个关于Python而非Java的问题。你引用的那个问题是在询问Python中是否存在一个已经存在于Java中的函数,但其背后的解释很少。请记住,问题是关于人们如何找到它的,而不是答案,因此即使在另一个问题中存在回答,也并不意味着人们会找到它,除非他们像那个问题的提问者一样从Java的角度考虑这个问题。 - Rick
@Rick 和 scottm:Java 开发人员通常认为设计模式与 Java 同义;我在第一行中编辑了“Python 中的 Builder 模式”,以防止出现这种情况。 - smci
6个回答

74

如果你想避免使用eval(),你可以这样做:

id = "1234asdf"
constructor = globals()[id]
instance = constructor()

假设该类已在您的当前范围内定义(或已导入)。


+1 - 这个很好用,我刚刚结合 abc 使用它来动态构建一个基类的所有子类的实例。 - Arj
4
在只有类名字符串的情况下,使用globals()[classname_string]()来实例化一个对象仍然是一个好方法吗?我之所以提出这个问题是因为我在原始回答发布近8年后才问这个问题。 - Vishal
如果您想让动态类继承另一个类,该怎么办? - Brōtsyorfuzthrāx

23

不确定这是否符合您的要求,但似乎有一种更Pythonic的方法来实例化在字符串中列出的一堆类:

class idClasses:
    class ID12345:pass
    class ID01234:pass
# could also be: import idClasses

class ProcessDirector:
    def __init__(self):
        self.allClasses = []

    def construct(self, builderName):
        targetClass = getattr(idClasses, builderName)
        instance = targetClass()
        self.allClasses.append(instance)

IDS = ["ID12345", "ID01234"]

director = ProcessDirector()
for id in IDS:
    director.construct(id)

print director.allClasses
# [<__main__.ID12345 instance at 0x7d850>, <__main__.ID01234 instance at 0x7d918>]

5
如果在运行时你不知道类的名称是什么怎么办?比如说它们是从文件或命令行中读取的。 - RyanN
2
@RyanN 不太清楚你的意思 - IDS 列表只是一个示例,它可以来自任何地方(例如文件等)。 - dbr
1
我可能忽略了某些东西,但在idClasses中,您有一个名为ID12345的类。 我现在看到OP已经有了他想要实例化的一堆类,我正在考虑多次实例化单个通用类,例如 class car():\r\n def __init__(self): pass 实例化为['ford','toyota','vw'] 在construct()中,您可以执行 setattr(self,builderName,idClasses())。这样,您将最终得到director.ford,director.toyota和director.vw对象。 - RyanN

8

尽量不要使用eval()。Python有很多更好的选项(分派字典,getattr()等),你永远不应该使用被称为eval()的安全漏洞。


11
“从语气上讲,“从未”过于绝对。只有在恶意用户可以访问配置文件的情况下,才会存在安全漏洞。防止恶意用户接触配置文件很容易。” - S.Lott
20
我理解“never”表示“除非你绝对确定它是你需要的,并且你知道自己在做什么,否则要避免使用”。使用“never”这个词是一个很好的简短替代方案,因为它会让那些不知道自己在做什么的人感到害怕而离开。那些知道自己在做什么的人会知道何时忽略这个建议。 - user21037
7
这个回答如果不展示任何替代方案,实际上并不具有帮助性。 - Harald Scheirich
是的,但究竟什么是解决方案呢?如果你不提供一个替代方案,仅仅说出不应该使用什么是没有帮助的! - undefined

6

最简单的方法就是创建一个字典。

class A(object): 
    pass
class B(object): 
    pass

namedclass = {'ID12345': A, 'ID2': A, 'B': B, 'AnotherB': B,  'ID01234': B}

然后使用它(您的代码示例):
IDS = ["ID12345", "ID01234"]

ProcessDirector = ProcessDirector()
for id in IDS:
    builder = namedclass[id]() 
    ProcessDirector.construct(builder)
    builder.run()

1
这是最符合Python风格的做法!实际上,这也是我们在Python中模拟缺失的“switch语句”的方式!! - Devy
这确实是一种方法,但它并不完全符合问题的要求:它没有使用字符串来派生要实例化的类,而是使用了实际的类。其中一个缺点是namedclass必须在列出的所有类之后。Django也使用字符串来引用尚未定义的模型。 - minusf
@minusf 这个问题实际上是关于如何从字符串中派生出要实例化的类。通过使用字典,您可以获得最有效和安全的方法,所以我不明白为什么不使用它。 - nosklo

1
我决定将被接受的答案对被接受的答案的评论结合起来。我还添加了重载的__getitem__,以使其更像工厂模式。
import sys
import traceback
import ipdb


class CarTypes:
    class Toyota:
        def __repr__(self):
            return "Toyota()"
        def __str__(self):
            return "Instance of Toyota() class"
    class Nissan:
        def __repr__(self):
            return "Nissan()"
        def __str__(self):
            return "Instance of Nissan() class"


class Car:
    def __init__(self):
        self._all_classes = {}

    def construct(self, builder_name):
        setattr(self, builder_name, CarTypes())
        try:
            target_class = getattr(CarTypes, builder_name)
            instance = target_class()
            self._all_classes[builder_name] = instance
        except AttributeError:
            print("Builder {} not defined.".format(builder_name))
            traceback.print_stack()

    def __getitem__(self, type_name):
        return self._all_classes[type_name]

    def car_type(self, type_name):
        return self._all_classes[type_name]


IDS = ["Toyota", "Nissan", "Unknown"]

director = Car()
for id in IDS:
    director.construct(id)

print(director["Toyota"])
print(director["Nissan"])
print(director.car_type("Toyota"))
print(director.car_type("Nissan"))

编辑:我加入了一些错误处理。

编辑:使用宽松的创意共享许可证。请享用。


0

你的问题中缺少一些东西,所以我不得不猜测省略的部分。请随意编辑您的问题以纠正遗漏。

class ProcessDirector( object ):
    # does something

class ID12345( SomeKindOfProcess ):
    pass

class ID001234( SomeKindOfProcess ):
    pass

idList= [ID12345, ID01234]

theProcessDirector = ProcessDirector()
for id in idList:
  builder = id() #Instantiate an object from the class object
  theProcessDirector.construct(builder)
  builder.run()

这个非常好用。它不会从字符串实例化 - 实际上你通常不需要这样做。有时候,但很少。更常见的情况是,你想要从类对象列表中获取实例对象。

如果你确实从命令行获取你的类名,那么你可以进行以下小改动。

validClasses = [ ID12345, ID01234 ]
validMap = dict( ( (c.__name__, c) for c in validClasses ) )
nameStrings = [ "ID12345", "ID01234" ] # from your command-line 
idList= [ validMap[s] for s in nameStrings ]

其它所有内容保持不变。

[此外,如果可以的话,请尽量以小写字母开头命名实例变量。以大写字母开头的名称通常是类名。]

编辑

删除了eval。尽管eval()绝对是安全漏洞。只有在某人明确授权恶意用户访问时,Eval(和execexecfile)才会成为问题。


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