6

I want to figure out the type of the class in which a certain method is defined (in essence, the enclosing static scope of the method), from within the method itself, and without specifying it explicitly, e.g.

class SomeClass: def do_it(self): cls = enclosing_class() # <-- I need this. print(cls) class DerivedClass(SomeClass): pass obj = DerivedClass() # I want this to print 'SomeClass'. obj.do_it() 

Is this possible?

14
  • 3
    enclosing_class = lambda: SomeClass ;-) Commented Sep 9, 2014 at 15:14
  • You'll probably just have to search the mro. Commented Sep 9, 2014 at 15:15
  • 2
    @reddish -- But you didn't state why. This smells like an XY problem. Commented Sep 9, 2014 at 15:28
  • 4
    @reddish -- Sure it isn't, but I'm really am trying to be helpful. But, what you're asking for is a very difficult thing to achieve. It probably boils down to inspecting stack frames and code objects -- possibly walking the MRO and inspecting the source code for every method defined there until you find what you're looking for. If you would let us know what utility there is in such a function, we might be able to point you to a much easier/neater/cleaner alternative to accomplish the same goal. Commented Sep 9, 2014 at 16:12
  • 1
    Which version of Python are you asking for? Because, while this is almost certainly a very bad thing to do, there are ways to do it for at least some implementations and versions, but they're not necessarily the same, and I don't want to answer for every possible version ever… Commented Sep 9, 2014 at 17:46

6 Answers 6

5

If you need this in Python 3.x, please see my other answer—the closure cell __class__ is all you need.


If you need to do this in CPython 2.6-2.7, RickyA's answer is close, but it doesn't work, because it relies on the fact that this method is not overriding any other method of the same name. Try adding a Foo.do_it method in his answer, and it will print out Foo, not SomeClass

The way to solve that is to find the method whose code object is identical to the current frame's code object:

def do_it(self): mro = inspect.getmro(self.__class__) method_code = inspect.currentframe().f_code method_name = method_code.co_name for base in reversed(mro): try: if getattr(base, method_name).func_code is method_code: print(base.__name__) break except AttributeError: pass 

(Note that the AttributeError could be raised either by base not having something named do_it, or by base having something named do_it that isn't a function, and therefore doesn't have a func_code. But we don't care which; either way, base is not the match we're looking for.)

This may work in other Python 2.6+ implementations. Python does not require frame objects to exist, and if they don't, inspect.currentframe() will return None. And I'm pretty sure it doesn't require code objects to exist either, which means func_code could be None.

Meanwhile, if you want to use this in both 2.7+ and 3.0+, change that func_code to __code__, but that will break compatibility with earlier 2.x.


If you need CPython 2.5 or earlier, you can just replace the inpsect calls with the implementation-specific CPython attributes:

def do_it(self): mro = self.__class__.mro() method_code = sys._getframe().f_code method_name = method_code.co_name for base in reversed(mro): try: if getattr(base, method_name).func_code is method_code: print(base.__name__) break except AttributeError: pass 

Note that this use of mro() will not work on classic classes; if you really want to handle those (which you really shouldn't want to…), you'll have to write your own mro function that just walks the hierarchy old-school… or just copy it from the 2.6 inspect source.

This will only work in Python 2.x implementations that bend over backward to be CPython-compatible… but that includes at least PyPy. inspect should be more portable, but then if an implementation is going to define frame and code objects with the same attributes as CPython's so it can support all of inspect, there's not much good reason not to make them attributes and provide sys._getframe in the first place…

Sign up to request clarification or add additional context in comments.

Comments

3

First, this is almost certainly a bad idea, and not the way you want to solve whatever you're trying to solve but refuse to tell us about…

That being said, there is a very easy way to do it, at least in Python 3.0+. (If you need 2.x, see my other answer.)

Notice that Python 3.x's super pretty much has to be able to do this somehow. How else could super() mean super(THISCLASS, self), where that THISCLASS is exactly what you're asking for?*

Now, there are lots of ways that super could be implemented… but PEP 3135 spells out a specification for how to implement it:

Every function will have a cell named __class__ that contains the class object that the function is defined in.

This isn't part of the Python reference docs, so some other Python 3.x implementation could do it a different way… but at least as of 3.2+, they still have to have __class__ on functions, because Creating the class object explicitly says:

This class object is the one that will be referenced by the zero-argument form of super(). __class__ is an implicit closure reference created by the compiler if any methods in a class body refer to either __class__ or super. This allows the zero argument form of super() to correctly identify the class being defined based on lexical scoping, while the class or instance that was used to make the current call is identified based on the first argument passed to the method.

(And, needless to say, this is exactly how at least CPython 3.0-3.5 and PyPy3 2.0-2.1 implement super anyway.)

In [1]: class C: ...: def f(self): ...: print(__class__) In [2]: class D(C): ...: pass In [3]: D().f() <class '__main__.C'> 

Of course this gets the actual class object, not the name of the class, which is apparently what you were after. But that's easy; you just need to decide whether you mean __class__.__name__ or __class__.__qualname__ (in this simple case they're identical) and print that.


