2

Consider the following code:

class A(object):
    def do(self):
        print self.z

class B(A):
    def __init__(self, y):
        self.z = y

b = B(3)
b.do()

Why does this work? When executing b = B(3), attribute z is set. When b.do() is called, Python's MRO finds the do function in class A. But why is it able to access an attribute defined in a subclass?

Is there a use case for this functionality? I would love an example.

donatello
  • 5,727
  • 6
  • 32
  • 56

4 Answers4

3

It works in a pretty simple way: when a statement is executed that sets an attribute, it is set. When a statement is executed that reads an attribute, it is read. When you write code that reads an attribute, Python does not try to guess whether the attribute will exist when that code is executed; it just waits until the code actually is executed, and if at that time the attribute doesn't exist, then you'll get an exception.

By default, you can always set any attribute on an instance of a user-defined class; classes don't normally define lists of "allowed" attributes that could be set (although you can make that happen too), they just actually set attributes. Of course, you can only read attributes that exist, but again, what matters is whether they exist when you actually try to read them. So it doesn't matter if an attribute exists when you define a function that tries to read it; it only matters when (or if) you actually call that function.

In your example, it doesn't matter that there are two classes, because there is only one instance. Since you only create one instance and call methods on one instance, the self in both methods is the same object. First __init__ is run and it sets the attribute on self. Then do is run and it reads the attribute from the same self. That's all there is to it. It doesn't matter where the attribute is set; once it is set on the instance, it can be accessed from anywhere: code in a superclass, subclass, other class, or not in any class.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
1

Since new attributes can be added to any object at any time, attribute resolution happens at execution time, not compile time. Consider this example which may be a bit more instructive, derived from yours:

class A(object):
  def do(self):
    print(self.z) # references an attribute which we have't "declared" in an __init__()

#make a new A
aa = A()

# this next line will error, as you would expect, because aa doesn't have a self.z
aa.do()

# but we can make it work now by simply doing
aa.z = -42
aa.do()

The first one will squack at you, but the second will print -42 as expected.

Python objects are just dictionaries. :)

Travis Griggs
  • 21,522
  • 19
  • 91
  • 167
  • They're quite a lot fancier than dictionaries, in more than one regard ;-) Consider attribute lookup, `__slots__`, the fact that dictionaries are objects yet don't mix up their attributes with their keys (I'm looking at you JS), type/class tagging, immutable types, attribute lookup (yes, again -- there's a ton of subtle issues, like descriptors and some magic method calls bypassing lookup on the instance), etc. But yes, most objects are (almost) as nondiscriminatory in what "keys" and values can be added as dictionaries are. –  Dec 29 '12 at 00:56
0

When retrieving an attribute from an object (print self.attrname) Python follows these steps:

  1. If attrname is a special (i.e. Python-provided) attribute for objectname, return it.

  2. Check objectname.__class__.__dict__ for attrname. If it exists and is a data-descriptor, return the descriptor result. Search all bases of objectname.__class__ for the same case.

  3. Check objectname.__dict__ for attrname, and return if found. If objectname is a class, search its bases too. If it is a class and a descriptor exists in it or its bases, return the descriptor result.

  4. Check objectname.__class__.__dict__ for attrname. If it exists and is a non-data descriptor, return the descriptor result. If it exists, and is not a descriptor, just return it. If it exists and is a data descriptor, we shouldn't be here because we would have returned at point 2. Search all bases of objectname.__class__ for same case.

  5. Raise AttributeError

    Source

    Understanding get and set and Python descriptors

Community
  • 1
  • 1
Alex Granovsky
  • 2,996
  • 1
  • 15
  • 10
-1

Since you instanciated a B object, B.__init__ was invoked and added an attribute z. This attribute is now present in the object. It's not some weird overloaded magical shared local variable of B methods that somehow becomes inaccessible to code written elsewhere. There's no such thing. Neither does self become a different object when it's passed to a superclass' method (how's polymorphism supposed to work if that happens?).

There's also no such thing as a declaration that A objects have no such object (try o = A(); a.z = whatever), and neither is self in do required to be an instance of A1. In fact, there are no declarations at all. It's all "go ahead and try it"; that's kind of the definition of a dynamic language (not just dynamic typing).

That object's z attribute present "everywhere", all the time2, regardless of the "context" from which it is accessed. It never matters where code is defined for the resolution process, or for several other behaviors3. For the same reason, you can access a list's methods despite not writing C code in listobject.c ;-) And no, methods aren't special. They are just objects too (instances of the type function, as it happens) and are involved in exactly the same lookup sequence.

1 This is a slight lie; in Python 2, A.do would be "bound method" object which in fact throws an error if the first argument doesn't satisfy isinstance(A, <first arg>).

2 Until it's removed with del or one of its function equivalents (delattr and friends).

3 Well, there's name mangling, and in theory, code could inspect the stack, and thereby the caller code object, and thereby the location of its source code.