Class Variable Access and Assignment

Steven D'Aprano steve at REMOVEMEcyber.com.au
Thu Nov 3 22:40:35 EST 2005


Graham wrote:

> Many thanks your explaination cleared up many of the questions I had.
> I know think i can understand the purpose, regardless of my opinion, i
> do however think that one should be able to assign the value in the
> same way it is accessed.

You mean, like this?

# access the value of an instance attribute:
x = instance.name

# assign the value of an instance attribute:
x = instance.name

No, of course that's not what you *meant* -- but it is 
what you said. So what do you actually mean? I'll 
assume you mean something like "the scoping rules for 
access and assignment should be the same".

The problem is, they are. When you access an attribute, 
Python looks in the instance scope, and if that lookup 
fails, it looks in the class scope. When you assign an 
attribute, at least ignoring the complication of slots, 
Python uses the same scoping rules: assign to the 
instance scope, WHICH ALWAYS SUCCEEDS, and if it fails, 
  assign to the class scope.

In fact, Python may not even bother to include code to 
push the assignment to the class, since the assignment 
at the instance level is guaranteed to either succeed, 
or fail in such a way that you have no choice but to 
raise an exception. But conceptually, assignment and 
access are using the same scoping rules.

(Again, I stress that new-style classes with slots may 
do things differently.)

Now that I've demonstrated that what you want is not 
either "assignment and access should be the same", nor 
"the scoping rules should be the same", can you 
describe precisely what you do want?


See below for further details.


> Given your previous example:
> 
>>class Counter(object):
>>     "A mutable counter."
>>     # implementation elided
> 
> 
>>class A(object):
>>     instance_count = Counter()
>>     def __init__(self):
>>         self.instance_count.increment()
> 
> 
> 
> if you changed
> 
>>class A(object):
>>     instance_count = Counter()
>>     def __init__(self):
>>         self.instance_count.increment()
> 
> 
> to
> 
> 
>>class A(object):
>>     instance_count = 0
>>     def __init__(self):
>>         self.instance_count = self.instance_count + 1
> 
> 
> It would not work as planned. 

Why not?

For starters, what is the plan? Do you want all 
instances of class A to share state? Are all instances 
of Counter supposed to share state?

Depending on whether you want the answers of those to 
be Yes or No, you would pick one technique or the 
other. Sometimes you want to increment mutables in 
place, and sometimes you don't.

But I would suggest very strongly that in general, you 
usually don't want instances to share state.


> I understand all the reasons why this
> occurs, but i dont understand why its implemented this way. Because it
> acts in a different way than you expect. It seems to me that
> self.instance_count should not create a new entry in the __dict__ if a
> class variable of that name is already present anywhere in that objects
> hierarchy.

But that would stop inheritance from working the 
expected way.

In standard OO programming, you expect instances to 
inherit behaviour from their class (and superclasses) 
unless over-ridden. This lets you do something like this:

class Paper:
     size = A4

Now all instances of Paper are created with a default 
size of A4 -- they inherit that size from the class.

If you are localising your application for the US 
market, you simply change the class attribute:

Paper.size = USLetter

and all the instances that inherit from the class will 
now reflect the new default.

Now suppose you have a specific instance that needs a 
different paper size:

instance = Paper()
instance.size = Foolscap

What do you expect should happen? Should all Paper 
instances suddenly be foolscap size, or just the one? 
If you say "just the one", then you want the current 
behaviour. If you say "all of them", then you want 
shared state -- but do you really want all class 
instances, all the time, to have shared state?


You can get ride of that behaviour by getting rid of 
inheritance, or at least inheritance of non-method 
attributes. Then you have to write code like this:

class PrintableThing:
     """Prints a Thing object with prefix and suffix.
     Customize the prefix and suffix by setting the
     appropriate instance attributes.
     """

     prefix = "START "
     suffix = " STOP"

     def __str__(self):
         try:
             # access the instance attributes,
             # if they exist
             prefix = self.prefix
             suffix = self.suffix
         except AttributeError:
             # fall back to class attributes
             prefix = self.__class__.prefix
             suffix = self.__class__.suffix
         # have you spotted the subtle bug in this code?
         return prefix + self.thing + suffix

instead of:

class PrintableThing:
     def __str__(self):
         return self.prefix + self.thing + self.suffix


Even worse would be the suggestion that Python allowed 
accessing instance.attribute to refer to either a class 
or instance attribute, decided at runtime as it does 
now, but *remembered* which it was so that assignment 
went back to the same object.

That would mean that class attributes would mask 
instance attributes -- or vice versa, depending on 
which was created first. I assume that in general, 
class attributes would be created before instances.

If you had a class with a default attribute, like 
Paper.size above, you couldn't over-write it at the 
instance level because instance.size would always be 
masked by class.size. You would need to write code like 
this:

class Paper:
     default_size = A4

     def __init__(self, size=None):
         self.size = size

     def print(self):
         if self.size is None:
             papersize = self.__class__.default_size
         else:
             papersize = self.size
         do_something(papersize)


The standard inheritance model used by Python and all 
OO languages I know of is, in my opinion, the optimal 
model. It gives you the most convenient behaviour for 
the majority of cases, and in those few cases where you 
want non-standard behaviour (e.g. shared state) it is 
easy to do with some variant of self.__class__.attribute.


-- 
Steven.




More information about the Python-list mailing list