-1

The get_calling_class function must pass the following tests by returning the class of the method that called the A.f method:

class A:
    def f(self): return get_calling_class()

class B(A):
    def g(self): return self.f()

class C(B):
    def h(self): return self.f()

c = C()
assert c.g() == B
assert c.h() == C
Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
  • Actually, what you want to know is what class _defined_ the method being called, which is potentially a very complex thing to determine. – martineau Aug 13 '20 at 21:49
  • @martineau Not exactly, I already the know the class that *defined* the method `A.f`: it is `A`. I want the class that *called* the method `A.f`. – Géry Ogam Aug 13 '20 at 21:51
  • Yes, but class `B` defined method `g` — which looks like the class you apparently want `c.g()` to return. – martineau Aug 13 '20 at 21:54
  • @martineau Yes, so I want the class of the method that called `A.f`. – Géry Ogam Aug 13 '20 at 21:59
  • A member of another class isn't the only thing that could call a method of any of the classes, so that's not a valid assumption to make. Why to you want this information anyway? — I'm beginning to suspect this may be an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – martineau Aug 13 '20 at 22:05
  • @martineau In my scenario class `A` needs to know which subclass invoked its `f` method, so this assumption is fine. – Géry Ogam Aug 13 '20 at 22:10
  • 2
    You've conveniently left out ***why*** you need to know which subclass invoked the method. Sounds like the whole problem may caused by poor design — in object-oriented programming, such information should rarely ever be needed. – martineau Aug 13 '20 at 23:55

1 Answers1

1

Walking the stack should give the answer.
The answer should ideally be, in the caller's stack frame.

The problem is, the stack frames only record the function
names (like so: 'f', 'g', 'h', etc.) Any information about
classes is lost. Trying to reverse-engineer the lost info,
by navigating the class hierarchy (in parallel with the
stack frame), did not get me very far, and got complicated.

So, here is a different approach:
Inject the class info into the stack frame
(e.g. with local variables),
and read that, from the called function.

import inspect

class A:
  def f(self):
    frame = inspect.currentframe()
    callerFrame = frame.f_back
    callerLocals = callerFrame.f_locals
    return callerLocals['cls']

class B(A):
  def g(self):
    cls = B
    return self.f()
    
  def f(self):
    cls = B
    return super().f()

class C(B):
  def h(self):
    cls = C
    return super(B, self).f()
  
  def f(self):
    cls = C
    return super().f()

c = C()
assert c.h() == C
assert c.g() == B
assert c.f() == B

Related:
get-fully-qualified-method-name-from-inspect-stack


Without modifying the definition of subclasses:
Added an "external" decorator, to wrap class methods.
(At least as a temporary solution.)

import inspect

class Injector:
  def __init__(self, nameStr, valueStr):
    self.nameStr = nameStr
    self.valueStr = valueStr
  
  # Should inject directly in f's local scope / stack frame.
  # As is, it just adds another stack frame on top of f.
  def injectInLocals(self, f):
    def decorate(*args, **kwargs):
      exec(f'{self.nameStr} = {self.valueStr}')
      return f(*args, **kwargs)
    return decorate

class A:
  def f(self):
    frame = inspect.currentframe()
    callerDecoratorFrame = frame.f_back.f_back  # Note:twice
    callerDecoratorLocals = callerDecoratorFrame.f_locals
    return callerDecoratorLocals['cls']

class B(A):
  def g(self): return self.f()
  def f(self): return super().f()

class C(B):
  def h(self): return super(B, self).f()
  def f(self): return super().f()

bInjector = Injector('cls', B.__name__)
B.g = bInjector.injectInLocals(B.g)
B.f = bInjector.injectInLocals(B.f)

cInjector = Injector('cls', C.__name__)
C.h = cInjector.injectInLocals(C.h)
C.f = cInjector.injectInLocals(C.f)

c = C()
assert c.h() == C
assert c.g() == B
assert c.f() == B

I found this link very interesting
(but didn't take advantage of metaclasses here):
what-are-metaclasses-in-python

Maybe someone could even replace the function definitions*,
with functions whose code is a duplicate of the original;
but with added locals/information, directly in their scope.

*
Maybe after the class definitions have completed;
maybe during class creation (using a metaclass).

  • Thanks for your effort! I think that it might be a good approach too. You can even improve it by replacing `cls = X` by `__class__`. However a constraint that I have is I can only modify the base class (`A`) and the most derived class (`C`), but not the intermediary classes (`B`). So how would you handle this? I thought of a metaclass on `C`. – Géry Ogam Aug 15 '20 at 09:35
  • This might help: https://stackoverflow.com/a/4885951/2326961 – Géry Ogam Aug 15 '20 at 10:10
  • @Maggyero Metaclasses are interesting, but I'm not very familiar with them. Added a temp workaround for now, may come back later. (From what little I read about them, you would want to add the metaclass to the root of your class hierarchy, so that all derived classes are affected by it.) –  Aug 15 '20 at 15:53
  • Thanks a lot, I like your idea. – Géry Ogam Aug 16 '20 at 21:14
  • @Maggyero It was an interesting question; a surprisingly difficult one. –  Aug 17 '20 at 00:54
  • Yes, it would have been trivial if the `__class__` reference introduced in [PEP 3135](https://www.python.org/dev/peps/pep-3135/) for accessing the current class was always available in the stack frame of a method instead of only when `__class__` or `super()` is used. I wish [PEP 3130](https://www.python.org/dev/peps/pep-3130/) was not rejected – Géry Ogam Aug 17 '20 at 11:07
  • Cf. the post [Schrödinger's variable: the \_\_class\_\_ cell magically appears if you're checking for its presence?](https://stackoverflow.com/questions/36993577/schrödingers-variable-the-class-cell-magically-appears-if-youre-checking). – Géry Ogam Aug 17 '20 at 11:15
  • Could you reformat your code with `lower_case` (instead of `camelCase`) and 4-space indentation, which is the common Python style? – Géry Ogam Aug 17 '20 at 13:48
  • @Maggyero Of course it's possible & easy to do (anyone can format it any way they want:) ) (it's just a (working) example). But why? I know the Python guidelines; they do not suit my case (split windows, 60-char lines, big monospace fonts, etc.). It is just the way I work with code. –  Aug 17 '20 at 14:02