Search code examples
pythonpython-descriptors

How does a descriptor with __set__ but without __get__ work?


I read somewhere about the fact you can have a descriptor with __set__ and without __get__.

How does it work?

Does it count as a data descriptor? Is it a non-data descriptor?

Here is a code example:

class Desc:
    def __init__(self, name):
        self.name = name
    def __set__(self, inst, value):
        inst.__dict__[self.name] = value
        print("set", self.name)

class Test:
    attr = Desc("attr")

>>>myinst = Test()
>>> myinst.attr = 1234
set attr
>>> myinst.attr
1234
>>> myinst.attr = 5678
set attr
>>> myinst.attr
5678

Solution

  • The descriptor you've given in the example is a data descriptor.

    Upon setting the attribute, as any other data descriptor, it takes the highest priority and is called like so:

    type(myinst).__dict__["attr"].__set__(myinst, 1234)
    

    This in turn, adds attr to the instance dictionary according to your __set__ method.

    Upon attribute access, the descriptor is checked for having the __get__ method but fails, causing for the search to be redirected to the instance's dictionary like so:

    myinst.__dict__["attr"]
    

    If it is not found in the instance dictionary, the descriptor itself is returned.

    This behavior is shortly documented in the data model like so:

    If it does not define __get__(), then accessing the attribute will return the descriptor object itself unless there is a value in the object’s instance dictionary.

    Common usecases include avoiding {instance: value} dictionaries inside the descriptors, and caching values in an efficient way.


    In Python 3.6, __set_name__ was added to the descriptor protocol thus eliminating the need for specifying the name inside the descriptor. This way, your descriptor can be written like so:

    class Desc:
        def __set_name__(self, owner, name):
            self.name = name
        def __set__(self, inst, value):
            inst.__dict__[self.name] = value
            print("set", self.name)
    
    class Test:
        attr = Desc()