Here's a simple class created declaratively:
class Person:
def say_hello(self):
print("hello")
And here's a similar class, but it was defined by invoking the metaclass manually:
def say_hello(self):
print("sayolala")
say_hello.__qualname__ = 'Person.say_hello'
TalentedPerson = type('Person', (), {'say_hello': say_hello})
I'm interested to know whether they are indistinguishable. Is it possible to detect such a difference from the class object itself?
>>> def was_defined_declaratively(cls):
... # dragons
...
>>> was_defined_declaratively(Person)
True
>>> was_defined_declaratively(TalentedPerson)
False
It is not possible to detect such difference at runtime with python. You can check the files with a third-party app but not in the language since no matter how you define your classes they should be reduced to the objects which the interpreter knows how to manage.
Everything other is syntax sugar and its death with at the preprocessing step of the operations on the text.
The whole metaprogramming is a technique that lets you close to the compiler/interpreter work. Revealing some of the type traits and giving you the freedom to work on the type with code.
It is possible — somewhat.
inspect.getsource(TalentedPerson)
will fail with anOSError
, whereas it will succeed withPerson
. This only works though if you don't have a class of that name in the file where it was defined:If your file consists of both of these definitions, and
TalentedPerson
also believes it isPerson
, theninspect.getsource
will simply findPerson
's definition.Obviously this relies on the source code still being around and findable by inspect — this won't work with compiled code, e.g. in the REPL, can be tricked, and is sort of cheating. The actual code objects don't differ AFAIK.
This should not matter, at all. Even if we dig for more attributes that differ, it should be possible to inject these attributes into the dynamically created class.
Now, even without the source file around (from which, things like
inspect.getsource
can make their way, but see below), class body statements should have a corresponding "code" object that is run at some point. The dynamically created class won't have a code body (but if instead of callingtype(...)
you calltypes.new_class
you can have a custom code object for the dynamic class as well - so, as for my first statement: it should be possible to render both classes indistinguishable.As for locating the code object without relying on the source file (which, other than by
inspect.getsource
can be reached through a method's.__code__
attibute which anotatesco_filename
andco_fistlineno
(I suppose one would have to parse the file and locate theclass
statement above theco_firstlineno
then)And yes, there it is: given a module, you can use
module.__loader__.get_code('full.path.tomodule')
- this will return a code_object. This object has aco_consts
attribute which is a sequence with all constants compiled in that module - among those are the code objects for the class bodies themselves. And these, have the line number, and code objects for the nested declared methods as well.So, a naive implementation could be:
For simple cases. If you have to check if the class body is inside another function, or nested inside another class body, you have to do a recursive search in all code objects
.co_consts
attribute in the file> Samething if you find if safer to check for any attributes beyond thecls.__name__
to assert you got the right class.And again, while this will work for "well behaved" classes, it is possible to dynamically create all these attributes if needed - but that would ultimately require one to replace the code object for a module in
sys.__modules__
- it starts to get a little more cumbersome than simply providing a__qualname__
to the methods.update This version compares all strings defined inside all methods on the candidate class. This will work with the given example classess - more accuracy can be achieved by comparing other class members such as class attributes, and other method attributes such as variable names, and possibly even bytecode. (For some reason, the code object for methods in the module's code object and in the class body are different instances,though code_objects should be imutable) .
I will leave the implementation above, which only compares the class names, as it should be better for understanding what is going on.