* In fact, this was one of the arguments against it: that the only plausible way to do this without changing the language syntax was to add a new closure cell to every function, or to require some horrible frame hacks which may not even be doable in other implementations of Python. You can't just use compiler magic, because there's no way the compiler can tell that some arbitrary expression will evaluate to the super function at runtime…

7 Comments

Thanks for your detailed answer. This is indeed a clean solution to the question I posed. For my specific situation it would be more convenient if I could reach the enclosing class from within an actual 'enclosing_scope()' function, but this solution goes 95% towards what I need.
As to the background of my question: this is about adding instrumentation to a code base to be able to generate reports of method invocation counts, for the purpose of checking certain approximate runtime invariants (e.g. "the number of times that method ClassA.x() is executed is approximately equal to the number of times that method ClassB.y() is executed in the course of a run of a complicated program). But regardless of that particular use case, I think the question as posed is interesting enough to warrant an answer, which is why I prefer to stay away from the background story.
@reddish: But it sounds like there's a much better way to solve this: at the time you're instrumenting the classes, you know the classes you're instrumenting, and therefore you don't need to recover that information dynamically in the first place. (Notice that this is the exact same reason the compiler adds __class__ at compile time: so it doesn't have to be recovered dynamically when you call super.)
yes, but I will have to instrument many classes by hand, and to prevent mistakes I want to avoid typing the class names everywhere. In essence, it's the same reason why typing super() is preferable to typing super(ClassX, self).
@reddish: And that's exactly why you should write a function to do the instrumentation for you instead of doing it by hand. (And the fact that you're doing it statically rather than dynamically just means the function should probably be a decorator, not that it shouldn't exist.)
|
1

If you can use @abarnert's method, do it.

Otherwise, you can use some hardcore introspection (for python2.7):

import inspect from http://stackoverflow.com/a/22898743/2096752 import getMethodClass def enclosing_class(): frame = inspect.currentframe().f_back caller_self = frame.f_locals['self'] caller_method_name = frame.f_code.co_name return getMethodClass(caller_self.__class__, caller_method_name) class SomeClass: def do_it(self): print(enclosing_class()) class DerivedClass(SomeClass): pass DerivedClass().do_it() # prints 'SomeClass' 

Obviously, this is likely to raise an error if:

  • called from a regular function / staticmethod / classmethod
  • the calling function has a different name for self (as aptly pointed out by @abarnert, this can be solved by using frame.f_code.co_varnames[0])

2 Comments

You can solve the last problem, at least in CPython and PyPy, by using f.f_code.co_varnames[0] instead of self. Or, maybe better (but I forget which version it was added) inspect.getargs(f.f_code).args[0].
After checking the docs, getargs is not documented, it's just part of the internals used for implementing the documented functions, and it doesn't exist in (at least) CPython 2.6-2.7, so… it doesn't help here (if you've got Python 3.x, just use __class__), so I guess you have to go with co_varnames.
1

Sorry for writing yet another answer, but here's how to do what you actually want to do, rather than what you asked for:

this is about adding instrumentation to a code base to be able to generate reports of method invocation counts, for the purpose of checking certain approximate runtime invariants (e.g. "the number of times that method ClassA.x() is executed is approximately equal to the number of times that method ClassB.y() is executed in the course of a run of a complicated program).

The way to do that is to make your instrumentation function inject the information statically. After all, it has to know the class and method it's injecting code into.

I will have to instrument many classes by hand, and to prevent mistakes I want to avoid typing the class names everywhere. In essence, it's the same reason why typing super() is preferable to typing super(ClassX, self).

If your instrumentation function is "do it manually", the very first thing you want to turn it into an actual function instead of doing it manually. Since you obviously only need static injection, using a decorator, either on the class (if you want to instrument every method) or on each method (if you don't) would make this nice and readable. (Or, if you want to instrument every method of every class, you might want to define a metaclass and have your root classes use it, instead of decorating every class.)

For example, here's an easy way to instrument every method of a class:

import collections import functools import inspect _calls = {} def inject(cls): cls._calls = collections.Counter() _calls[cls.__name__] = cls._calls for name, method in cls.__dict__.items(): if inspect.isfunction(method): @functools.wraps(method) def wrapper(*args, **kwargs): cls._calls[name] += 1 return method(*args, **kwargs) setattr(cls, name, wrapper) return cls @inject class A(object): def f(self): print('A.f here') @inject class B(A): def f(self): print('B.f here') @inject class C(B): pass @inject class D(C): def f(self): print('D.f here') d = D() d.f() B.f(d) print(_calls) 

The output:

{'A': Counter(), 'C': Counter(), 'B': Counter({'f': 1}), 'D': Counter({'f': 1})} 

Exactly what you wanted, right?

5 Comments

Not quite, but it is an interesting approach. It does make me yearn for days gone when Python was actually a simple language.
@reddish: I've been using Python since 1.5. The last changes I can think of that really made the language more complex are descriptors (2.2) and generators (2.3). And since then, there have definitely been things that made it simpler (no more int/long, two kinds of classes, cmp vs. rich comparisons, stdlib modules that don't quite work with Unicode but don't give errors, …), although of course most of those happened all at once in 3.0.
What I perhaps mean to say is that it takes quite a bit of effort and background to understand how your proposed code works; which contrasts starkly to the basic idea of Python as a language to write easily readable code in (the 'executable pseudocode' idea). The rather complex scaffolding you need to reach the clean decorator syntax makes me uncomfortable. But I appreciate the end result.
@reddish: I really don't see what's so complicated here. You're trying to dynamically modify all of the methods of a class; there's a limit to how easy that could be—or even should be. More importantly, the way I do it—by monkeypatching the class—is exactly the same way I would have done it in Python 1.6. The only difference is that I can use inspect and functools methods instead of accessing and setting quasi-undocumented attributes, and I can wrap it in a decorator so I don't have to write C = inspect(C) for each class. (And unlike 2.2-2.7, I don't have to deal with bound methods.)
@reddish: Unless your argument is that "no one would have tried to do this in Python 1.6, therefore it wouldn't have mattered that it would have been a bit complicated"… in which case the answer isn't that Python has gotten more complex, but that your needs and demands have, because you've learned some powerful ideas in the mean time (like aspects) that aren't built into Python and have to be built manually.
0

You can either do what @mgilson suggested or take another approach.

class SomeClass: pass class DerivedClass(SomeClass): pass 

This makes SomeClass the base class for DerivedClass.
When you normally try to get the __class__.name__ then it will refer to derived class rather than the parent.

When you call do_it(), it's really passing DerivedClass as self, which is why you are most likely getting DerivedClass being printed.

Instead, try this:

class SomeClass: pass class DerivedClass(SomeClass): def do_it(self): for base in self.__class__.__bases__: print base.__name__ obj = DerivedClass() obj.do_it() # Prints SomeClass 

Edit:
After reading your question a few more times I think I understand what you want.

class SomeClass: def do_it(self): cls = self.__class__.__bases__[0].__name__ print cls class DerivedClass(SomeClass): pass obj = DerivedClass() obj.do_it() # prints SomeClass 

3 Comments

I understand all this. Your proposed solution does not address my question.
@reddish, I edited my answer for what I think you want. Let me know if it works as you intended
This approach doesn't do what I need. Your approach presupposes that I call 'do_it' from a class that is one inheritance level below SomeClass. If I call 'do_it' from a direct SomeClass instance, or from an instance of a class that derives from 'DerivedClass', it will break.
0

[Edited] A somewhat more generic solution:

import inspect class Foo: pass class SomeClass(Foo): def do_it(self): mro = inspect.getmro(self.__class__) method_name = inspect.currentframe().f_code.co_name for base in reversed(mro): if hasattr(base, method_name): print(base.__name__) break class DerivedClass(SomeClass): pass class DerivedClass2(DerivedClass): pass DerivedClass().do_it() >> 'SomeClass' DerivedClass2().do_it() >> 'SomeClass' SomeClass().do_it() >> 'SomeClass' 

This fails when some other class in the stack has attribute "do_it", since this is the signal name for stop walking the mro.

23 Comments

Doesn't this just print the first base class name? It won't work if SomeClass is derived from another class.
Yes, it prints the first class in the inheritance stack, but OP is not specifying on what position his class has in the stack, and in his example it is the first...
I specified that I want the 'enclosing static scope of the method'. Your approach doesn't do that.
Updated to also include baseclasses.
I'm not sure why the other linked answer and this one both reverse the MRO. You need to search it in the order given, to find the last latest definition of the function, no?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.