1

First time trying to use descriptors.

The goal: any integer from 0 to 100 (inclusive) will be a legal value for this descriptor. Non-integers and numbers higher than 100 or lower than 0 should result in an exception being thrown, indicating that the type and/or value is wrong.

My code:

class Percentage(): def __init__(self, initial_value=None): self.pct_min = 0 self.pct_max = 100 self.value = initial_value def __get__(self, obj, initial_value): return self.value def __set__(self, obj, initial_value): if self.pct_min <= int(self.value) <= self.pct_max: return self.value elif self.value < self.pct_min: raise ValueError(print("Value to low")) else: raise ValueError(print("Value to high")) class Foo(object): participation = Percentage() f1 = Foo() f1.participation = 30 f2 = Foo() f2.participation = 70 print(f1.participation) # should print 30 print(f2.participation) # should print 70 

For this line: if self.pct_min <= int(self.value) <= self.pct_max:

I get this error:

TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType' 

I am running it through PythonTutor and it appears I am not passing in the integer, but I do not understand where I am failing.

I am also using this as a guide: https://www.blog.pythonlibrary.org/2016/06/10/python-201-what-are-descriptors/

6
  • 2
    You initialize your self.value with None in __init__ Commented Aug 8, 2018 at 18:05
  • 1
    Note also that both of your Foo instances share a Foo.participation attribute instead of each having their own instance attribute. Commented Aug 8, 2018 at 18:07
  • 1
    __get__ and __set__ need to use their obj argument to keep values for f1 and f2 separate. Commented Aug 8, 2018 at 18:12
  • I got it cleaned up to where it prints 70 for each instance, so I am 'violating' what @Patrick Haugh is referring to -- only I am not sure what that is. Commented Aug 8, 2018 at 18:48
  • @Patrick Haugh, Bingo! That was it. Thanks. Commented Aug 8, 2018 at 21:04

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

@Martjin Peters, I am unable to replicate your results. 1) In PythonTutor I never see the set method hit. If I pipe in 120 as a value, it returns 120 instead of the ValueError.
Ahh... I removed the change I made for Patrick Haugh above and now I can replicate your results.
Ah, yes, Patrick appears to have misunderstood the why and how of descriptors and binding.
1

That's not a problem with the descriptor itself. The problem is that, in the __set__, instead of checking the value that you get, you check the existing self.value. Since this is initialized to None, you will always fail on the first assignment.

There's also an (unrelated) problem with the descriptor itself. The getter and setters are supposed to read/write from the obj.__dict__, not from the descriptor object itself. Otherwise, all the instances will share a common value

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.