This isn't really a problem with descriptors! You've merely set the initial self.value attribute on the descriptor to None:
def __init__(self, initial_value=None): # ... self.value = initial_value # this is None by default!
You then try to convert that self.value attribute value, set to None, to an integer:
# self.value starts out as None; int(None) fails. if self.pct_min <= int(self.value) <= self.pct_max:
You probably want to apply this to the initial_value argument to __set__!
Your descriptor implementation does have several more problems:
- The
__set__ setter method should not return anything. It should set the new value for the bound object, and that's it. Python ignores anything returned by __set__. You are storing the data on the descriptor instance itself. Your class has only a single copy of the descriptor object, so when you use the descriptor between different instances of Foo() you see the exact same data. You want to store the data on the object the descriptor is bound to, so in the __set__ method, on obj, or at the very least, in a place that lets you associate the value with obj. obj is the specific instance of Foo() the descriptor is bound to.
The __get__ getter method doesn't take an 'initial value', it takes the object you are being bound to, and the type (class) of that object. When accessed on a class, obj is None and only the type is set.
Don't mix print() and creating an exception instance. ValueError(print(...)) doesn't make much sense, print() writes to the console then returns None. Just use ValueError(...).
I'm making the following assumptions:
- You want to raise an exception when setting an invalid value.
- You don't want to do any validation when getting the value.
- The initial value set on the descriptor should be used when no value is present on the bound object.
- The value itself can be stored on the instance directly.
Then the following code would work:
class Percentage: def __init__(self, initial_value=None): self.pct_min = 0 self.pct_max = 100 self.initial_value = initial_value def __get__(self, obj, owner): if obj is None: # accessed on the class, there is no value. Return the # descriptor itself instead. return self # get the value from the bound object, or, if it is missing, # the initial_value attribute return getattr(obj, '_percentage_value', self.initial_value) def __set__(self, obj, new_value): # validation. Make sure it is an integer and in range new_value = int(new_value) if new_value < self.pct_min: raise ValueError("Value to low") if new_value > self.pct_max: raise ValueError("Value to high") # validation succeeded, set it on the instance obj._percentage_value = new_value
Demo:
>>> class Foo(object): ... participation = Percentage() ... >>> f1 = Foo() >>> f1.participation is None True >>> f1.participation = 110 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 24, in __set__ ValueError: Value to high >>> f1.participation = -10 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 22, in __set__ ValueError: Value to low >>> f1.participation = 75 >>> f1.participation 75 >>> vars(f1) {'_percentage_value': 75} >>> Foo.participation # this calls Participation().__get__(None, Foo) <__main__.Percentage object at 0x105e3a240> >>> Foo.participation.__get__(f1, Foo) # essentially what f1.participation does 75
Note that the above uses a specific attribute name on the instance to set the value. If you have multiple instances of the descriptor on the same class, that will be an issue!
If you want to avoid that, your options are:
- use some kind of mapping storage that uses some other unique identifier in the key to track what descriptor stored what value on the bound object.
- store data on the descriptor instance, keyed by a unique identifier for the instance.
- store the name of the attribute on the class where this descriptor is stored, on the descriptor instance itself and base your instance attribute name on that. You can do this by implementing the
descriptor.__set_name__() hook method, which is called by Python when a class is created that uses your descriptor. This hook requires Python 3.6 or newer. For the Foo class example, that'll by called with Foo and 'participation'.
Note that you should not use the attribute name (participation in your Foo example), at least not directly. Using obj.participation will trigger the descriptor object itself, so Percentage.__get__() or Percencage.__set__(). For a data descriptors (descriptors that implementing either a __set__ or __delete__ method), you could use vars(obj) or obj.__dict__ directly to store attributes, however.
self.valuewithNonein__init__Fooinstances share aFoo.participationattribute instead of each having their own instance attribute.__get__and__set__need to use theirobjargument to keep values forf1andf2separate.