Decorating functions with a specific signature

Revisiting something I wrote. I had a long (to my taste) list of functions to define, that all had the same signature. Motivated exclusively by laziness, I did not want to write their list of keyword arguments (with their type hints) every time. So, I did something like

def my_signature(original_function):
    def _wrapper(
        value1: float,
        value2: str,
        value3: int
    ) -> float:  # The original really had 6 arguments and types with longer names.
        return original_function(value1=value1, value2=value2, value3=value3)
    return _wrapper

# And then all the decorated function definitions

@my_signature
def function1(**kwargs) -> float:
    ...

@my_signature
def function2(**kwargs) -> float:
    ...

# ... etc.

I read the documentation of the decorator module and it looks like my_signature is changing more features from the original_function than the signature.

Previously, you needed to manually copy the function attributes __name__ , __doc__ , __module__ , and __dict__ to the decorated function by hand.

Question: What would be, or how to find out, the complete list of attributes to copy over from original_function to _wrapper?

Question: If a place to look is dir(original_function), are there some of those that I should not copy from original_function to _wrapper?


For example, in the source code of the module decorator, I see them copying the following, where there are entries from dir(func) that are not being copied over. One example is __le__, among others.

    fun.__name__ = func.__name__
    fun.__doc__ = func.__doc__
    fun.__wrapped__ = func
    fun.__signature__ = sig
    fun.__qualname__ = func.__qualname__
    # builtin functions like defaultdict.__setitem__ lack many attributes
    try:
        fun.__defaults__ = func.__defaults__
    except AttributeError:
        pass
    try:
        fun.__kwdefaults__ = func.__kwdefaults__
    except AttributeError:
        pass
    try:
        fun.__annotations__ = func.__annotations__
    except AttributeError:
        pass
    try:
        fun.__module__ = func.__module__
    except AttributeError:
        pass
    try:
        fun.__dict__.update(func.__dict__)
    except AttributeError:
        pass

Have a look at https://github.com/python/cpython/blob/6e97a9647ae028facb392d12fc24973503693bd6/Lib/functools.py#L33

Real answer is: You don’t really need to copy any of those attributes. It all depends on what you want to do with the wrapped function. But if you want it to mimic as far as possible the original function, then you’d use all those attributes.

Question: If a place to look is dir(original_function) , are there some of those that I should not copy from original_function to _wrapper ?

You could copy some others too but it might not really make sense. For instance, __le__ is present but when called will (by default) raise NotImplementedError. Trying to copy __code__ may make your wrapper fail and could defeat wrapping. I don’t know how all the other attributes are used, but know that you don’t need to copy those over, and can imagine that in some contexts it could cause problems if you do.

(There are ways to use __code__ inside a wrapping function, and to re-assign a new code attribute to the original function, which you’d then return. In this case you would only need to reassign the __code__ attribute. But it’s a bit weird, and doesn’t seem generally usable.)

2 Likes

Maybe I missed it somewhere, but any reason to not just simply use the standard functools.wraps inside your decorator, which is explicitly designed for this purpose? Or functools.update_wrapper, if you need more advanced control over what gets copied?

On a broader XY problem note, if you’ve got a bunch of functions with a bunch of identical parameters with identical types, wrapping them in a single dataclass seems like a more conventional, idiomatic design pattern to solve this problem that avoids the dynamanicsm, metaprogramming and complexity of the above approach—have you considered that for your application?

2 Likes

The only reason I had was ignorance. I didn’t know about functools then.

It didn’t cross my mind passing a single instance of a dataclass. This I did know at the time. I guess I obstinately thought in a straight line: “This is the signature. I don’t want to write it. Let Python apply them on the functions.”

Let’s see how I end up refactoring this code. It was from before match was in Python, perhaps at the end I no longer have a list of functions.

Thank you, I will take your suggestions into account.

1 Like