trinth
trinth

Reputation: 6047

How to use a decorator to modify mutable class variables?

I have the following code:

class X(object):
    items = []

class Y(X):
    pass

class Z(X):
    pass

I'm trying to create a decorator that will add an object only to Y's "item" list. So the following code,

@addItems
class Y(X):
    pass

should append something (for sake of example, let's say the integer 1) to Y.items but not modify X.items, Z.items, or any other subclasses of X. Basically, Y.items should have items = [1] but X.items and Z.items should both have items = []. This is the decorator function definition I created that was supposed to achieve that goal:

def addItems(cls):
    cls.items.append(1)
    return cls

This does not work though - when I use the decorator, it ends up modifying the "items" list for X and all other subclasses of X. What gives? I know that class attributes in Python are shared among all instances of X, however, I did not know that subclasses were affected. Perhaps this behavior occurs only for mutable attributes?

What is the correct way of achieving my goal?

Upvotes: 1

Views: 184

Answers (1)

BrenBarn
BrenBarn

Reputation: 251428

The issue is not with the decorator. You have a misunderstanding about where/how the items attribute is stored. Y and Z do not have their own items attribute. Rather, there is only one items attribute. You defined it on X, and if you do Y.items (or Z.items), since the attribute is not found on the subclass, it is dynamically looked up on the superclass. If you do X.items is Y.items you will see that it evalutes to True --- there is only one items list. That means that when you modify it, all classes see the modification.

If you want to give each class its own items list, you have to, well, give each class its own items list:

class X(object):
    items  = []

class Y(object):
    items  = []

class Z(object):
    items  = []

Python never copies anything unless you tell it to. Just because you define a subclass that inherits from a class with an attribute, that doesn't mean Python makes a new copy of the attribute for the subclass; it just re-uses the same object from the superclass.

If you want to "magically" give each class its own items attribute, you could do it inside the decorator:

def addItems(cls):
    cls.items = []
    cls.items.append(1)
    return cls

This first sets the class's items to an empty list. This actually sets the attribute on the particular class it operates on, not on any sub- or superclass. Now when you access that, it will work as you expect.

Upvotes: 2

Related Questions