Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is one of the "__get__" arguments redundant? [duplicate]

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?

like image 440
Dusan Krantic Avatar asked Oct 27 '22 19:10

Dusan Krantic


1 Answers

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)
'''
like image 134
Anthony Sottile Avatar answered Nov 15 '22 06:11

Anthony Sottile