30 November 2022
7 mins read

Selfless Python

A horrendous hack to make Python methods selfless.

Shashwat
Shashwat TheTrio

Introduction

Recently someone asked me why python has an explicit self unlike other languages like C++, Java or C# where the keyword this is just globally available. While the answer to that isn’t relevant to this post, it nonetheless gave me an idea - how hard would it be to remove the need for an explicit self from Python?

Today we’ll be looking at a horrendous hack that does exactly that.

Warning: Python is a highly dynamic language - more than most people realize. This means that it’s possible to do some really weird things with it. This post is one of those things. I don’t recommend you try this at home. If you do, you’re on your own.

The hack

Metaclasses. Of course it’s metaclasses. What else would it be? I won’t even attempt to go into the details of metaclasses here, but if you’re interested, you can read up on them here. I’ve also written a couple of posts on metaclasses in the past -

  1. Implementing Dataclasses from scratch
  2. Meta-Programming

Now that that’s out of the way, let’s get to the hack. The idea is simple - we’ll create a metaclass that will add a decorator to every method in a class. Let’s call this decorator add_self_to_globals. As the name implies, the function adds all the variables to the global namespace - this means that instead of doing self.x, we can just do x. Let’s see how this works.

Let’s start by defining the decorator -

def add_self_to_globals(func):
  def wrapper(self, *args, **kwargs):
    for name, value in self.__dict__.items():
      func.__globals__[name] = value

    return func(self, *args, **kwargs)

  return wrapper

We define a wrapper method - which like our original method takes in self and all other parameters. We then iterate over all the variables in self.__dict__ and add them to the global namespace for the function. We now return this wrapper, which replaces the original method.

Let’s try this out


class Person:
  @add_self_to_globals
  def say_hi(self):
    print(f"Hi, I am {name} and my age is {age}")

  def __init__(self, name, age) -> None:
    self.name = name
    self.age = age


p = Person("Shashwat", 20)
p.say_hi()  # Hi, I am Shashwat and my age is 20

I get an error for this code in my editor but it works as expected.

However, given that a class might have tens of methods, it’s not practical to add this decorator to every method. So we’ll create a metaclass that will do this for us.

For that, let’s extract all the functions from the attributes of the class. We then loop over them and add the decorator to them. We then construct a new class with the same name and the modified attributes.

class SelflessMetaClass(type):
  def __new__(cls, name, bases, attrs):
      # get all the functions
    functions = {
      name: attr for name, attr in attrs.items() if callable(attr)
    }

    def add_self_to_globals(func):
      def wrapper(self, *args, **kwargs):
        for name, value in self.__dict__.items():
          func.__globals__[name] = value

        return func(self, *args, **kwargs)

      return wrapper

    for func_name, func in functions.items():
      attrs[func_name] = add_self_to_globals(func)

  return super().__new__(cls, name, bases, attrs)

And that’s it. We can now use this metaclass to make our classes selfless.

class Selfless(metaclass=SelflessMetaClass):
    ...


class Person(Selfless):
  def say_hi(self):
    print(f"Hi, I am {name} and my age is {age}")

  def __init__(self, name, age) -> None:
    self.name = name
    self.age = age


p = Person("Shashwat", 20)
p.say_hi()  # Hi, I am Shashwat and my age is 20

Potential Problems

At the cost of repeating myself, let me again state it’s not practical to use this. However, it’s a fun exercise to see how far you can push the limits of a language. Some potential problems I’ve found with this are -

  1. This likely won’t play well with static and class functions
  2. The @property decorator will break this
  3. Accessing properties from a parent class is not really possible with this approach - unless we recursively add all the variables from the parent class to the global namespace as well - which sounds even worse than what I’ve done here

In short, its a recipe for disaster.

An alternative

A more practical approach for this this would to be do the following - instead of injecting all the variables into the global namespace of the function, just inject self. You’d still have to do self.x instead of just x but this might be a lot more practical.

def selfless_method(func):
  def wrapper(self, *args, **kwargs):
    func.__globals__["self"] = self
    return func(*args, **kwargs)
  return wrapper


class Person:
  @selfless_method
  def say_hi():
    print(f"Hi, I am {self.name} and my age is {self.age}")

  @selfless_method
  def __init__(name, age) -> None:
    self.name = name
    self.age = age


p = Person("Shashwat", 20)
p.say_hi()  # Hi, I am Shashwat and my age is 20

As you can see, the methods now don’t require self as a parameter. Your IDE should still complain but hey, at least it works!

Conclusion

Someone reading this might be thinking - why would you want to do this? Well, I don’t know. I just wanted to see if it was possible. And often the answer in python is - yes it is possible if you’re willing to dabble with metaclasses.

As always, I hope you’re leaving this entry with a greater level of understanding of python’s internals than before. See you next time!

Categories

Python