Python Descriptor — A Thorough Guide

Learn about Descriptor Protocol, Data vs. Non-data descriptor, look-up chain, mechanism behind functions, and the __getattribute__that empowers the descriptors at the back

Photo by Joseph Gonzalez on Unsplash
__get__(self, obj, type=None) -> object
__set__(self, obj, value) -> None
__delete__(self, obj) -> None
class DataDescriptor(object):
def __init__(self):
self.value = 0

def __get__(self, obj, type):
print("__get__")
return self.value

def __set__(self, obj, value):
print(" __set__")
try:
self.value = value
except AttributeError:
print(f"Can not set value {value}")
def __delete__(self, obj):
print(" __del__")

class Example():
attr = DataDescriptor()

d = DataDescriptor()
e = Example()
e.attr # 0, __get__
e.attr = "new attribute" #__set__
del e.attr # __del__
print(d.__dict__)
# {'value': 0}
print(e.__dict__)
# {}
print(Example.__dict__)
# {'__module__': '__main__', 'attr': <__main__.DataDescriptor object at 0x7f1635e58940>, '__dict__': <attribute '__dict__' of 'Example' objects>, '__weakref__': <attribute '__weakref__' of 'Example' objects>, '__doc__': None}

Lookup chain

e.attr
(type(e).__dict__['attr'].__get__(e, type(e)))
#__get__
e.attr = "another new attribute"
(type(e).__dict__['attr'].__set__(e, type(e)),"another new attribute")
# __set__
def getattr_hook(obj, name):
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name) # __getattr__

Non-data descriptor

class NonDataDescriptor():
def __init__(self):
self.value = 0
def __get__(self, obj, type):
print(" __get__")
return self.value + 1
class Example():
attr = NonDataDescriptor()
e = Example()
d = NonDataDescriptor()
print(e.attr) # __get__ 1
print(e.attr) # __get__ 2
print(e.__dict__) # {}
print(d.__dict__) # {"value": 2}
e.attr = 4
print(e.attr) # 4
print(e.__dict__) # {'attr': 4}
print(d.__dict__) # {'value': 0}

Functions and Methods

class Function:    def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return MethodType(self, obj)
class D:
def f(self, x):
return x
1. dotted access from class from dictionary --> function
>>>
D.__dict__['f']
<function D.f at 0x00C45070>
2. dotted access from class --> function
>>>
D.f
<function D.f at 0x00C45070>
3. dotted access from instance --> bound function
>>>
d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
Internally, the bound method stores the underlying function and the bound instance:>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>

Property vs Descriptor

__getattribute__

>>> class Example():
def __init__(self, valid_attr):
self.valid_attr=valid_attr
>>> e = Example("valid")>>> print(e.__dict__)
{'valid_attr': 'valid'}
>>> print(e.valid_attr)
valid
>>> print(e.invalid_attr)
AttributeError: 'Example' object has no attribute 'invalid_attr'
>>> class Example():
def __init__(self, valid_attr):
self.valid_attr=valid_attr
def __getattr__(self, attr):
self.__dict__[attr]= "this is invalid"
return "this is indeed invalid"
>>> e = Example("valid")>>> print(e.__dict__)
{'valid_attr': 'valid'}
>>> print(e.valid_attr)
valid
>>> print(e.invalid_attr)
this is indeed invalid
>>> print(e.__dict__)
{'valid_attr': 'valid', 'invalid_attr': 'this is invalid'}
>>> class Example():
def __init__(self, valid_attr):
self.valid_attr=valid_attr
def __getattribute__(self, attr):
return "this is indeed invalid"
>>> e = Example("valid")>>> print(e.__dict__)
this is indeed invalid
>>> print(e.valid_attr)
this is indeed invalid
>>> print(e.invalid_attr)
this is indeed invalid
def __getattribute__(self, attr):
if attr == “invalid”:
return “this is indeed invalid"
else:
return object.__getattribute__(self,attr)
# same as----- super().__getattribute__(attr)
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype)
# data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name]
# instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype)
# non-data descriptor
if cls_var is not null:
return cls_var
# class variable
raise AttributeError(name)

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store