Search code examples
pythonpython-3.xpython-descriptors

Why does this class descriptor __get__ method return self?


I am working through the O Reilley Python Cookbook and I have a question about the following code:

class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError('Expected ' + str(self.expected_type))
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

# Class decorator that applies it to selected attributes
def typeassert(**kwargs):
    def decorate(cls):
        for name, expected_type in kwargs.items():
            # Attach a Typed descriptor to the class
            setattr(cls, name, Typed(name, expected_type))
        return cls
    return decorate

# Example use
@typeassert(name=str, shares=int, price=float)
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

if __name__ == '__main__':
    s = Stock('ACME', 100, 490.1)
    print(s.name, s.shares, s.price)
    s.shares = 50
    try:
        s.shares = 'a lot'
    except TypeError as e:
        print(e)

Im confused about the part:


def __get__(self, instance, cls):
    if instance is None:
        return self
    else:
        return instance.__dict__[self.name]

if instance is not set (ie None) then it says return 'self', given that self represents the class descriptor what exactly is returned?


Solution

  • Yes, it returns the descriptor instance.

    The second argument (the first after self) for __get__ is either the instance on which the descriptor is looked up - or None if it's looked up on the class.

    So in the given case it returns the descriptor in case you look up the descriptor on the class.

    Just to illustrate that:

    class Descriptor:
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return 10
    
    class Test:
        test = Descriptor()
    
    
    >>> Test.test
    <__main__.Descriptor at 0x2769b7d44c8>
    
    >>> Test.__dict__['test']
    <__main__.Descriptor at 0x2769b7d44c8>
    

    Now, if it didn't use return self there it would look like this:

    class Descriptor:
        def __get__(self, instance, owner):
            return 10
    
    class Test:
        test = Descriptor()
    
    
    >>> Test.test
    10
    
    >>> Test.__dict__['test']
    <__main__.Descriptor at 0x2769b7de208>
    

    The reason this return self is often done is because it allows to get the descriptor instance without having to search in __dict__ (potentially in all superclasses). In most cases it simply makes no sense to do anything when the property is looked up on the class, so returning the instance is a good idea.

    It's also what the built-in property does (and also the function-descriptor):

    class A:
        @property
        def testproperty(self):
            return 10
    
        def testmethod(self):
            return 10
    
    >>> A.testproperty
    <property at 0x2769b7db9a8>
    >>> A.testproperty.__get__(None, A)
    <property at 0x2769b7db9a8>
    
    >>> A.testmethod
    <function __main__.A.testmethod(self)>
    >>> A.testmethod.__get__(None, A)
    <function __main__.A.testmethod(self)>
    

    In those cases where something meaningful should happen when the attribute is looked up on the class (for example the built-in staticmethod and classmethod descriptors) then that's of course different and self should not be returned there!