I was playing around with descriptors and ended up hitting a wall. I thought I could make direct calls to it like I can do with any other methods, but apparently, it doesn't seems consistent or I'm missing something.
Say I have a Coordinate class that serves as a descriptor:
class Coordinate:
def __set_name__(self, owner, name):
self._name = name
def __get__(self, instance, owner):
return instance.__dict__[self._name]
def __set__(self, instance, value):
try:
instance.__dict__[self._name] = float(value)
except ValueError:
raise ValueError(f'"{self.__set_name__}" must be a number') from None
And a Point class that will have 2 properties that are Coordinates:
class Point:
x = Coordinate()
y = Coordinate()
def __init__(self, x, y):
self.x = x
self.y = y
And let's say I create a new Point like this on my main code:
point1 = Point(12,5)
I know that if I try to do this:
print(point1.x)
I will get this in the console: 12.0
I know that if I try this, it will fail:
print(point1.x.__get__(Point))
My get method will be called with the parameters as follows:
All of that because of the order of the arguments. Since I have:
x.__get__()
x will be my first argument.
Then since I have point1.x.__get()
x will be my first argument and point1 will be my second argument.
And having the Point class passed inside the parenthesis complete what I wanted, but it still won't work because even though the get method returns 12.0
, Python will now try to call the get method in the float (12.0) returned by my manual get call, and that will throw an exception.
'float' object has no attribute '__get__'
All of that make sense to me. What doesn't make sense is that when I try to do something like this:
print(Point.x.__get__(Point))
Instead of populating my get method arguments like this:
It's populating the instance argument as None.
Why does it happen? Why doesn't it populate with a reference to Point, if all the other arguments are being passed correctly? (Both the self and owner arguments have the expected values).
When you do:
Point.x.__get__(Point)
It errors before it event gets to the .__get__(Point)
part, because Point.x
invokes your descriptor, with instance=None
, exactly as you should expect, because the descriptor was invoked on the class, not on any instance.
You are trying to do a weird mix of invoking the descriptor protocol without invoking it. This might be possibe if you did something like if instance is None: return self
, which is similar to what the built-in property
descriptor does, but fundamentally, I would urge you to reconsider whatever it is you are trying to do here. If you just want the descriptor object, the best way to get to that is:
vars(Point)['x'] # equivalent to Point.__dict__['x']