Search code examples
pythonclassstatic-methods

Assigning an Attribute to a @staticmethod in Python


I have a scenario where I have objects with static methods. They are all built using an outside def build_hello() as class variables.

def build_hello(name: str):
    @staticmethod
    def hello_fn():
        return "hello my name is "

    # Assign an attribute to the staticmethod so it can be used across all classes
    hello_fn.first_name = name
    print(hello_fn() + hello_fn.first_name) # This works
    return hello_fn

class World:
    hello_fn = build_hello("bob")

# Error, function object has no attribute "first_name"
World.hello_fn.first_name

What is happening here? I am able to access the attribute of hello_fn() within the build_hello() function call. but when its added to my object, that attribute no longer lists.

Also if I call dir() on the static method. I do not see it present:

dir(World.hello_fn)
['__annotations__',
 '__builtins__',
 '__call__',
 '__class__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__get__',
 '__getattribute__',
 '__getstate__',
 '__globals__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__kwdefaults__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__type_params__']

Solution

  • Method retrieving from a class, in Python, be it regular instance methods, class methods or static methods, use an underlying mechanism to actually build the method object at the time it is requested (usually with the . operator, but also with a getattr(...) call):

    The objects that are bound to class namespaces have a __get__ method - it is this __get__ which builds the object that is retrieved and that will be called as the method - so, if it is a classmethod, the cls is inserted as first parameter, or the self argument for regular methods, and nothing inserted for staticmethods.

    The thing is that the __get__ method for a regular function will make it behave like a common instance methods. The @classmethod and @staticmethod decorators have a different behavior for the __get__ method which produce the desired final effects when calling the method.

    So, when you create a staticmethod wrapping your function, it is not this object that is retrieved when you do MyClass.mymethod - rather, it is whatever the __get__ method of @staticmethod returns - in this case, it returns the underlying function as is.

    TL;DR: put your attributes in the underlying function, before wrapping it with the staticmethod call, as that is what is returned by World.hello_fn:

    def build_hello(name: str):
       
        def hello_fn():
            return "hello my name is "
    
        # Assign an attribute to the staticmethod so it can be used across all classes
        hello_fn.first_name = name
        print(hello_fn() + hello_fn.first_name) # This works
        return staticmethod(hello_fn)  # Just wrap the function with the staticmethod decorator here!
    
    class World:
        hello_fn = build_hello("bob")
    
    World.hello_fn.first_name
    

    Alternatively, you can reach the function after applying the decorator through the attribute __func__ of the staticmethod object:

    def build_hello(name: str):
        @staticmethod
        def hello_fn():
            return "hello my name is "
    
        hello_fn.__func__.first_name = name
    
        return hello_fn