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.
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 -
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
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 -
@property
decorator will break thisIn short, its a recipe for disaster.
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!
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!