3

I am trying to write a class that will catch any generic function call, then try some operations with it.

As an example, I try to use the answer to a similar question from here.

However, it throws a AttributeError: 'method' object has no attribute '__name__' error when I try to change the method's name by assigning the __name__ attribute.

The code I am using, adapted from the example above is:

class A():
    def __init__(self):
        self.x = 1 # set some attribute

    def __getattr__(self,attr):
        return self.__get_global_handler(attr)

    def __get_global_handler(self, name):
        # Do anything that you need to do before simulating the method call
        handler = self.__global_handler
        handler.__name__ = name # Change the method's name
        return handler

    def __global_handler(self, *args, **kwargs):
        # Do something with these arguments
        print("I am an imaginary method with name %s" % self.__global_handler.__name__)
        print("My arguments are: " + str(args))
        print("My keyword arguments are: " + str(kwargs))

    def real_method(self, *args, **kwargs):
        print("I am a method that you actually defined")
        print("My name is %s" % self.real_method.__name__)
        print("My arguments are: " + str(args))
        print("My keyword arguments are: " + str(kwargs))

It works fine if I comment the line that change the method's name (handler.__name__ = name # Change the method's name):

>>> a.imaginary_method
<bound method A.__global_handler of <__main__.A object at 0x00000150E64FC2B0>>

>>> a.imaginary_method(1, 2, x=3, y=4)
I am an imaginary method with name __global_handler
My arguments are: (1, 2)
My keyword arguments are: {'x': 3, 'y': 4}

However, if I uncomment the line (to force the name change), I get:

>>> a.imaginary_method
[...]
AttributeError: 'method' object has no attribute '__name__'

What I expected to get was something like:

>>> a.imaginary_method
<bound method A.imaginary_method of <__main__.A object at 0x00000150E64FC2B0>>

>>> a.imaginary_method(1, 2, x=3, y=4)
I am an imaginary method with name imaginary_method
My arguments are: (1, 2)
My keyword arguments are: {'x': 3, 'y': 4}

So, is there a way to change the method's name on the fly like that? Thanks a lot!

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149

1 Answers1

7

The reason why handler.__name__ = name doesn't work is because handler is a bound method, i.e. it's an object that encapsulates a reference to a function and the instance that'll take the value of self. You can confirm this by printing it out; it says "bound method":

>>> a.imaginary_method
<bound method A.__global_handler of <__main__.A object at 0x00000150E64FC2B0>>

You can access the underlying function through the __func__ attribute and change its name:

def __get_global_handler(self, name):
    handler = self.__global_handler
    handler.__func__.__name__ = name # Change the method's name
    handler.__func__.__qualname__ = __class__.__qualname__ + '.' + name
    return handler

The problem with this approach, though, is that all "imaginary methods" actually refer to the very same __global_handler method. If you try to create two methods with different names, you'll see that they actually both have the same name (because they're the same method):

a = A()
foo = a.foo
bar = a.bar

print(foo)  # <bound method A.bar of <__main__.A object at 0x00000212AFCC7198>>
print(bar)  # <bound method A.bar of <__main__.A object at 0x00000212AFCC7198>>

So, a better solution is to create a new function each time:

class MethodFactory:
    def __getattr__(self, name):
        def func(*args, **kwargs):
            print("I am an imaginary method with name", name)
            print("My arguments are:", args)
            print("My keyword arguments are:", kwargs)

        func.__name__ = name
        func.__qualname__ = __class__.__qualname__ + '.' + name
        return func

a = MethodFactory()
foo = a.foo
bar = a.bar
print(foo)  # <function MethodFactory.foo at 0x00000238BDADC268>
print(bar)  # <function MethodFactory.bar at 0x00000238BF8AB730>

As you can see, a side effect of this implementation is that foo and bar are regular functions instead of bound methods. If, for some reason, you have to return bound methods, you can manually convert the function to a bound method by calling its __get__ method:

class MethodFactory:
    def __getattr__(self, name):
        def func(self, *args, **kwargs):
            print("I am an imaginary method with name", name)
            print("My arguments are:", args)
            print("My keyword arguments are:", kwargs)

        func.__name__ = name
        func.__qualname__ = __class__.__qualname__ + '.' + name
        return func.__get__(self, type(self))

a = MethodFactory()
foo = a.foo
bar = a.bar
print(foo)  # <bound method MethodFactory.foo of <__main__.MethodFactory object at 0x00000137BF0C29E8>>
print(bar)  # <bound method MethodFactory.bar of <__main__.MethodFactory object at 0x00000137BF0C29E8>>
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149