Is there a way to add an attribute to a function as part of the function definition?

Viewed 363

The function attribute do_something.n is incremented each time you call the function.

It bothered me that I declared the attribute do_something.n=0 outside the function.

I answered the question Using queue.PriorityQueue, not caring about comparisons using a "function-attribute" to provide a unique counter for usage with PriorityQueue's - there is a nicer solution by MartijnPieters)

MCVE:

def do_something():
    do_something.n += 1
    return do_something.n 

# need to declare do_something.n before usign it, else 
#     AttributeError: 'function' object has no attribute 'n'
# on first call of do_something() occures
do_something.n = 0

for _ in range(10):
    print(do_something())  # prints 1 to 10

What other ways are there, to define the attribute of a function "inside" of it so you avoid the AttributeError: 'function' object has no attribute 'n' if you forget it?


Edited plenty of other ways in from comments:

5 Answers

Not quite inside, but a decorator makes the function attribute more obvious:

def func_attr(**attrs):
    def wrap(f):
        f.__dict__.update(attrs)
        return f
    return wrap

@func_attr(n=0)
def do_something():
    do_something.n += 1
    return do_something.n

This is probably cleaner than anything that places the attribute initialization inside the function.

This is was what I had in mind when I referred you to my answer to that other question:

def with_this_arg(func):
    def wrapped(*args, **kwargs):
        return func(wrapped, *args, **kwargs)
    return wrapped

@with_this_arg
def do_something(this):
    if not getattr(this, 'n', None):
        this.n = 0
    this.n += 1
    return this.n

for _ in range(10):
    print(do_something())  # prints 1 to 10

If you prefer the more "pythonic" EAFP style of coding—which would would be slightly faster—it could be written thusly:

@with_this_arg
def do_something(this):
    try:
        this.n += 1
    except AttributeError:  # First call.
        this.n = 1
    return this.n

Of course...

This could be combined with @user2357112's answer (if done in the proper order) into something like this which doesn't require checking or exception handling:

def func_attr(**attrs):
    def wrap(f):
        f.__dict__.update(attrs)
        return f
    return wrap

def with_this_arg(func):
    def wrapped(*args, **kwargs):
        return func(wrapped, *args, **kwargs)
    return wrapped

@func_attr(n=0)
@with_this_arg
def do_something(this):
    this.n += 1
    return this.n

for _ in range(10):
    print(do_something())  # prints 1 to 10

Here's yet another couple of ways. The first uses what some call a "functor" class to create a callable with the desired attributes—all from within the class.

This approach doesn't require handling exceptions so the only runtime overhead is from the one-time creation of its single instance.

class do_something:
    def __init__(self):
        self.n = 0

    def __call__(self, *args, **kwargs):
        do_something.n += 1
        return do_something.n

do_something = do_something()  # Allow only one instance to be created.


for _ in range(10):
    print(do_something())  # Prints 1 to 10.

The second way — which is very "pythonic" — would be to put the function in a module (which are effectively singletons). This is what I mean:

File do_something.py:

n = 0

def do_something():
    global n
    n += 1
    return n

Sample usage (in some other script):

from do_something import do_something

for _ in range(10):
    print(do_something())  # Prints 1 to 10.

The only way I can think of, following pythons Ask forgiveness not permission methodology is:

def do_something():
    try:
        do_something.n += 1
    except AttributeError:
        do_something.n = 1

    return do_something.n 

This will automatically generate the attribute on the first call and after that the try: codeblock will work.

There is some overhead due to try: ... catch: but that is the only way I can think of to solve this inside the function.

Related