Function instance variables inside a class

2019-07-27 09:31发布

问题:

I'm trying to implement a so-called static variable in my method, similar to the decorator method described in this Stackoverflow thread. Specifically, I define a decorator function as follows:

def static_var(varName, value):
    def decorate(function):
        setattr(function,varName,value)
        return function
    return decorate

As the example shows, this can be used to attach a variable to the function:

@static_var('seed', 0)
def counter():
    counter.seed +=1
    return counter.seed

This method will return the number of times it has been called.

The issue i am having is that this does not work if I define the method inside a class:

class Circle(object):

    @static_var('seed',0)
    def counter(self):
        counter.seed +=1
        return counter.seed

If I instantiate a Circle and run counter,

>>>> myCircle = Circle()
>>>> myCircle.counter()

I get the following error: NameError: global name 'counter' is not defined.

My response to this was that maybe I need to use self.counter, i.e.

class Circle(object):

    @static_var('seed',0)
    def counter(self):
        self.counter.seed +=1
        return self.counter.seed

However this produces the error, AttributeError: 'instancemethod' object has no attribute 'seed'.

What is going on here?

回答1:

You want to access the function object, but you are instead accessing a method. Python treats functions on instances and classes as descriptors, returning bound methods at lookup time.

Use:

@static_var('seed',0)
def counter(self):
    self.counter.__func__.seed += 1

to reach the wrapped function object.

In Python 3, you can also access the function object on the class:

@static_var('seed',0)
def counter(self):
    Circle.counter.seed += 1

In Python 2 that'd still return an unbound method object (a method without an instance attached).

Of course, just because you can do this, does not necessarily make it a good idea. With a method you have a class, giving you an alternative location to store that counter. You could put it on either Counter or on type(self), where the latter would give you a counter per subclass.



回答2:

What you're trying to achieve looks like something you shouldn't be doing at all.

In the first case, you can just as easily get away with a much simpler:

def counter():
    counter.seed += 1
    return counter
counter.seed = 0

And in the second case, you can just as easily put the "function state" in the class.

class Circle(object):
    seed = 0

    # if you want the count to be unique per instance
    def counter_inst(self):
        self.seed += 1
        return self.seed

    # if you want the count to be shared between all instances of the class
    @classmethod
    def counter_cls(cls):
        cls.seed += 1
        return cls.seed


回答3:

The problem is that class methods are descriptor objects, not functions. You can use the same decorator for both types of callables, in Python v2.6 on including v3.x, if you do a little more work in methods. Here's what I mean:

def static_var(var_name, value):
    def decorator(function):
        setattr(function, var_name, value)
        return function
    return decorator

# apply it to method
class Circle(object):
    @static_var('seed', 0)
    def counter(self):
        counter_method = Circle.counter.__get__(self, Circle).__func__  # added
        counter_method.seed +=1
        return counter_method.seed

myCircle = Circle()
print(myCircle.counter())  # 1
print(myCircle.counter())  # 2

What the method version does is call the descriptor's__get__method to get a bound method instance object, and then accesses its__func__attribute to get the actual function instance which has the named attribute attached to it.

For versions of Python before 2.6, you would need to useim_funcinstead of__func__.

Update:

Most of the issues noted can be avoided by changing the decorator so that it adds an argument to the beginning of the call and writing the decorated functions to refer to that rather than to themselves to access the variables. Another nice thing is this approach works in both Python 2.x and 3.x:

def static_var(var_name, value):
    def decorator(function):
        static_vars = getattr(function, 'static_vars', None)
        if static_vars:  # already have a container?
            setattr(static_vars, var_name, value)  # add another var to it
            return function
        else:
            static_vars = type('Statics', (object,), {})()  # create container
            setattr(static_vars, var_name, value)  # add first var to it
            def decorated(*args, **kwds):
                return function(static_vars, *args, **kwds)
            decorated.static_vars = static_vars
            return decorated
    return decorator

@static_var('seed', 0)  # apply it to a function
def counter(static_vars):
    static_vars.seed +=1
    return static_vars.seed

print(counter())  # 1
print(counter())  # 2

class Circle(object):
    @static_var('seed', 0)  # apply it to a method
    def counter(static_vars, self):
        static_vars.seed +=1
        return static_vars.seed

myCircle = Circle()
print(myCircle.counter())  # 1
print(myCircle.counter())  # 2

This decorator allows adding more than one static:

@static_var('seed', 0)  # add two of them to a function
@static_var('offset', 42)
def counter2(static_vars):
    static_vars.seed += 1
    static_vars.offset *= 2
    return static_vars.seed + static_vars.offset

print(counter2())  # 1 + 2*42 = 85
print(counter2())  # 2 + 2*84 = 170


回答4:

May I present another alternative which might be a bit nicer to use and will look the same for both methods and functions:

@static_var2('seed',0)
def funccounter(statics, add=1):
    statics.seed += add
    return statics.seed

print funccounter()       #1
print funccounter(add=2)  #3
print funccounter()       #4

class ACircle(object):
    @static_var2('seed',0)
    def counter(statics, self, add=1):
        statics.seed += add
        return statics.seed

c = ACircle()
print c.counter()      #1
print c.counter(add=2) #3
print c.counter()      #4
d = ACircle()
print d.counter()      #5
print d.counter(add=2) #7
print d.counter()      #8

If you like the usage, here's the implementation:

class StaticMan(object):
    def __init__(self):
        self.__dict__['_d'] = {}

    def __getattr__(self, name):
        return self.__dict__['_d'][name]
    def __getitem__(self, name):
        return self.__dict__['_d'][name]
    def __setattr__(self, name, val):
        self.__dict__['_d'][name] = val
    def __setitem__(self, name, val):
        self.__dict__['_d'][name] = val

def static_var2(name, val):
    def decorator(original):
        if not hasattr(original, ':staticman'):    
            def wrapped(*args, **kwargs):
                return original(getattr(wrapped, ':staticman'), *args, **kwargs)
            setattr(wrapped, ':staticman', StaticMan())
            f = wrapped
        else:
            f = original #already wrapped

        getattr(f, ':staticman')[name] = val
        return f
    return decorator


标签: python oop