How can you add custom attributes to a python function object within the function declaration?

Viewed 186

I'm looking to create a richly annotated codebase. I remembered functions in Python are objects, therefore they allow arbitrary attributes. For example:

def foo(x:int, y:int)->int:
    '''This is a doc string'''
    return x**2 + y**2

foo.__notes__ = {
    'Dependencies':['x', 'y']
}

foo.__notes__

>>> {'Dependencies': ['x', 'y']}

I'm looking to set the __notes__ attribute from within the function definition. Any ideas on how this could be achieved?


EDIT: As pointed out in the comments, __notes__ is a poor name for this attribute. _notes is more appropriate.

2 Answers

You can use a decorator to attached the annotation after the function is defined.

def add_note(k, v):
    def _(f):
        if not hasattr(f, '_notes'):
            f._notes = {}
        f._notes[k] = v
        return f
    return _


@add_note('Dependencies', ['x', 'y'])
def foo(x: int, y: int) -> int:
    return x**2 + y**2

As an aside, dunder names (like __notes__) are reserved for the implementation; you should not invent your own. Just use an ordinary "private" name prefixed with a single _.

I'm going to answer somewhat differently. A function can refer to its own name in its body and can assign/read values to whatever it finds.

In your case, assuming you want to introspect what a function does, what you want to do is to drive function behavior from what's decorated on its object.

Note that in my example below adder3 - which does what you want and updates from within the function body is not documented from the POV of an introspection tool until it has been executed at least once.

adder2 on the other hand is set up so that it adjusts its behavior according to its annotation and that annotation is visible to introspection tools at all times.

adder4 is added to show one limit of this approach. Referring to the function's name is not a guarantee that you are in fact accessing the function, only whatever the function name resolves to in the globals.


def adder1(x:int = 1, y: int = 2, z: int =3 )->int:
    '''This is a doc string'''
    return x**2 + y**2

def decorate(**kwds):
    """ assign kwds as attributes to the function object """
    def actual_decorator(func):
        for k, v in kwds.items():
            setattr(func, k, v)
        return func
    return actual_decorator

depends = {"x","y"}
@decorate(_note=f"depends:{depends}", depends=depends)
def adder2(x:int = 1, y: int = 2, z: int =3 )->int:
    # a function can totally refer to its name 
    # and use annotations to dynamically adjust its behavior
    depends = adder2.depends
    #sum up dependencies
    li = []
    for key in depends:
        v = locals()[key] ** 2
        li.append(v)

    return sum(li)        

def adder3(x:int = 1, y: int = 2, z: int =3 )->int:
    """ no note until function execution so this won't allow tools to inspect the module"""
    _note = getattr(adder3,"_note",None)
    if not _note:
        adder3._note = "depends x,y"
    return x**2 + y**2

def adder4(x:int = 1, y: int = 2, z: int =3 )->int:
    """there is no guarantee that referring by its own name works"""
    print("\n\nadder4.name:", adder4.__name__)
    return (x**2 + y**2) * 1000

adder4_saved = adder4
#swap name
adder4 = adder2 

for func in [adder1,adder2,adder3,adder4_saved]:
    print(f"\n{func.__name__} [note(before)={getattr(func,'_note',None)}] -> {func()} [note(after)={getattr(func,'_note',None)}]")


output:


adder1 [note(before)=None] -> 5 [note(after)=None]

adder2 [note(before)=depends:{'y', 'x'}] -> 5 [note(after)=depends:{'y', 'x'}]
adder3 [note(before)=None] -> 5 [note(after)=depends x,y]


adder4.name: adder2

adder4 [note(before)=None] -> 5000 [note(after)=None]

Related