Search code examples
pythonpython-descriptors

__set_name__ execution in Descriptor


I have came across a code that it is including descriptors. As I understand, __set_name__ is a method that is called when the class is created. Then, if the class is called twice I'd get two calls.

In the following snippet I would expect to get the call in __set_name__ twice, but I am getting just one call. Why this behavior?

class SharedAttribute:
    def __init__(self, initial_value=None):
        self.value = initial_value
        self._name = None
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        if self.value is None:
            raise AttributeError(f'{self._name} was never set')
        return self.value

    def __set__(self, instance, new_value):
        self.value = new_value

    def __set_name__(self, owner, name):
        print(f'{self} was named {name} by {owner}')
        self._name = name


class GitFetcher:
    current_tag = SharedAttribute()
    current_branch = SharedAttribute()

    def __init__(self, tag, branch=None):
        self.current_tag = tag
        self.current_branch = branch
    
    @property
    def current_tag(self):
        if self._current_tag is None:
            raise AttributeError("tag was never set")
        return self._current_tag

    @current_tag.setter
    def current_tag(self, new_tag):
        self.__class__._current_tag = new_tag

    def pull(self):
        print(f"pulling from {self.current_tag}")
        return self.current_tag


f1 = GitFetcher(0.1)
f2 = GitFetcher(0.2)
f1.current_tag = 0.3
f2.pull()
f1.pull()

During the previous execution, __set_name__ is called with current_branch, but not called with current_tag. Why this distinction? The only call is this one:

<__main__.SharedAttribute object at 0x047BACB0> was named current_branch by <class '__main__.GitFetcher'>

Solution

  • TL;DR By the time __set_name__ methods are called, current_tag refers to an instance of property, not an instance of SharedAttribute.


    __set_name__ is called after the class has been defined (so that the class can be passed as the owner argument), not immediately after the assignment is made.

    However, you changed the value of current_tag to be a property, so the name is no longer bound to a SharedAttribute instance once the class definition has completed.'

    From the documentation (emphasis mine):

    Automatically called at the time the owning class owner is created.

    The SharedAttribute instance is created while the body of the class statement is being executed. The class itself is not created until after the body is executed; the result of executing the body is a namespace which is passed as an argument to the metaclass that creates the class. During that process, the class attributes are scanned for values with a __set_name__ method, and only then is the method called.

    Here's a simpler example:

    class GitFetcher:
        current_branch = SharedAttribute()
        current_tag = SharedAttribute()
        current_tag = 3
    

    By the time GitFetcher is defined, current_tag is no longer bound to a descriptor, so no attempt to call current_tag.__set_name__ is made.

    It's not clear if you want to somehow compose a property with a SharedAttribute, or if this is just an inadvertent re-use of the name current_tag.