Python - where does the self argument come from

79 Views Asked by At

I understand that "self" is implicitly passed as an argument to methods when they are called. I am learning about descriptors and understand how functions "become" methods, I understand (at least conceptually) that attributes are 'bound' to objects, i have read multiple articles online and Raymond Hettinger's Descriptor how to.

In the example below, though, I just don't understand where the reference to the instance of the Function f (i.e. the parameter "self" on __get__, for which an argument will be implicitly passed) comes from.

I contrast this with the self parameter on f itself, which I understand will get passed as an argument to parameter obj on __get__ when __get__ is called.

How does python know, when __get__ is called, what underlying function needs to be passed to MethodType? I know this is the argument which is passed to self, but how/when does this happen? I thought I understood how self worked when I learned about __get__, and how the __get__ on the function Type can either return a Function or a MethodType, but I'm getting super frustrated.

I hope this makes sense. Thank you

class Function:
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return MethodType(self, obj)

class D:
    def f(self, x):
         return x
1

There are 1 best solutions below

0
jsbueno On

MethodType is not important. ;-)

The keys here are the "descriptor" protocol, the __get__ and the __call__ special methods.

Python does implement a thing called "MethodType" that works to hold together the "raw" function that will be called as a method, the self argument which the function will receive, a proper representation (by implementing __repr__).

It is the __call__ method that is executed when a method is invoked - at this occasion, it just prepends the instance it has been bound to (when a call to __get__ created it) to the arguments it received and pass then down to the function. A user-created class can fulfill the exact same role as the existing MethodType without ever creating one (although the Python runtime likely contain some optimizations that will make using a native function object's get and the MethodType instance it creates more efficient, by creating some shortcuts.

So, to recap: the usage of "." for attribute reference for retrieving a function will call it's __get__ method (the code for that is contained in object.__getattribute__ - a class overriding __getattribute__ can customize this behavior.

If __get__ was called on a class (MyClass.method), the second argument to it is None, and in the case of functions, it just returns the function itself. (In old Python2, another kind of object, an "unbound method" existed. In Python 3, just the plain function, as defined in the class body, and requiring an explicit self first argument is returned). If __get__ is called on an instance, the second argument is the instance itself - the instace of MyClass, not the instance of Function or other descriptor.

The code bellow builds upon your example, but I add an extra class with the role of InstanceMethod class itself: you can then add "print" statements at will to better understand its workings, if you can't just by looking at the code and comments



class MyMethod:
    def __init__(self, instance, function):
         self.function = function
         self.instance = instance
    def __call__(self, *args, **kw):
          # this is called by Python when a method in an instance
          # is called, and is the part of the code responsible
          # for injecting the `self` argument that the method receives.

          # note that "self" in here means _this_ instance of MyMethod
          # and the actual instance which is to be passed as  "self"
          # to the method was annotated here by the `__get__` method
          # in the function descriptor:
          return self.function(self.instance, *args, **kw)
 
    def __repr__(self):
         return f"bound method {self.function.__name__} of {self.instance}"
          

class Function:
    def __init__(self, function):
        self.function = function
    def __get__(self, instance, cls):
        # here "instance" refers to the instance of "MyClass"
        # and "self" refers to the instance of "Function" itself
        if instance is None:
            return self
        return MyMethod(instance, self.function)



def artificial_method(self):
    return self.value

class MyClass:
    def __init__(self, value):
        self.value = value
    test = Function(artificial_method)

m  = MyClass(value=42)
m.test()  # this expression first calls Function.__get__ 
          # with the parameters "(MyClass.test, m, Myclass)"
          # when Python executes the "." operator 
          # for retrieving the `test` attribute.
          # then, when executing the call itself, indicated by the pair of 
          # parentheses, the `__call__`  method in whatever was returned 
          # by `Function.__get__` is executed.

Upon running the code above, as is in the interactive interpreter, m.test() returns 42