Passing a "pointer to a virtual function" as argument in Python
Asked Answered
B

2

11

Compare the following code in C++:

#include <iostream>
#include <vector>

struct A
{
  virtual void bar(void) { std::cout << "one" << std::endl; }
};

struct B : public A
{
  virtual void bar(void) { std::cout << "two" << std::endl; }
};

void test(std::vector<A*> objs, void (A::*fun)())
{
  for (auto o = objs.begin(); o != objs.end(); ++o)
  {
    A* obj = (*o);
    (obj->*fun)();
  }
}

int main()
{
  std::vector<A*> objs = {new A(), new B()};

  test(objs, &A::bar);
}

and in Python:

class A:

    def bar(self):
        print("one")


class B(A):

    def bar(self):
        print("two")


def test(objs, fun):
    for o in objs:
        fun(o)

objs = [A(), B()]
test(objs, A.bar)

The C++ code will print:

one
two

while the Python code will print

one
one

How can I pass "a pointer to a method" and resolve it to the overridden one, achieving the same behavior in Python as in C++?

To add some context and explain why I initially thought about this pattern. I have a tree consisting of nodes that can be subclassed. I would like to create a generic graph traversal function which takes a node of the graph as well as a function which might be overridden in subclasses of graph nodes. The function calculates some value for a node, given values calculated for adjacent nodes. The goal is to return a value calculated for the given node (which requires traversing the whole graph).

Blinders answered 5/8, 2016 at 1:43 Comment(4)
Have you considered structuring the Python code so that you're not trying to replicate semantics in a different language? I feel that the more Pythonic way of doing this has nothing to do with inheritance or dynamic dispatch.Fraternity
I agree, the problem is that I don't have a better pattern in mind for my problem. Please see the edit for the problem description. I would prefer a Pythonic solution.Blinders
Sounds like you want the visitor pattern.Fraternity
Would you be able to post a (simplified) example of how you would approach it in Python?Blinders
W
9

Regarding your edit, one thing you could do is use a little wrapper lambda that calls the method you want to reference. This way the method call looks like "regular python code" instead of being something complicated based on string-based access.

In your example, the only part that would need to change is the call to the test function:

test(objs, (lambda x: x.bar()))
Wickliffe answered 5/8, 2016 at 3:56 Comment(5)
This seems like a cleaner, Pythonic way of solving the problem. However, it doesn't allow for dynamic dispatch, something that is possible to emulate with the other answer (if one needs it).Blinders
I don't understand what you mean. The bar method call is going to be dynamically dispatched.Wickliffe
My terminology might be off. What I meant was that with this approach, it is not possible to handle the case where bar takes different arguments in A and in B, for instance, in B it takes an additional argument. In the string approach, test can choose to apply different arguments to bar depending on the type of obj.Blinders
Making A and B take different parameters would be weird (the point of having a common method is to share an interface...) but there is nothing that stops test from inspecting the type of the objects in the objs list. The only problem is if A and B take a different number of arguments but you could solve that by making the lambda into a vararg function: lambda x, *args : x.bar(*args)Wickliffe
Agree this is a better approach than the one in my own answer. Up-voting...Harbard
H
4

The following produces the output you want:

class A:
    def bar(self):
        print("one")

class B(A):
    def bar(self):
        print("two")

def test(objs, funcname):
    noop = lambda: None
    for o in objs:
        getattr(o, funcname, noop)()

objs = [A(), B()]
test(objs, "bar")
Harbard answered 5/8, 2016 at 2:8 Comment(6)
This answers the question literally but I don't think it at all represents what OP is trying to accomplish. Surely this program wouldn't scale in a more complex example.Fraternity
@uh oh: IMO it answers the question and represents what the OP is trying to accomplish given that Python doesn't have pointers. Furthermore it should "scale" as well as the C++ version.Harbard
That was my point. Python doesn't have pointers or multiple dispatch, so I would expect a more idiomatic way of accomplishing the same objective. All this does is reinforce the wrong way of doing things.Fraternity
@uh oh: Other than you, who said anything about multiple dispatch? The code in my answer works with inheritance and shows the idiomatic way of referencing to a variable attribute of an instance. Feel free to post your own answer, even if it's "You can't do this in Python".Harbard
Thanks for this answer. I think it indeed does answer my original question the way it was asked. A similar solution crossed my mind, but I was hoping there is a cleaner/better one. If there isn't, I would suggest extending this answer with "this cannot be done in Python in any other way than:", I would also be happy to hear an opinion about the best pattern to use for my problem (see the edit).Blinders
And: Your edit doesn't make me feel the need to change my answer, plus I would be reluctant to say "can't be done in any other way" although I suspect any would just be doing essentially the same thing. Just let your question sit for a while, and see if anyone else offers what you think is a better approach.Harbard

© 2022 - 2024 — McMap. All rights reserved.