Python explicit self and turning function into method
Tags: python
Published: 2020-07-21 00:37:12

Python `self` is one of the source of confusion and questions when someone start learning Python, especially if they come from other languages such as Java, PHP, Ruby etc.

In all languages mentioned above, when you define a class, a reference to the class instance always created automatically. Take for example the following PHP code:-

```
<?php
class Person {
    public $name = "Kamal";

    public function info() {
        echo $this->name;
    }
}
```
We can see above `$this` can be used to refer to the class instance. The same code in Python is like this:-

```
class Person:
    name = "Kamal"

    def info(self):
        print(self.name)
```
So the code almost identical except for the difference in PHP the reference to class instance is called `this` while in Python it is `self`. And even `self` is arbitrary and simply pure convention. We can name it as `this` if we want.

The main difference between PHP and Python code above is that we have to specify the `self` parameter as a first parameter to the method definition. Omitting it will give us error when trying to call the instance method:-

```
>>> class Person:
...     name = "Kamal"
...     def info():
...         print(self.name)
...
>>> p = Person()
>>> p.info()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: info() takes 0 positional arguments but 1 was given
```

One common question is why we have to explicitly define the `self` parameter? Why can't it be made implicit like in other language? One way to answer this is because the explicit self parameter allow us to specify different instance when calling the method. Take the following code:-

```
p = Person()
p.info()
```
Above code actually just a syntatic sugar over calling a function in certain class and specifying the class instance as the first argument. The above code can also be written this way:-

```
p = Person()
Person.info(p)
```
The following question  could what the practical use case for this? I can't think anything specific on top of my head right now. Probably in situation when you want to call method on `p` dynamically.

However, when looking through the code of pq recently, I noticed something interesting with how they define the [task()](https://github.com/malthe/pq/blob/master/pq/tasks.py#L11) function. Some snippet of the function:-

```
def task(
    queue,
    schedule_at=None,
    expected_at=None,
    max_retries=0,
    retry_in='30s',
):
    def decorator(f):
        f._path = "%s.%s" % (f.__module__, f.__qualname__)
        f._max_retries = max_retries
        f._retry_in = retry_in

        queue.handler_registry[f._path] = f

        @wraps(f)
        def wrapper(*args, **kwargs):
```

Just look at the function parameters and ignore the rest of the code. And this is how the docs show how to use it:-

```
from pq.tasks import PQ

pq = PQ(...)

queue = pq['default']

@queue.task(schedule_at='1h')
def eat(kind):
    print 'umm, %s apples taste good.' % kind

eat('Cox')

queue.work()
```

So `task()` is a method of `PG` class? But we can see above that task() is defined as function? Turn out in [line 94](https://github.com/malthe/pq/blob/master/pq/tasks.py#L94) the task function was added as attribute to class `PQ`. That essentially make it a method of that class. How this can work? Let see how the task() function will be used as function:-

```
pq = PQ(....)
queue = pq["default"]

@pq.task(queue, schedule_at='1h')
def eat(kind):
    print 'umm, %s apples taste good.' % kind
```

Notice that when calling `task()` as function, we have to specify `queue` as its first argument, since that what it expecting as per the function definition. And when calling it as method, Python will automatically pass the PQ instance as the first argument, also satisfying the function requirement.

This pattern allow us to offer our library API both as function and method.

[task]: https://github.com/malthe/pq/blob/master/pq/tasks.py#L11
[94]: https://github.com/malthe/pq/blob/master/pq/tasks.py#L94