Usage of descriptors
Clash 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/
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.
You initialize your
self.value
withNone
in__init__
– juanpa.arrivillaga
Aug 8 at 18:05