Reputation: 88711
I have a "factory" that creates instances of classes using a tuple of arguments:
def factory(cls, arg):
return cls(*arg)
class Foo(object):
def __init__(self, a, b):
pass
a = (1,2)
f = factory(Foo, a)
This works well, so then I decided (for various reasons beyond the scope of this question) to add support for classes that don't take any arguments as a fallback so that this can be used more widely within existing code. So I needed to detect the availability of a 2-arg vs 0-arg constructor and fallback appropriately. Duck-typing seems like the "Pythonic" answer and works beautifully:
def factory(cls, arg):
try:
return cls(*arg)
except TypeError:
print("Missing 2-arg, falling back to 0-arg ctor")
return cls()
class Foo(object):
def __init__(self,a,b):
pass
class Bar(object):
def __init__(self,a):
pass
a = (1,2)
f = factory(Foo, a)
b = factory(Bar, a)
The problem comes however when there's an error inside one of the __init__
functions. I tried to be helpful and warn when neither the 0-arg nor 2-arg __init__
exist:
def factory(cls, arg):
try:
return cls(*arg)
except TypeError:
print("%s Missing 2-arg, falling back to 0-arg ctor" % str(cls))
return cls()
class Foo(object):
def __init__(self,a,b):
iter(a) # Oops, TypeError
a = (1,2)
try:
f = factory(Foo, a)
except TypeError: # 0-arg must be missing too
print("Neither 2-arg nor 0-arg ctor exist, that's all we support, sorry")
But there's a fatal flaw here - I can't distinguish between a TypeError
that was raised because no appropriate __init__
existed and a TypeError
that was raised because of a problem deeper in the __init__
.
I'm not looking for ways to fix Foo
or Bar
, rather ways to understand the failures more precicely at the level of the "factory" or its callers.
How can I programatically tell if the TypeError
was the result of no overload matching, or a failure elsewhere? The best idea I have right now is programatically walking the stack trace and looking at line numbers, which is hideous at best.
Upvotes: 3
Views: 105
Reputation: 398
Testing the number of arguments required by the __init__
method would let you separate the two failure modes.
class Foo(object):
def __init__(self,a,b):
pass
class Bar(object):
def __init__(self,a):
pass
class Baz(object):
def __init__(self):
pass
def ctr_arg_count(cls):
fn = getattr(cls, '__init__')
return len(inspect.getargspec(fn).args) - 1
def factory(cls, *args):
if len(args) != ctr_arg_count(cls):
print("Can't initialize %s - wrong number of arguments" % cls)
return None
return cls(*args)
print factory(Foo, 1, 2)
print factory(Bar, 1)
print factory(Baz)
print factory(Foo, 1)
>>> <__main__.Foo object at 0x100483d10>
<__main__.Bar object at 0x100483d10>
<__main__.Baz object at 0x100483d10>
Can't initialize <class '__main__.Foo'> - wrong number of arguments
None
Upvotes: 1