The answer to my question may depend on the interpreter for the code although I'm not sure. If it does, then I would be happy to hear about any widely used Python interpreter, especially CPython perhaps.
Consider the following (uninteresting) example of a callable object c.
class C:
def __call__(self):
pass
c = C()
Calling this c will call c.__call__, which may then use its own __call__ method in turn. However, in general, it's clear that not every callable object c always uses c.__call__ when it's called in a chain of calls such as considered.
To give an example, call of the callable(!) object get := vars(cls)["__get__"], where cls is the class types.WrapperDescriptorType, can't always rely on get.__call__ since get happens to be of type cls and so does call := vars(cls)["__call__"]! (If get.__get__ were needed for getting get.__call__, then getting it would have involved some call of get like get(get, get, cls) at some point. Actually, get.__call__ would be got not as call.__get__(get, cls) but apparently as get(call, get, cls).)
Question. Given a callable object c, how can one know whether call of it always relies on c.__call__?
I'd like a way which can be expected to work for all near future versions of Python, with the more robust way wanted more strongly.
You shouldn't ever need to check whether calling an object uses
__call__. That's good, because trying to check is really awkward.If you're worried about Python ignoring a
__call__method you write, you don't need to worry. Unless you try to set__call__on an instance or something (it needs to be on the class), Python will respect your__call__methods. The cases where Python internally bypasses__call__are cases you'd only need to worry about if you're writing C extensions or working on the interpreter core itself.You've correctly recognized that not all callables can be called by calling their
__call__method, because trying to do so would involve recursion with no base case. Aside from the__get__example you used, there's also the problem of, how do you call a__call__method? Call its__call__method? How would you call that?The primary way this is resolved in the CPython implementation is that, at C level,
__call__isn't actually the primary hook for the function call operator. The primary hook is atp_callslot in every type object's memory layout, holding a function pointer to the C-level function that handles the call operator for this type.For types that don't implement
__call__,tp_callis NULL. For types with__call__written in Python,tp_callholds a pointer toslot_tp_call, a C function that searches the type's MRO for a Python-level__call__method and calls that.But for types with
__call__written in C,tp_callholds a pointer to a C function that directly implements the type's call functionality. In this case, there is no need to go through a Python-level__call__method, and no need to invoke further__call__methods in the process of finding and calling that method.(There's also a
tp_vectorcallslot some types use for more efficient handling, and a bunch of places where CPython special-cases certain types and hardcodes the right handling instead of going throughtp_call.)Types with a
tp_callthat works this way still have a__call__method. In this case,__call__is usually set to an instance oftypes.WrapperDescriptorTypewrapping thetp_callfunction pointer, where calling the__call__method delegates to the function pointer.It would be extremely unusual for a program to ever need to check whether calling a type bypasses
__call__, because one oftp_callor__call__is always supposed to delegate to the other. It would take very unusual issues for a type'stp_calland__call__to behave differently from each other - for example, memory corruption, or an interpreter core bug, and in most such cases, the interpreter state would be broken enough that your check wouldn't do much good.If you really wanted to check anyway, the most robust check would probably be to compare the class's
tp_callpointer againstslot_tp_call, but neithertp_callnorslot_tp_callis exposed at Python level.slot_tp_callis evenstaticat C level, so you wouldn't even be able to writetypeobj->tp_call == &slot_tp_callin a C extension. You'd have to retrieve theslot_tp_callpointer from thetp_callslot of a type you know usesslot_tp_call.Less robust checks would be things like assuming
tp_callwill delegate to__call__if and only if a type's__call__is an ordinary Python function object: