0

I'm having trouble understanding what is going on to make a difference in static object and member objects (those created in constructor).

The following will run the overridden get():

class A(object):
    class B(object):
        def __init__(self, initval=None, name='var'):
            self.val = initval
            self.name = name

        def __get__(self, obj, objtype):
            print('B Retrieving', self.name)
            return self.val

    b = B(10, 'var "b"')

But, if I pull b in to the constructor it does not:

class A(object):
    class B(object):
        def __init__(self, initval=None, name='var'):
            self.val = initval
            self.name = name

        def __get__(self, obj, objtype):
            print('B Retrieving', self.name)
            return self.val

    def __init__(self)):
        self.b = A.B(10, 'var "b"')

I really want to make this work in the latter and maybe this isn't the right way to do it.

Can someone please explain what is going on here in a call to print(a.b) where a = A()?

Also, is there a way to have print(a.b) call a member function from b?

devanl
  • 1,232
  • 1
  • 10
  • 20
  • 1
    Nested classes will almost never do and work the way you expect them to. Don’t use them unless you absolutely know what you are doing. – poke Jul 17 '16 at 22:14

2 Answers2

2

By implementing __get__, you turned your class B into a descriptor class. Descriptors are objects that take care of attribute access by performing custom logic on an instance.

In order to make descriptors work, you need to declare them as members on the type. Only then will Python call __get__ and __set__ methods of the object properly.

The reason why doing self.b = SomeDescriptor() does not work is because by assinging something to self.b, you are directly changing the object’s underlying __dict__:

>>> class A(object): pass
>>> x = A()
>>> x.b = B(10, '')
>>> x.__dict__
{'b': <__main__.B object at 0x000000437141F208>}

As you can see x.b has the value of that B object. That is the descriptor object. And when you just try to access x.b, you just get that descriptor object back. The __get__ is never called.

When you set the descriptor on the type however, a member b does not exist in the object’s __dict__, so Python will look further up in the inheritance chain and will find the descriptor object for b at which point it will also execute the descriptor:

>>> class A(object): pass
>>> A.b = B(10, '')
>>> x = A()
>>> x.__dict__
{}
>>> x.b
B Retrieving 
10
Community
  • 1
  • 1
poke
  • 369,085
  • 72
  • 557
  • 602
  • It's not *quite* correct to say that Python checks the class dict only after the object dict (though it is mostly right in the current situation). Python actually checks the class dict first, and if there's a data descriptor (like a `property`), it takes precedence over any instance variable of the same name. Non-data descriptors (that is, descriptors that have a `__get__` method, but not a `__set__` or `__delete__`) and other class variables that are not descriptors at all have lower precedence than instance variables. – Blckknght Jul 20 '16 at 01:52
0

Your b object is a descriptor. That means that Python treats it differently than most other objects when it is bound as a class variable. Specifically, in your class-variable code, A().b gets turned into the function call A.__class__.__dict__["b"].__get__(A(), A). Python uses descriptors all over internally. It's part of how method binding and most __magic__ methods work.

When a descriptor is stored as an instance variable no special treatment is given. Lookup up a instance variable that's a descriptor just gets you the descriptor object, not the result of calling its __get__ method.

If you need to be able to set the value that your descriptor returns on a per-instance basis, you may want to have the descriptor check an attribute on the instance it's been called on, rather than an attribute on itself:

class B(object):
    def __init__(self, name='var'):
        self.name = name

    def __get__(self, obj, objtype):
        print('B Retrieving', self.name)
        return obj._bval      # lookup the value on the object, not on the descriptor

class A(object):
    b = B()                   # assign the descriptor as an class variable
    def __init__(self):
        self._bval = 10       # initialize the value

Note that I've unnested your classes, mostly just for clarity. It wasn't strictly wrong to have the B class defined within A, but it was confusing and offered no real benefits over making B a top-level class.

Blckknght
  • 100,903
  • 11
  • 120
  • 169