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)
'''