Search code examples
pythonpython-descriptors

access descriptor in class local functions


I'm wondering how to access a descriptor in different functions? I can initialize speed as a descriptor when I initialize the Vehicle class, however I can't access its value within a Vehicle function (calc_speed()-> self.speed.value) returns AttributeError: 'int' object has no attribute 'value'. Also, shouldn't changing the value audi.speed = 120 trigger the set function (as well as printing should trigger the get function)?'

class SpeedDesc(object):

    def __init__(self, name, val):
        self.var_name = name
        self.value = val

    def __get__(self, obj, objtype):
        print('Getting', self.var_name)
        return self.value

    def __set__(self, obj, value):
        msg = 'Setting {name} to {value}'
        print(msg.format(name=self.var_name, value=value))
        self.value = value

class Vehicle(object):

    def __init__(self, vType):
        self.vehicle_type = vType
        self.speed = SpeedDesc('speed desc', 100)

    def calc_speed(self, accel):
        return self.speed.value * accel

if __name__ == '__main__':

    audi = Vehicle('sedan')
    print('vehicle speed:', audi.speed.value)
    audi.speed = 120
    print(audi.calc_speed(1.5))
vehicle speed: 100
Traceback (most recent call last):
  File "descriptor_example.py", line 31, in <module>
    audi.calc_speed(1.5)
  File "descriptor_example.py", line 24, in calc_speed
    return self.speed.value * accel
AttributeError: 'int' object has no attribute 'value'

What I expect

Getting speed desc
vehicle speed: 100
Setting speed desc to 120
180

Solution

  • speed should be a class attribute; __get__ is passed the instance whose speed you want when you access it via an instance rather than a class. I've adjusted the definition of SpeedDesc slightly to emphasize that you can still initialized it from Vehicle.__init__.

    You never need to access the value attribute of the descriptor explicitly: that's an implementation detail for __get__ and __set__. In fact, because there is exactly one instance of SpeedDesc shared by all instance of Vehicle, you don't want to store the speed in self.value. You should store it in a dict that associates a speed with a particular instance, which is most easily done by attaching it to the object received as the obj argument.

    class SpeedDesc(object):
    
        def __init__(self, name, val=None):
            self.var_name = name
            self.default = val
            self.attr_name = "_" + name  # e.g.
    
        def __get__(self, obj, objtype):
            if obj is None:
                return self
            print('Getting', self.var_name)
            return getattr(obj, self.attr_name, self.default)
    
        def __set__(self, obj, value):
            msg = 'Setting {name} to {value}'
            print(msg.format(name=self.var_name, value=value))
            setattr(obj, self.attr_name, value)
    
    
    class Vehicle(object):
        speed = SpeedDesc('speed desc')
    
        def __init__(self, vType):
            self.vehicle_type = vType
            self.speed = 100
    
        def calc_speed(self, accel):
            return self.speed * accel
    
    
    if __name__ == '__main__':
    
        audi = Vehicle('sedan')
        # Produces a call to Vehicle.speed.__get__(audi, Vehicle)
        print('vehicle speed:', audi.speed)
        # Produces a call to Vehicle.speed.__set__(audi, 120)
        audi.speed = 120
        print(audi.calc_speed(1.5))
    

    Note that you don't necessarily need to pass a name explicitly; the __set_name__ method can be used to get the name of the attribute the descriptor is assigned to.

    class SpeedDesc(object):
    
        def __init__(self, val=None):
            self.default = val
    
        def __get__(self, obj, objtype):
            if obj is None:
                return self
            print('Getting', self.var_name)
            return getattr(obj, self.attr_name, self.default)
    
        def __set__(self, obj, value):
            msg = 'Setting {name} to {value}'
            print(msg.format(name=self.var_name, value=value))
            setattr(obj, self.attr_name, value)
    
        def __set_name__(self, owner, name):
            self.var_name = name
            self.attr_name = "_" + name  # e.g.
            print(f"Created {self.var_name} for {owner}, backed by {self.attr_name}")
    
    class Vehicle(object):
        speed = SpeedDesc()
    
        def __init__(self, vType):
            self.vehicle_type = vType
            self.speed = 100
    
        def calc_speed(self, accel):
            return self.speed * accel
    

    After the type is defined, Vehicle.speed.__set_name__(Vehicle, "speed") is called on your behalf.