Reputation: 22324
With a metaclass and the __prepare__
method, it is possible to intercept attributes before they are added to a class' namespace. Is there a way, as hacky as it is, to intercept naked expressions from the class' scope.
class Class(metaclass=Metaclass):
# Would it be possible to intercept those:
1
2
3
The closest syntax I have found that allows something similar is to reserve the _
name for such expressions.
class ClassNamespace(dict):
def __init__(self):
self.expressions = []
super().__init__()
def __setitem__(self, key, value):
if key == '_':
self.expressions.append(value)
else:
super().__setitem__(key, value)
class Metaclass(type):
def __prepare__(metacls, name):
return ClassNamespace()
def __new__(cls, name, bases, namespace):
# Do something with the expressions
print(namespace.expressions)
return super().__new__(cls, name, bases, namespace)
class Class(metaclass=Metaclass):
_ = 1
_ = 2
_ = 3
# prints: [1, 2, 3]
It is clean, but is there a way to recover 1, 2 and 3 without using assignments?
Upvotes: 2
Views: 77
Reputation: 123501
You could sort of do it with a class decorator. For example:
class Metaclass(type):
@staticmethod
def handle_expressions(args):
print(args)
def expressions(*args):
""" Class decorator that passes expressions to Metaclass. """
def decorator(cls):
Metaclass.handle_expressions(args) # Pass to metaclass method.
return cls
return decorator
# Sample usage.
@expressions(1, 1+1, 2*2-1)
class Class(metaclass=Metaclass):
pass
Would print:
(1, 2, 3)
Note if you want the called metaclass method to be able to do something to or otherwise be able to reference the class being constructed, you could make handle_expressions()
a @classmethod
instead of a staticmethod
.
Upvotes: 0
Reputation: 451
It's not "clean", but it is without assignment:
class ClassNamespace(dict):
def __init__(self):
super().__init__()
self.expressions = []
self['record'] = self.expressions.append #make a special function to used for "clean" expressions
class Metaclass(type):
def __prepare__(metacls, name):
return ClassNamespace()
def __new__(cls, name, bases, namespace):
print(namespace.expressions)
return super().__new__(cls, name, bases, namespace)
class Class(metaclass=Metaclass):
record(1) #identifiers are first looked for in the class namespace, where we set "record" to append to the expressions list
record(2)
record(3)
You can also set the special function to be _: _(1)
might look prettier than record(1)
Upvotes: 1
Reputation: 281604
No metaclass mechanisms support what you're trying to do. 1
doesn't generate a name lookup, so __prepare__
doesn't help. Heck, 1
as a statement on its own doesn't generate any bytecode at all, because it gets optimized out.
Even if you were willing to go so far as having the metaclass locate the function object for the class body and replace its bytecode with instrumented bytecode to try to capture these expressions, it would still find no trace of the original expressions in the bytecode, and there would be nothing to instrument. Also, the metaclass has no way to get involved in the original compilation pass for the class body.
The most plausible (still utterly crazy) option seems to be to have the metaclass locate the original source code for the class, parse it, modify the AST, recompile, and replace the original code object for the class body. This would never work in interactive mode (since the source code isn't kept in interactive mode), among the many other reasons why this is a bad idea.
Upvotes: 3