Re: constructor vs initializer assignments

Tim Peters (tim@ksr.com)
Tue, 08 Mar 94 00:15:29 EST

> What exactly are the rules concerning scope of instance and class
> attributes? They don't seem to follow the same rules as scope of other
> variables.

I looked thru the docs, & was surprised to see that the lookup rules for
data attributes (as opposed to method attributes) don't appear to be
completely spelled out! They're very much alike, though.

You're right that they don't follow the same rules as for plain
variables, and this is a feature. Forget "scope" here! Attribute lookup
in Python is (at present) quite dynamic (although few programs take
advantage of (abuse?) the enormous flexibility Python currently allows).

The mapping of attribute name -> attribute value is represented as a
Python dictionary, available for inspection as the special attribute
__dict__. If you pepper your examples with dumps of the __dict__s,
you'll soon see exactly what's going on. E.g., running

class foo:
bar = 10
def __init__(self):
print foo.__dict__, self.__dict__
self.bar = foo.bar
print foo.__dict__, self.__dict__
foo.bar = foo.bar + 1
print foo.__dict__, self.__dict__

a = foo()

prints

{'__init__': <function __init__ at 65960>, 'bar': 10} {}
{'__init__': <function __init__ at 65960>, 'bar': 10} {'bar': 10}
{'bar': 11, '__init__': <function __init__ at 65960>} {'bar': 10}

So:
class objects (may) have attributes
instance objects (may) have attributes
a class object's attributes are (initially) defined by the class's
local namespace at the time the defining "class ...:" block was
executed
a newly-created instance has _no_ attributes
an instance never has an attribute unless and until you assign
it an attribute via instance.attribute = some_value (there are
other ways, but let's pretend ...). And such an assignment has no
effect on any class attribute.

When you reference the value of (as opposed to assign to)
instance.attr

if instance has attribute attr, its value is used. Else the instance's
class is searched for that attr, and if found its value is used. Else
the instance's class's base classes (if any) are searched for that attr,
in left-to-right depth-first order, and the first found (if any) is used.
Else AttributeError is raised.

References to class attributes also undergo a similar search; e.g.,

class foo: a = 3
class bar(foo): pass
bar.a

prints 3, even though bar.__dict__ is empty.

Note that this is the mechanism by which inheritance _works_ in Python,
so it's not really confusing if you keep the purpose in mind. And much
like a derived class can override base class methods, an instance can
override its class's data attributes (whether that's good _style_ is
another question entirely ...). The only asymmetry (between data and
method attributes) is that an _instance_ cannot override its class's
methods (it can override the _name_ of a class method with its own
function, but Python refuses to treat the new function as a method (don't
worry if that seems very obscure: it _is_ <wink>).

Note too that this prints "12" today, but I don't think Guido wants to
promise it will do so forever:

class A: pass
class B(A): pass
class C(B): pass

c = C()
A.data = 12 # give the deepest class a brand-new attribute
c.data # and the previously-created instance two levels down
# sees the change

So it's _very_ dynamic now.

> When a class has static attribute assignments outside of any method
> definitions do the assignments get made before or after the __init__
> method is run when creating a new class instance?

So far from Python's model it can't be answered in those terms <wink>.

> My guess is that if there is an explicit assignment then a new entry is
> made in the dictionary for the instance.

Right!

> If there is no assignment then the instance attribute is scoped to the
> class attribute.

"Scope" is misleading here -- it's really a runtime search through the
inheritance hierarchy now. Consider this piece of exquisite sickness:

class A: x = 1 # A.x == 1
class B: x = 2 # B.x == 2
bases = A, B

from whrandom import choice

for i in range(10):
class C(choice(bases)): pass # pick a base class at random
c = C()
print c.x, # and see which "x" we got

That printed "2 1 1 2 1 2 2 1 1 2" for me.

don't-try-that-at-home<wink>-ly y'rs - tim

Tim Peters tim@ksr.com
not speaking for Kendall Square Research Corp