-1

In python, a class instance can be used almost like a function, if it has a __call__ method. I want to have a class B that has a method A, where A is the instance of a class with a __call__ method. For comparison, I also define two other methods foo and bar in the "traditional" way (i.e. using def). Here is the code:

class A(object):
    def __call__(*args):
        print args

def foo(*args):
    print args

class B(object):
    A = A()
    foo = foo
    def bar(*args):
        print args

When I call any of the methods of B, they are passed a class instance as implicit first argument (which is conventionally called self). Yet, I was surprised to find that b.A() gets passed an A instance, where I would have expected a B instance.

In [13]: b = B()

In [14]: b.A()
(<__main__.A object at 0xb6251e0c>,)

In [15]: b.foo()
(<__main__.B object at 0xb6251c0c>,)

In [16]: b.bar()
(<__main__.B object at 0xb6251c0c>,)

Is there a way (maybe a functools trick or similar) to bind A() in such a way that b.A() is passed a reference to the b object?

Please note that the example presented above is simplified for the purpose of illustrating my question. I'm not asking for advice on my actual implementation use case, which is different.

Edit: I get the same output, if I define my class B like this:

class B(object):

    def __init__(self):
        self.A = A()
    foo = foo
    def bar(*args):
        print args
hamogu
  • 638
  • 6
  • 13
  • 1
    You've simplified it so much that we can't tell you if there's a better way of doing what you want done. – Ignacio Vazquez-Abrams Mar 22 '15 at 16:26
  • *"I was surprised to find"* - why? What else would it be passed?! You're calling *the attribute*, not the instance. – jonrsharpe Mar 22 '15 at 16:26
  • @IgnacioVazquez-Abrams: There are certainly many different ways to do what I have done, and some of them might be "better" in terms of readability, others might be better in speed,... I appreciate your willingness to help, but the solution I use in my real code is good enough for you and I want to keep the question focussed. – hamogu Mar 22 '15 at 16:43
  • @jonrsharpe: That's what I thought at first. When I make an instance of `A` in the `__init__` of `B` I get the same answer (see edit of my question). Should `b.A` not be an instance of `A` in this case? Obviously, I don't fully understand what is going on here, so please correct me if I'm wrong. – hamogu Mar 22 '15 at 16:48
  • Again, **why would you expect otherwise?!** – jonrsharpe Mar 22 '15 at 16:50
  • @jonrsharpe: Maybe my last comment did not fully answer your point. I expected that `b.foo` and `b.A` would look the same for the instance `b` because they are both callable objects (my understanding is that a function is an object, too). Thus, I expected that they both would get passed an reference to the instance `b`. – hamogu Mar 22 '15 at 16:51

1 Answers1

1

The problem with your code is:

class B(object):
    A = A()

class B has a member named A that is an instance of A. When you do B.A(), it calls the method __call__ of that A instance (that is confusingly named A); and since it is an A all the time, and A's method, of course the actual object in args is an A.


What you're after is a descriptor; that is, A should have the magic method __get__; :

class A(object):
    def __get__(self, cls, instance):
        print(cls, instance)
        return self

class B(object):
    a = A()

b = B()
c = b.a

Now when b.a is executed, __get__ method gets B and b as the cls and instance arguments, and whatever it returns is the value from the attribute lookup (the value that is stored in c) - it could return another instance, or even a function, or throw an AttributeError - up to you. To have another function that knows the B and b; you can do:

class A(object):
    def __get__(self, cls, instance):
        if instance is not None:
            def wrapper():
                print("I was called with", cls, "and", instance)
            return wrapper
        return self

class B(object):
    a = A()

B.a()

The code outputs:

I was called with <__main__.B object at 0x7f5d52a7b8> and <class '__main__.B'>

Task accomplished.

Community
  • 1
  • 1
  • That's disappointing. It seems to me the behavior ought to be consistent between callable objects and functions. The instance of B should be passed as the second argument, making the A instance transparent to the caller. Without this, mocking attributes that can be either methods or data members is impossible without using callable(), which is prone to false positives. – Brent Sep 03 '20 at 01:53