Reputation:
Given a string as user input to a Python function, I'd like to get a class object out of it if there's a class with that name in the currently defined namespace. Essentially, I want the implementation for a function which will produce this kind of result:
class Foo:
pass
str_to_class("Foo")
==> <class __main__.Foo at 0x69ba0>
Is this, at all, possible?
Upvotes: 247
Views: 409852
Reputation: 41755
I've looked at how django handles this
django.utils.module_loading has this
def import_string(dotted_path):
"""
Import a dotted module path and return the attribute/class designated by the
last name in the path. Raise ImportError if the import failed.
"""
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError:
msg = "%s doesn't look like a module path" % dotted_path
six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
module = import_module(module_path)
try:
return getattr(module, class_name)
except AttributeError:
msg = 'Module "%s" does not define a "%s" attribute/class' % (
module_path, class_name)
six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
You can use it like import_string("module_path.to.all.the.way.to.your_class")
Upvotes: 43
Reputation: 442
If you really want to retrieve classes you make with a string, you should store (or properly worded, reference) them in a dictionary. After all, that'll also allow to name your classes in a higher level and avoid exposing unwanted classes.
Example, from a game where actor classes are defined in Python and you want to avoid other general classes to be reached by user input.
Another approach (like in the example below) would to make an entire new class, that holds the dict
above. This would:
Example:
class ClassHolder:
def __init__(self):
self.classes = {}
def add_class(self, c):
self.classes[c.__name__] = c
def __getitem__(self, n):
return self.classes[n]
class Foo:
def __init__(self):
self.a = 0
def bar(self):
return self.a + 1
class Spam(Foo):
def __init__(self):
self.a = 2
def bar(self):
return self.a + 4
class SomethingDifferent:
def __init__(self):
self.a = "Hello"
def add_world(self):
self.a += " World"
def add_word(self, w):
self.a += " " + w
def finish(self):
self.a += "!"
return self.a
aclasses = ClassHolder()
dclasses = ClassHolder()
aclasses.add_class(Foo)
aclasses.add_class(Spam)
dclasses.add_class(SomethingDifferent)
print aclasses
print dclasses
print "======="
print "o"
print aclasses["Foo"]
print aclasses["Spam"]
print "o"
print dclasses["SomethingDifferent"]
print "======="
g = dclasses["SomethingDifferent"]()
g.add_world()
print g.finish()
print "======="
s = []
s.append(aclasses["Foo"]())
s.append(aclasses["Spam"]())
for a in s:
print a.a
print a.bar()
print "--"
print "Done experiment!"
This returns me:
<__main__.ClassHolder object at 0x02D9EEF0>
<__main__.ClassHolder object at 0x02D9EF30>
=======
o
<class '__main__.Foo'>
<class '__main__.Spam'>
o
<class '__main__.SomethingDifferent'>
=======
Hello World!
=======
0
1
--
2
6
--
Done experiment!
Another fun experiment to do with those is to add a method that pickles the ClassHolder
so you never lose all the classes you did :^)
UPDATE: It is also possible to use a decorator as a shorthand.
class ClassHolder:
def __init__(self):
self.classes = {}
def add_class(self, c):
self.classes[c.__name__] = c
# -- the decorator
def held(self, c):
self.add_class(c)
# Decorators have to return the function/class passed (or a modified variant thereof), however I'd rather do this separately than retroactively change add_class, so.
# "held" is more succint, anyway.
return c
def __getitem__(self, n):
return self.classes[n]
food_types = ClassHolder()
@food_types.held
class bacon:
taste = "salty"
@food_types.held
class chocolate:
taste = "sweet"
@food_types.held
class tee:
taste = "bitter" # coffee, ftw ;)
@food_types.held
class lemon:
taste = "sour"
print(food_types['bacon'].taste) # No manual add_class needed! :D
Upvotes: 11
Reputation: 5482
You want the class Baz
, which lives in module foo.bar
. With Python 2.7,
you want to use importlib.import_module()
, as this will make transitioning to Python 3 easier:
import importlib
def class_for_name(module_name, class_name):
# load the module, will raise ImportError if module cannot be loaded
m = importlib.import_module(module_name)
# get the class, will raise AttributeError if class cannot be found
c = getattr(m, class_name)
return c
With Python < 2.7:
def class_for_name(module_name, class_name):
# load the module, will raise ImportError if module cannot be loaded
m = __import__(module_name, globals(), locals(), class_name)
# get the class, will raise AttributeError if class cannot be found
c = getattr(m, class_name)
return c
Use:
loaded_class = class_for_name('foo.bar', 'Baz')
Upvotes: 120
Reputation: 6460
This could work:
import sys
def str_to_class(classname):
return getattr(sys.modules[__name__], classname)
Upvotes: 206
Reputation: 392010
Warning:
eval()
can be used to execute arbitrary Python code. You should never useeval()
with untrusted strings. (See Security of Python's eval() on untrusted strings?)
This seems simplest.
>>> class Foo(object):
... pass
...
>>> eval("Foo")
<class '__main__.Foo'>
Upvotes: 155
Reputation: 20838
Using importlib worked the best for me.
import importlib
importlib.import_module('accounting.views')
This uses string dot notation for the python module that you want to import.
Upvotes: 3
Reputation: 33
In terms of arbitrary code execution, or undesired user passed names, you could have a list of acceptable function/class names, and if the input matches one in the list, it is eval'd.
PS: I know....kinda late....but it's for anyone else who stumbles across this in the future.
Upvotes: 3
Reputation: 101801
import sys
import types
def str_to_class(field):
try:
identifier = getattr(sys.modules[__name__], field)
except AttributeError:
raise NameError("%s doesn't exist." % field)
if isinstance(identifier, (types.ClassType, types.TypeType)):
return identifier
raise TypeError("%s is not a class." % field)
This accurately handles both old-style and new-style classes.
Upvotes: 21
Reputation: 6123
Yes, you can do this. Assuming your classes exist in the global namespace, something like this will do it:
import types
class Foo:
pass
def str_to_class(s):
if s in globals() and isinstance(globals()[s], types.ClassType):
return globals()[s]
return None
str_to_class('Foo')
==> <class __main__.Foo at 0x340808cc>
Upvotes: 5