reddish
reddish

Reputation: 1400

Find class in which a method is defined

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?

Upvotes: 5

Views: 2202

Answers (6)

abarnert
abarnert

Reputation: 365617

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?

Upvotes: 1

abarnert
abarnert

Reputation: 365617

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…

Upvotes: 5

abarnert
abarnert

Reputation: 365617

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…

Upvotes: 3

shx2
shx2

Reputation: 64298

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])

Upvotes: 1

RickyA
RickyA

Reputation: 16029

[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.

Upvotes: 0

Chrispresso
Chrispresso

Reputation: 4061

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

Upvotes: 0

Related Questions