In my 2 previous posts [1] and [2] we've seen how the class body of a Python class statement is just executable code that is put in a code object around which a synthetic function is created. This code is executed during the class creation (receiving a namespace object, a dictionary, created by the __prepare__ method of the metaclass). This is pretty powerful, as you can put complex initialization code there, not just a normal assignment). We can apply a decorator conditionally, create multiple function alias, define functions conditionally... Let's see some examples.
def log_deco(fn):
def log_wrapper(*args, **kwargs):
print(f"invoking {fn.__name__}")
return fn(*args, **kwargs)
return log_wrapper
def do_nothing_deco(fn):
return fn
log_mode = True
profile_mode = Trumult
class Person:
def __init__(self, name: str):
self.name = name
# conditional decorator
@(log_deco if log_mode else do_nothing_deco)
def say_hi(self, to_x: str):
print(f"hi {to_x} I'm {self.name}")
# declare a function conditionally
if profile_mode:
def get_memory_consumption(self):
print("my memory consumtpion is ...")
def do_something(self):
print(f"{self.name} is doing_something")
# function aliases
work = do_something
cook = do_something
sleep = do_something
p1 = Person("Francois")
p1.say_hi("Iyan")
p1.get_memory_consumption()
p1.sleep()
# invoking say_hi
# hi Iyan I'm Francois
# my memory consumtpion is ...
# Francois is doing_something
That's pretty nice, right? As the class body ends up being executed as a function you can put any code in it, and the compiler will compile any declarations that you put in it as attributes in the namespace object (that then is passed to the __new__ and __init__ of the metaclass to create the class). But there's one limitation. What if I want to create several alias for a same function using a loop? in principle we can't.
class MyGeoManager:
_not_implemented = lambda self, item: print(f"Method is not implemented yet")
for name in ["get_city", "get_country"]:
# obviously this does not do what we would like
name = _not_implemented
Obviously the above is not doing what we want. It's just creating an attribute named "name" and assigning it in a loop. How could we add an attribute get_city and an attribute get_countr?
Well, we can leverage something we've seen in previous posts, our friend frame.f_locals. I mentioned it in my last post and talked about it in depthhere. f_locals gives us a write-through proxy to the locals of a function. When using an optimized scope, adding variables to that proxy has no particular useful effect, as the function has been compiled to acces by index to the variables that were found at compile time, but in the kind of scope used in a class body, that is a real dictionary, not a fast-array, adding new variables via f_locals adds them to the namespace dictionary that then is passed to __new__ and __init__. So we can do this:
class MyGeoManager:
local_namespace = inspect.currentframe().f_locals
not_implemented = lambda self, item: print(f"Method is not implemented yet")
for name in ["get_city", "get_country"]:
local_namespace[name] = not_implemented
print(f"get_country: {MyGeoManager.get_country}")
print(f"get_city: {MyGeoManager.get_city}")
It works like a charm!
There's another way to do this, derived from how class scope differs from optimized scopes. We are used to the builtin functions exec, compile and eval working with a snapshot of the locals namespace (because we are used to work in optimized scopes), but in a class scope, these functions receive the real namespace object. So we can leverage exec() like this:
class MyGeoManager:
not_implemented = lambda self, item: print("Method is not implemented yet")
for name in ["get_city", "get_country"]:
exec(f"{name} = not_implemented")