8

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
abarnert
  • 313,628
  • 35
  • 508
  • 596
Neil G
  • 28,787
  • 31
  • 143
  • 234
  • How about `x.im_class`? – dano Sep 18 '14 at 20:10
  • @dano: doesn't work in my ipython 3.4? – Neil G Sep 18 '14 at 20:10
  • possible duplicate of [Get class that defined method](http://stackoverflow.com/questions/961048/get-class-that-defined-method) – Yoel Sep 18 '14 at 20:11
  • 1
    Ah, sorry, it's python 2 only. – dano Sep 18 '14 at 20:11
  • @yoel: yes, but I've updated my question to specify python 3. I suspect it's not possible since Python 3 seems to have done away with types.MethodType for unbound methods? – Neil G Sep 18 '14 at 20:15
  • Is there are particular use-case you want this information for? In Python 3, the concept of unbound methods has been removed, so `x` is just a regular function that isn't directly tied to `C` at all. – dano Sep 18 '14 at 20:15
  • @dano: just asked that in my most recent question: http://stackoverflow.com/questions/25921537/how-do-i-call-super-in-a-method-decorator-in-python-3 – Neil G Sep 18 '14 at 20:15
  • What about the answer in this SO question: http://stackoverflow.com/questions/961048/get-class-that-defined-method . Probably uglier than what you have but would work on Python2 (but that isn't a requirement of yours) – Michael Petch Sep 18 '14 at 20:23
  • @MichaelPetch It uses the same `x.im_class` that isn't available in Python 3.x – chepner Sep 18 '14 at 20:24
  • Sorry, you are correct. Accidentally ran on Python2.7 – Michael Petch Sep 18 '14 at 20:25
  • 1
    It's worth noting you shouldn't use `im_class` or `self.__class__` as an argument to `super` (which is the OP's intention according to his other question), because if you ever subclass `C` you'll get infinite loops when `f` is called. – dano Sep 18 '14 at 20:27
  • 1
    Maybe something like this - `x.__globals__['.'.join(x.__qualname__.split('.')[:-1])]`? Still ugly, but at least without an `exec`... – Yoel Sep 18 '14 at 20:29
  • 1
    have you tried `x.__self__` for the object instance? or `x.__self__.__class__` for the class – ashwinjv Sep 18 '14 at 20:30
  • @Ashwin: `AttributeError: 'function' object has no attribute '__self__'` – Neil G Sep 18 '14 at 20:33
  • have you looked at this? http://stackoverflow.com/questions/3589311/get-defining-class-of-unbound-method-object-in-python-3 – ashwinjv Sep 18 '14 at 20:52
  • 1
    I think as long as you do `x = C.f` you may have issues. Since you are operating on the class itself (and not an instance of the class) the function x becomes detached. Only way I see would be to get the function on `f` through an instance like `x = C().f` – Michael Petch Sep 18 '14 at 20:52
  • @MichaelPetch: I am still interested to know if qualname is required to solve this problem, so this was not meant to be a Python 3.3 question, but it's fine as there is no reason for anyone to be using Python 3.0–3.2. – Neil G Sep 18 '14 at 22:25
  • It appears under 3.0 to 3.2 you may not have any alternatives. The only thing I suggest is that if you want to keep a method associated with its class (and have it seen as a method) after being assigned a variable - under 3.0-3.2 you may have to consider getting a method through an instance (and not the class). So rather than `x = C.f` you could used `x = C().f`. However if that doesn't suit your needs then I do not know of another solution at this time. – Michael Petch Sep 18 '14 at 22:44
  • @eryksun: No, `C.f` is a method: it is a function that belongs to the class `C` that takes a reference to `self`. It's true that Python 3 stopped using the `MethodType` for unbound methods, but semantically it is a method. – Neil G Sep 19 '14 at 02:32
  • @NeilG, it's semantically a method with respect to OOP design, but technically it's just a function object. Python lets you add a function (method) to a class at runtime, so the function's `__qualname__` isn't necessarily related to the class. Also, parsing `__qualname__` like this is beyond its intended usage. I found one issue with respect to closure ``; I'm not sure what other corner cases exist now or may come up in future releases. – Eryk Sun Sep 19 '14 at 03:01
  • @eryksun: True. If a function is added to a class at runtime, how can a decorated version of it call super? I guess it would be impossible. It is unfortunate that Python does not have a `__parent__` member for modules, classes, and class members so that the declaration structure can be walked. – Neil G Sep 19 '14 at 13:04
  • @eryksun: Unfortunately, I'm using the decorator on methods in the class and the class name is not defined until the class definition is complete. – Neil G Sep 19 '14 at 17:34
  • 1
    @NeilG, ok, maybe this will work. Use a decorator to set an identifying attribute on the target functions (e.g. `func._derived = True`). Then define `derived_generator` in a class decorator that creates a closure over the class, so you can call `super(cls, self)`. Loop over the class dict to apply this to all of the `_derived` functions. – Eryk Sun Sep 19 '14 at 18:01
  • @eryksun: Yes, great idea. It avoids my solution of using a metaclass, which I wanted to avoid. You should copy this comment into an answer to my other question. – Neil G Sep 19 '14 at 22:10
  • Since [How do I call super in a method decorator in Python 3](http://stackoverflow.com/questions/25921537/how-do-i-call-super-in-a-method-decorator-in-python-3?noredirect=1) is just the same question with a use case attached, and the answer to that question would be "do what's in this question", I closed it as a dup and copied the use case over. @NeilG, you may want to review the edit and undo if you think it's not appropriate. – abarnert Oct 23 '14 at 22:41

2 Answers2

5

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.

Community
  • 1
  • 1
Yoel
  • 7,519
  • 5
  • 35
  • 53
  • 1
    My concern here is that the target is Python 3. But `__qualname__` didn't get added until Python 3.4. So does the question have to be amended to be Python 3.4 and higher? This solution will not work with Python 3.2 and 3.3 variants. – Michael Petch Sep 18 '14 at 21:47
  • 1
    @MichaelPetch, I wasn't aware of that. I've edited my answer to reflect your comment. Thanks! – Yoel Sep 18 '14 at 22:01
  • I've assumed it's safe to use `__qualname__` since the OP used it in his question as a possible solution. I'll suggest a tag edit of the question to reflect your observation. – Yoel Sep 18 '14 at 22:13
  • Yeah, he used it in the question body. Also, it seems that [it was introduced in `Python 3.3`](https://docs.python.org/3.3/whatsnew/3.3.html#pep-3155-qualified-name-for-classes-and-functions). – Yoel Sep 18 '14 at 22:21
  • @MichaelPetch: Just FYI downvotes are for unclear or bad questions. It would have been nice to have a Python 3 solution, and an answer that says it's not possible in Python 3.0–3.2, but this is how you do it in Python 3.3+ is a good answer to my question. – Neil G Sep 18 '14 at 22:24
  • @MichaelPetch: Regardless of your downvote, I think you should consider upvoting Yoel's answer since you're paying such close attention to this question. – Neil G Sep 18 '14 at 22:27
  • It is a bad question because the title and tag do not match what the accepted answer was. If the accepted answer doesn't match the question - question is bad. – Michael Petch Sep 18 '14 at 22:27
  • @MichaelPetch: All he had to do was say that it wasn't possible in 3.0–3.2. – Neil G Sep 18 '14 at 22:33
  • @NeilG I don't think he knew. As he said he went off the fact that in one of your comments you were using `__qualname__` therefore as he said he assumed he was okay to use it. I had to verify it worked on 3.3 and it does so I will remove my down vote on the question and upvote the answer. – Michael Petch Sep 18 '14 at 22:41
  • https://github.com/wbolster/qualname provides a __qualname__ equivalent for older python versions. – wouter bolsterlee Apr 11 '15 at 23:03
  • @WouterBolsterlee: Thanks, I've edited my answer accordingly. – Yoel Apr 12 '15 at 13:51
1

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
pschanely
  • 81
  • 1
  • 6