Search code examples
pythonpython-3.6python-descriptors

What is the right way to implement descriptors in Python 3.6+?


In Raymond Hettingers Mental Game on YouTube:

class Validator:

    def __set_name__(self, owner, name):
        self.private_name = f'_{name}'    

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

Daw-Ran Liou states in Writing descriptors in Python 3.6+:

[...] instead of using builtin function getattr and setattr, we need to reach into the dict object directly, because the builtins would be intercepted by the descriptor protocols too and cause the RecursionError.

class Validator:

    def __set_name__(self, owner, name):
        self.name= name
    
    def __get__(self, obj, objtype=None):
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        self.validate(value)
        obj.__dict__[self.name] = value

But Matthew Egans Describing Descriptors on YouTube says:

from weakref import WeakKeyDictionary

class Validator:
    def __init__(self):
        self.data = WeakKeyDictionary()

    def __get__(self, obj, owner):
        return self.data[obj]

    def __set__(self, obj, value):
        self.validate(value)
        self.data[obj] = value

What would be the correct way to implement descriptors?


Solution

  • The first example is fine. All three are valid implementations.

    Not sure why the second author says you can't use getattr in that way. Yes, getattr invokes the descriptor protocol, but the descriptor is being assigned to type(obj).__dict__[name] but you set private_name as f'_{name}' so there won't be any infinite recursion... It would if you used self.private_name = name in __set_name__ instead of self.private_name = f'_{name}', but that isn't what either of the first two are doing...

    EDIT: reading that link, that is what the author is doing...

    That being said, the second solution isn't incorrect.

    As to the third solution, I surmise that this is an alternative, which doesn't pollute the instances namespace at all, keeping a separate namespace -- the WeakKeyDictionary. Not a bad idea, but it isn't any more "correct" than the other two. Note, it does assume the the class hashes based on identity, which isn't the case. You can implement __hash__ in a class to hash based on something else, which is fine if your class is "conceptually" immutable, e.g. some Point(x, y) class which doesnt' expose any mutator methods, and that hashes based on the values of x and y. So, that would make this approach a little more restrictive, but otherwise, it's a clever solution to not messing with the instances namespace.

    I'd opine that the first is the most Pythonic in the sense that it is idiomatic. But again, all three are valid solutions.