Given a method, how do I return the class it belongs to in Python 3.3 onward?

Question:

Given x = C.f after:

class C:
    def f(self):
        pass

What do I call on x that will return C?

The best I could do is execing a parsed portion of x.__qualname__, which is ugly:

exec('d = ' + ".".join(x.__qualname__.split('.')[:-1]))

For a use case, imagine that I want a decorator that adds a super call to any method it’s applied to. How can that decorator, which is only given the function object, get the class to super (the ??? below)?

def ensure_finished(iterator):
    try:
        next(iterator)
    except StopIteration:
        return
    else:
        raise RuntimeError

def derived_generator(method):
    def new_method(self, *args, **kwargs):
        x = method(self, *args, **kwargs)
        y = getattr(super(???, self), method.__name__)
            (*args, **kwargs)

        for a, b in zip(x, y):
            assert a is None and b is None
            yield

        ensure_finished(x)
        ensure_finished(y)

    return new_method
Asked By: Neil G

||

Answers:

If your aim is to get rid of the exec statement, but are willing to use the __qualname__ attribute, even though you are still required to manually parse it, then at least for simple cases the following seems to work:

x.__globals__[x.__qualname__.rsplit('.', 1)[0]]

or:

getattr(inspect.getmodule(x), x.__qualname__.rsplit('.', 1)[0])

I’m not a Python expert, but I think the second solution is better, considering the following documentation excerpts:

  • from What’s new in Python 3.3:

    Functions and class objects have a new __qualname__ attribute representing the “path” from the module top-level to their definition. For global functions and classes, this is the same as __name__. For other functions and classes, it provides better information about where they were actually defined, and how they might be accessible from the global scope.

  • from __qualname__‘s description in PEP 3155:

    For nested classed, methods, and nested functions, the __qualname__ attribute contains a dotted path leading to the object from the module top-level.

EDIT:

  1. As noted in the comments by @eryksun, parsing __qualname__ like this goes beyond its intended usage and is extremely fragile considering how __qualname__ reflects closures. A more robust approach needs to exclude closure namespaces of the form name.<locals>. For example:

    >>> class C:
    ...     f = (lambda x: lambda s: x)(1)
    ... 
    >>> x = C.f
    >>> x
    <function C.<lambda>.<locals>.<lambda> at 0x7f13b58df730>
    >>> x.__qualname__
    'C.<lambda>.<locals>.<lambda>'
    >>> getattr(inspect.getmodule(x), x.__qualname__.rsplit('.', 1)[0])
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'module' object has no attribute 'C.<lambda>.<locals>'
    

    This specific case can be handled in the following manner:

    >>> getattr(inspect.getmodule(x),
    ...         x.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
    <class '__main__.C'>
    

    Nonetheless, it’s unclear what other corner cases exist now or may come up in future releases.

  2. As noted in the comment by @MichaelPetch, this answer is relevant only for Python 3.3 onward, as only then the __qualname__ attribute was introduced into the language.

  3. For a complete solution that handles bound methods as well, please refer to this answer.

Answered By: Yoel

I’ll contribute one more option that relies on the gc module to follow references backwards.

It relies on implementation details that certainly aren’t guaranteed and certainly won’t work on all Python implementations. Nevertheless, some applications may find this option preferable to working with __qualname__.

You actually need two hops backwards, because the class hides a dict inside it, which holds the member function:

def class_holding(fn):
    '''
    >>> class Foo:
    ...     def bar(self):
    ...         return 1
    >>> class_holding(Foo.bar)
    <class Foo>
    '''
    for possible_dict in gc.get_referrers(fn):
        if not isinstance(possible_dict, dict):
            continue
        for possible_class in gc.get_referrers(possible_dict):
            if getattr(possible_class, fn.__name__, None) is fn:
                return possible_class
    return None
Answered By: pschanely