Can you use a string to instantiate a class?
Asked Answered
N

6

52

I'm using a Builder pattern in Python to separate a bunch of different configuration possibilities. Basically, I have a bunch of classes that are named ID... (e.g. ID12345). These all inherit from the base Builder class. In my script, I need to instantiate an instance for each class (about 50) every time this app runs. So, I'm trying to see if instead of doing something like this:

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

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

ID12345.run()
ID01234.run()

Can I do something like this (I know this doesn't work):

IDS = ["ID12345", "ID01234"]

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

That way, when I need to add a new one in the future, all I have to do is add the id to the IDS list, rather than peppering the new ID throughout the code.

EDIT:

Looks like there are different opinions based on where the data is coming from. These IDs are entered in a file that no one else has access to. I'm not reading the strings from the command line, and I'd like to be able to do as little alteration when adding a new ID in the future.

Neutron answered 16/2, 2009 at 16:0 Comment(4)
Are these ID classes in the same file as the loop, or do you import them from somewhere else?Janettajanette
Duplicate: #453469Psychopharmacology
this is not a duplicate of that, this is a python question, not a java one, the question you referenced is asking if a function exists in python that exists in java with little explanation behind it, remember that the question is how people find it, not the answers so just because an answer exists in the other question that may answer this one doesn't mean people are going to find it unless they are thinking of the question in terms of java like the OP of that question.Aglimmer
@Aglimmer and scottm: often Java people assume design patterns are synonymous with Java; I edited "Builder pattern in Python" in the first line to prevent that.Jollity
D
23

Not sure this is what you want but it seems like a more Pythonic way to instantiate a bunch of classes listed in a string:

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>]
Dorsoventral answered 16/2, 2009 at 22:18 Comment(3)
But what if you don't know what the class names will be until runtime? What if they are read from a file or command line?Hollister
@Hollister Not sure what you mean - the IDS list is just an example, it could come from anywhere (file etc)Dorsoventral
I'm probably overlooking something, but in idClasses you have a class called ID12345. I see now that the OP already had a bunch of classes he wanted to instantiate, and I'm thinking of a single generic class instantiated multiple times e.g. a class class car():\r\n def __init__(self): pass instantiated as ['ford', 'toyota', 'vw'] In construct() you could do setattr(self,builderName,idClasses()). That way you would end up with director.ford, director.toyota, and director.vw objects.Hollister
D
75

If you wanted to avoid an eval(), you could just do:

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

Provided that the class is defined in (or imported into) your current scope.

Dilks answered 16/2, 2009 at 20:34 Comment(3)
+1 - this works great, just did this in conjunction with abc to dynamically build one instance of all subclasses of a base class.Midland
is globals()[classname_string]() still a good way to instantiate an object given only the name of class as a string? Still because I am asking the question almost 8 years after the original response.Polymorphous
What if you want the dynamic class to inherit from another class?Quadrennium
D
23

Not sure this is what you want but it seems like a more Pythonic way to instantiate a bunch of classes listed in a string:

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>]
Dorsoventral answered 16/2, 2009 at 22:18 Comment(3)
But what if you don't know what the class names will be until runtime? What if they are read from a file or command line?Hollister
@Hollister Not sure what you mean - the IDS list is just an example, it could come from anywhere (file etc)Dorsoventral
I'm probably overlooking something, but in idClasses you have a class called ID12345. I see now that the OP already had a bunch of classes he wanted to instantiate, and I'm thinking of a single generic class instantiated multiple times e.g. a class class car():\r\n def __init__(self): pass instantiated as ['ford', 'toyota', 'vw'] In construct() you could do setattr(self,builderName,idClasses()). That way you would end up with director.ford, director.toyota, and director.vw objects.Hollister
S
8

Never use eval() if you can help it. Python has so many better options (dispatch dictionary, getattr(), etc.) that you should never have to use the security hole known as eval().

Soraya answered 16/2, 2009 at 18:17 Comment(4)
-1: "never" is too strong. It's not a security hole unless you also have malicious users with access to configuration files. Preventing malicious users from touching config files is easy.Psychopharmacology
I read "never" to mean "avoid at all costs unless you're absolutely certain its what you need AND you know what you're doing". Using the word "never" is a good short alternative though, as it scares those who don't know what they're doing away. Those who do, will know when to ignore this anyway.Behind
This answer isn't really helpful without actually showing any of the alternativesPragmaticism
Yeah but what exactly is the solution then? It doesn't help to say what you shouldn't use, if you don't give an alternative!Sharolynsharon
G
6

Simplest way is to just create a dict.

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

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

Then use it (your code example):

IDS = ["ID12345", "ID01234"]

ProcessDirector = ProcessDirector()
for id in IDS:
    builder = namedclass[id]() 
    ProcessDirector.construct(builder)
    builder.run()
Gilkey answered 16/2, 2009 at 18:46 Comment(3)
This is the most pythonic way to do it! It's in fact how we mimic the missing "switch statement" in python as well!!Grochow
This is certainly one way to do it, however it is not exactly what the question is about: it's not using a string to derive the class to be instantiated, but its actual class. One downside of this is that namedclass must come after all the classes it lists. Django also uses strings to refer to models not yet defined.Pendergrass
@Pendergrass the question is actually about deriving which class to instantiate from a string. By using a dict you have the most efficient and safe way so I don't see why not use it.Gilkey
S
1

I decided to put together the accepted answer and a comment on the accepted answer. I also added the overloaded __getitem__ so this looks more like a factory pattern.

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"))

Edited: I added in some error handling.

Edited: Placed under permissive Creative Commons license. Enjoy.

Someway answered 9/12, 2018 at 15:45 Comment(0)
P
0

There's some stuff missing from your question, so I'm forced to guess at the omitted stuff. Feel free to edit your question to correct the omissions.

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()

This works very nicely. It doesn't instantiate from a string -- in practice you don't often want this. Sometimes, but rarely. More commonly, you a list of class objects from which you want instance objects.

If you actually are getting your class names from the command line, then you'd make the following small change.

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 ]

Everything else remains the same.

[Also, if possible, try to start instance variable names with lowercase letters. Names which start with Uppercase Letters are usually class names.]

Edit

Removed eval. In spite of the fact that eval() is absolutely not a security hole. Eval (and exec and execfile) are only a problem if someone specifically grants access to malicious users.

Psychopharmacology answered 16/2, 2009 at 18:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.