Usage of descriptors

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP



Usage of descriptors



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/





You initialize your self.value with None in __init__
– juanpa.arrivillaga
Aug 8 at 18:05


self.value


None


__init__





Note also that both of your Foo instances share a Foo.participation attribute instead of each having their own instance attribute.
– Patrick Haugh
Aug 8 at 18:07


Foo


Foo.participation





__get__ and __set__ need to use their obj argument to keep values for f1 and f2 separate.
– chepner
Aug 8 at 18:12


__get__


__set__


obj


f1


f2





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.
– MarkS
Aug 8 at 18:48





@Patrick Haugh, Bingo! That was it. Thanks.
– MarkS
Aug 8 at 21:04




2 Answers
2



This isn't really a problem with descriptors! You've merely set the initial self.value attribute on the descriptor to None:


self.value


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


None


# 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__!


initial_value


__set__



Your descriptor implementation does have several more problems:


__set__


__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.


Foo()


__set__


obj


obj


obj


Foo()



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.


__get__


obj


None



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(...).


print()


ValueError(print(...))


print()


None


ValueError(...)



I'm making the following assumptions:



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:


descriptor.__set_name__()


Foo


Foo


'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.


participation


Foo


obj.participation


Percentage.__get__()


Percencage.__set__()


__set__


__delete__


vars(obj)


obj.__dict__





@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.
– MarkS
Aug 8 at 21:52






Ahh... I removed the change I made for Patrick Haugh above and now I can replicate your results.
– MarkS
Aug 8 at 22:06





Ah, yes, Patrick appears to have misunderstood the why and how of descriptors and binding.
– Martijn Pieters
Aug 8 at 22:25



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.


__set__


self.value


None



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


obj.__dict__






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Firebase Auth - with Email and Password - Check user already registered

Dynamically update html content plain JS

How to determine optimal route across keyboard