pythondescriptor

Internals of the __get__ method in Python


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


Solution

  • 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']