You can't call a non-existing method on a object without wrapping it first, e.g:
# A legacy class
class dummy:
def foo(self):
return "I'm a dummy!"
obj = dummy()
obj.a("1") # You can't
You can do it using a wrapper class first, here's just a idea of how you can get it done:
# Creates a object you can append methods to
def buildable(cls):
class fake:
# This method will receive the class of the object to build
def __init__(self, cls):
self.cls = cls
# This will simulate a constructor of the underlying class
# Return the fake class so we can call methods on it
def __call__(self, *args, **kwargs):
self.obj = self.cls(*args, **kwargs)
return self
# Will be called whenever a property (existing or non-existing)
# is called on a instance of the fake class
def __getattr__(self, attr):
# If the underlying object has the called attribute,
# just return this attribute
if hasattr(self.obj, attr):
return getattr(self.obj, attr)
# Call the respective function on globals with the provided
# arguments and return the fake class so we can add more methods
def wrapper(*args, **kwargs):
globals()[attr](self.obj, *args, **kwargs)
return self
return wrapper
return fake(cls)
So, how does this work?
- Decorate your legacy class:
@buildable
class dummy:
def foo(self):
return "I'm a dummy!"
- Create the build methods that'll modify
dummy
:
def a(self, some):
self.a = some + 'a'
def b(self, some):
self.b = some + 'b'
def c(self, some):
self.c = some + 'c'
- Modify it:
obj = dummy()
obj.a("1").b("2").c("3")
- See the brand new attributes (and the old ones too!):
print(obj.a) # 1a
print(obj.b) # 2b
print(obj.c) # 3c
print(obj.foo()) # I'm a dummy!
Note that this has some important drawbacks, such as:
- Calling a non-existing attribute on
dummy
will not raise AttributeError:
print(obj.nini) # <function buildable.<locals>.fake.__getattr__.<locals>.wrapper at 0x7f4794e663a0>
- You can't do it with multiple objects:
obj1 = dummy()
obj1.a("1").b("2")
print(obj1.a) # 1a
print(obj1.b) # 2b
obj2 = dummy()
obj2.c("3")
print(obj2.c) # 3c
print(obj1.a) # <function buildable.<locals>.fake.__getattr__.<locals>.wrapper at 0x7f524ae16280>
print(obj1.b) # <function buildable.<locals>.fake.__getattr__.<locals>.wrapper at 0x7f524ae16280>
- The type of
obj
will not be dummy
:
print(type(obj)) # <class '__main__.buildable.<locals>.fake'>
print(type(obj.obj)) # <class '__main__.dummy'>
- You can't call a build method with the same name of an already existing method:
def foo(bar):
self.foo = 'foo' + bar
obj.foo("bar")
print(obj.foo())
# raises TypeError: foo() takes 1 positional argument but 2 were
list = buildable(list)
obj = list()
obj.a("4").b("5").c("6")
# raises AttributeError: 'list' object has no attribute 'a'