Adding attributes to instance methods in Python
Asked Answered
P

5

10

I would like to add an attribute to an instance method in one of my classes. I tried the answer given in this question, but this answer only works for functions -- as far as I can tell.

As an example, I would like to be able to do something like:

class foo(object):
    ...
    def bar(self):
        self.bar.counter += 1
        return self.bar.counter
    bar.counter = 1
    ...

but, when I call foo().bar() I get:

AttributeError: 'instancemethod' object has no attribute 'counter'

My goal in doing this is to try to impress that the 'counter' variable is local to the bar() method, and also to avoid cluttering my class namespace with yet another attribute. Is there a way to do this? Is there a more pythonic way of doing this?

Perdition answered 1/3, 2012 at 20:17 Comment(3)
Should counter be class level (all instances share the current count), or instance level (each instance would start at 1) ?Launderette
counter should be instance levelPerdition
This related question may (or may not) be helpful: #7034563Tinkling
L
8

In Python 3 your code would work, but in Python 2 there is some wrapping that takes place when methods are looked up.

Class vs Instance

  • class level: storing counter with the function (either directly, or by using a mutable default) effectively makes it a class level attribute as there is only ever one of the function, no matter how many instances you have (they all share the same function object).

  • instance level: to make counter an instance level attribute you have to create the function in __init__, then wrap it with functools.partial (so it behaves like a normal method), and then store it on the instance -- now you have one function object for every instance.

Class Level

The accepted practice for a static-like variable is to use a mutable default argument:

class foo(object):
    ...
    def bar(self, _counter=[0]):
        _counter[0] += 1
        return _counter[0]

If you want it to be prettier you can define your own mutable container:

class MutableDefault(object):
    def __init__(self, start=0):
        self.value = start
    def __iadd__(self, other):
        self.value += other
        return self
    def value(self):
        return self.value

and change your code like so:

class foo(object):
    def bar(self, _counter=MutableDefault()):
        _counter += 1
        return _counter.value

Instance level

from functools import partial

class foo(object):
    def __init__(self):
        def bar(self, _counter=MutableDefault(1)):   # create new 'bar' each time
            value = _counter.value
            _counter += 1
            return value
        self.bar = partial(bar, self)

Summary

As you can see, readability took a serious hit when moving to instance level for counter. I strongly suggest you reevaluate the importance of emphasizing that counter is part of bar, and if it is truly important maybe making bar its own class whose instances become part of the instances of foo. If it's not really important, do it the normal way:

class foo(object):
    def __init__(self):
        self.bar_counter = 0
    def bar(self):
        self.bar_counter += 1
        return self.bar_counter
Launderette answered 1/3, 2012 at 20:52 Comment(5)
Note that MutableDefault is no where near to being a complete implementation -- that's left as an exercise for the reader. :)Launderette
The default argument is only evaluated once, so it's class-level. The OP clarified in a comment to the question that it should be instance-level.Tinkling
@Series8217: Yup, that's why I asked the question -- answer updated to show how to do it at the instance level.Launderette
New answer looks like it should work. It's practically as un-Pythonic as monkeypatching though!Tinkling
Thanks for all of this... sounds like the only way to get this done in Python. My original intention was to make the logic easier to follow, so I think I'll just end up refactoring my codePerdition
S
2

Sorry to dig up an older post but I came across it in my searches and actually found someone with a solution.

Here's a blog post that describes the issue you're having and how to create a decorator that does what you want. Not only will you get the functionality you need but the author does a great job of explaining why Python works this way.

http://metapython.blogspot.com/2010/11/python-instance-methods-how-are-they.html

Subsequent answered 26/12, 2012 at 17:57 Comment(0)
A
1

You can't add an attribute directly to an instancemethod but and instance method is a wrapper around a function and you can add an attribute to the wrapped function. ie

class foo(object):
    def bar(self):
        # When invoked bar is an instancemethod.
        self.bar.__func__.counter = self.bar.__func__.counter + 1
        return self.bar.__func__.counter
    bar.counter = 1 # At this point bar is just a function

This works but counter effectively becomes a class variable.

Accipiter answered 14/5, 2019 at 8:42 Comment(0)
S
0

If you want the counter to be persistent across all instances you can do the following, this is still not Pythonic at all.

class foo(object):
    def bar(self):
        self.bar.im_func.counter += 1
        return self.bar.im_func.counter
    bar.counter = 0
Stuccowork answered 1/3, 2012 at 20:35 Comment(0)
S
-1

You need an __init__ method to initialise the data member:

class foo(object):
    def __init__(self):
        self.counter = 0
    def bar(self):
        self.counter += 1
        return self.counter

Attaching data values directly to functions is decidedly non-Pythonic.

Straightforward answered 1/3, 2012 at 20:19 Comment(5)
I appreciate this, but my goal is to tie the counter variable to the bar method (for the reasons I stated in my question). I modified your answer to: class foo(object): def init__(self): self.bar.counter = 0 def bar(self): self.bar.counter += 1 return self.bar.counter but now I get the same AttributeError in the __init callPerdition
Doesn't tie counter to bar like the OP requested.Launderette
@EthanFurman: The OP asked for a "more pythonic" way to do this. Attaching data directly to functions is the opposite of Pythonic.Straightforward
Nevertheless, you have not provided a way to do what the OP asked, pythonic or not.Launderette
I think the OP asked for a static counter, not a per-instance counter. The question is a bit vague, though, so I don't see a reason to downvote this answer.Moonshiner

© 2022 - 2024 — McMap. All rights reserved.