Why is partial used to add attributes to functions

Viewed 155

I read this article the other day, and I don't understand why partial is used in the section "Logging Decorators":

def attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

attach_wrapper is unsed to set some attributes on a function that wraps another function:

@attach_wrapper(wrapper)  # Attaches "set_level" to "wrapper" as attribute
def set_level(new_level):  # Function that allows us to set log level
    nonlocal level
    level = new_level

Why can't these attributes be directly set on wrapper with setattr?

2 Answers

It's used so that if the attach_wrapper function is called without a function to wrap, it will return something that you can then call with other functions to attach more than one to the targeted object. For example, if you had three functions f, g, and h, and an object foo, you could create an "attacher" with attach_to_foo = attach_wrapper(foo) and then use it with attach_to_foo(f), attach_to_foo(g), and attach_to_foo(h) to end up with foo.f, foo.g, and foo.h.

If you instrument attach_wrapper you can see that it accounts for its func argument being None.

def attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object
    if func is None:
        print(f'obj is {obj.__name__} - func is',func)
        return partial(attach_wrapper, obj)
    print(f'a_w:obj is {obj.__name__} - func is {func.__name__}')
    setattr(obj, func.__name__, func)
    return func

Then

@log(logging.WARN, "example-param")
def somefunc(args):
    return args

Results in

>>>
obj is somefunc - func is None
a_w:obj is somefunc - func is set_level
obj is somefunc - func is None
a_w:obj is somefunc - func is set_message
>>>

attach_wrapper is called twice. The first time its func is None and it returns a partial function attach_wrapper with its first argument obj set to some_func. Then the partial is called (all parts of the decorator mechanism) with the function being decorated.

attach_wrapper's obj looks like some_func but it is actually some_func wrapped with wrapper - it looks and feels the same because that is what @wraps was made for.


Here is a little more detail adding inspect.stack to the instrumentation.

import inspect

def attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object
    print("called from: ", inspect.stack()[1].code_context[0].strip())
    print("\tby function ", inspect.stack()[1].function)
    if func is None:
        print(f'\tobj is {obj.__name__}, func is {func}\n')
        return partial(attach_wrapper, obj)
    print(f'\tobj is {obj.__name__}, func is {func.__name__}\n')
    setattr(obj, func.__name__, func)
    return func

Which produces

>>>
called from:  @attach_wrapper(wrapper)  # Attaches "set_level" to "wrapper" as attribute
    by function  decorate
    obj is somefunc, func is None

called from:  def set_level(new_level):  # Function that allows us to set log level
    by function  decorate
    obj is somefunc, func is set_level

called from:  @attach_wrapper(wrapper)  # Attaches "set_message" to "wrapper" as attribute
    by function  decorate
    obj is somefunc, func is None

called from:  def set_message(new_message):  # Function that allows us to set message
    by function  decorate
    obj is somefunc, func is set_message

>>>

There are some better detailed explanations around. If and when I find one I'll update.

Related