6

As described here:

https://docs.python.org/3/reference/datamodel.html#object.__get__

The two arguments ('self' excluded) passed to the __get__ method are the object and a class through which the attribute was accessed, respectively. Isn't the second argument redundant? Furthermore, why is there a need to make a distinction between object and class access when 'classes' are also objects?

So, to me it looks like there are two possibilities:

  • Attribute gets accessed from an object, in which case the owner argument will be equal to type(instance), so it brings no new information
  • Attribute gets accessed from a class (an object of 'type'), in which case the source object just sits in the owner argument with the instance being None

It looks to me like the same functionality could be achieved if only one argument was used (for example instance) which will always hold the originating object, regardless of whether it is a "class" or not. If that information is really needed, one could just check using isinstance(instance, type).

So, why the need for both arguments?

Dusan Krantic
  • 119
  • 1
  • 8
  • 2
    I'd say it has to do with subclasses. That is, you define a descriptor in `class A`, then create a subclass `class B(A)`, then create an object `b = B()` and use the descriptor on that object. Depending on the semantics, you may need to know in which exact type it was defined. – rodrigo Aug 12 '20 at 16:49
  • 1
    @rodrigo I tried your example in python3. When accessed from `a=A()`, the owner argument is class A; when accessed from `b=B()` the owner argument is class B. It doesn't look like owner holds the class the descriptor is defined in, it just holds the type of the first argument. – Dusan Krantic Aug 12 '20 at 21:19
  • That's because subclasses define a "is-a" relationship. Class `B` automatically contains everything class `A` does, plus more. – Mark Ransom Aug 12 '20 at 22:52
  • I've just tested, and you are right! I still think it has to do with subclasses, though. Maybe you can use this extra argument to chain to base class, something like the implementation of `super` for descriptors. – rodrigo Aug 13 '20 at 08:27

1 Answers1

4

The reason they are separate comes from the original prose in PEP 252

__get__(): a function callable with one or two arguments that retrieves the attribute value from an object. This is also referred to as a "binding" operation, because it may return a "bound method" object in the case of method descriptors. The first argument, X, is the object from which the attribute must be retrieved or to which it must be bound. When X is None, the optional second argument, T, should be meta-object and the binding operation may return an unbound method restricted to instances of T. When both X and T are specified, X should be an instance of T. Exactly what is returned by the binding operation depends on the semantics of the descriptor; for example, static methods and class methods (see below) ignore the instance and bind to the type instead.

in other words, the two arguments allow for differentiation between an "unbound" descriptor (one called upon the class) and a "bound" descriptor (one called upon the instance). one example of where you see this often but don't really think about it is classmethod (which uses the owner parameter and ignores the instance parameter).

If you're always using "bound" descriptors, you're right the owner is a bit redundant since instance should be an instance of that type.

Perhaps easier to see is a classmethod descriptor implemented in pure python:

class MyClassmethod(object):
    def __init__(self, func):
        self._func = func

    def __get__(self, instance, owner = None):
        # instance is ignored, `owner` is bound to the first arg
        return self._func.__get__(owner)


class C:
    @MyClassmethod
    def func(cls, x):
        print(cls)
        print(x)

C.func(1)
C().func(2)

OUTPUT = '''\
$ python3 t.py 
<class '__main__.C'>
1
<class '__main__.C'>
2
'''

or consider this (somewhat incomplete) cached_class_property:

class cached_class_property:
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, obj, owner):
        val = self.fget(owner)
        setattr(owner, self.fget.__name__, val)
        return val


class C:
    @cached_class_property
    def f(self):
        print('calculating...')
        return 42


print(C.f)
print(C().f)

OUTPUT = '''\
$ python3 t.py
calculating...
42
42
'''

note that since python3, "unbound" and "bound" methods aren't really a concept any more, but the api persists at the descriptor level -- notably functions on classes no longer validate that the type of the instance matches the owner:

class C:
    def d(self):
        print(self)

class D:
    pass

C().d()
C.d(D())

OUTPUT = '''\
$ python3 t.py
<__main__.C object at 0x7f09576d3040>
<__main__.D object at 0x7f09576d3040>

$ python2 t.py
<__main__.C instance at 0x7efe2c8a7910>
Traceback (most recent call last):
  File "t2.py", line 9, in <module>
    C.d(D())
TypeError: unbound method d() must be called with C instance as first argument (got D instance instead)
'''
anthony sottile
  • 61,815
  • 15
  • 148
  • 207
  • "in other words, the two arguments allow for differentiation between an "unbound" descriptor (one called upon the class) and a "bound" descriptor (one called upon the instance). Wouldn’t a single `instance` argument also allow such a distinction, thanks to the test `isinstance(instance, type)` the OP suggested? – Géry Ogam Mar 28 '21 at 17:12
  • @Maggyero no -- a class call will have `instance=None` -- with only a single argument you wouldn't be able to recover the class being called upon – anthony sottile Mar 28 '21 at 18:06
  • I should have been more explicit but like the OP, my question assumes that the `instance` argument is always bound to the originating object, not to `None`, regardless of whether the originating object is a class instance or a class (i.e. a metaclass instance). If that information is really needed, one could just check using `isinstance(instance, type)`. Do you see our point? – Géry Ogam Mar 28 '21 at 19:22
  • no, `instance` will be `None` if accessed from not-an-instance -- assuming it will always be bound to an originating object is a broken assumption. when accessed from a class `instance` will be `None`. please reread my answer if you're confused – anthony sottile Mar 28 '21 at 19:29
  • Yes we know that Guido chose to bind `None` to the `instance` parameter during attribute lookup *from a class* and therefore had to introduce an extra `owner` parameter. But the question the OP asked is why, couldn’t he have chosen to always bind the originating object to the `instance` parameter? If yes, it would have been equivalent but simpler, please confirm. If no, please explain why. – Géry Ogam Mar 28 '21 at 19:48
  • 3
    that's a separate question entirely, but if you must, a descriptor attached to a metaclass would have no way to differentiate (as metaclasses are instances of `type`) – anthony sottile Mar 28 '21 at 20:00
  • Very interesting remark! A descriptor can be attached to a class (in which case you want to distinguish between an attribute lookup from the class and from an instance of the class), but also, as you noted—I had never thought about it—, to a metaclass (in which case you want to distinguish between an attribute lookup from the metaclass and from an instance of the metaclass). You can make the distinction with `isinstance(instance, type)` in the first case. But you can also make the distinction in the second case, with `issubclass(instance, type)`! So this is not a valid argument for `owner`. – Géry Ogam Mar 28 '21 at 22:21
  • However I am realizing that for a descriptor attached to a metaclass, the test `issubclass(instance, type)` will not allow the distinction between an attribute lookup from the metaclass and from an instance of the metaclass if the latter is itself a metaclass. So your argument was actually valid, the `owner` parameter is necessary! And it is a shame that only the `__get__` method has it, not the `__set__` and `__delete__` methods. Thanks a lot Anthony for finding the decisive argument supporting `owner`. – Géry Ogam Mar 29 '21 at 13:03
  • Do you know the reason why Guido omitted `owner` for `__set__` and `__delete__`? – Géry Ogam Mar 29 '21 at 14:34