Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make an Python subclass uncallable

Tags:

python

How do you "disable" the __call__ method on a subclass so the following would be true:

class Parent(object):
    def __call__(self):
        return

class Child(Parent):
    def __init__(self):
        super(Child, self).__init__()
        object.__setattr__(self, '__call__', None)

>>> c = Child()
>>> callable(c)
False

This and other ways of trying to set __call__ to some non-callable value still result in the child appearing as callable.

like image 390
Cerin Avatar asked May 13 '15 21:05

Cerin


2 Answers

You can't. As jonrsharpe points out, there's no way to make Child appear to not have the attribute, and that's what callable(Child()) relies on to produce its answer. Even making it a descriptor that raises AttributeError won't work, per this bug report: https://bugs.python.org/issue23990 . A python 2 example:

>>> class Parent(object):
...     def __call__(self): pass
... 
>>> class Child(Parent):
...     __call__ = property()
...
>>> c = Child()
>>> c()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: unreadable attribute
>>> c.__call__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: unreadable attribute
>>> callable(c)
True

This is because callable(...) doesn't act out the descriptor protocol. Actually calling the object, or accessing a __call__ attribute, involves retrieving the method even if it's behind a property, through the normal descriptor protocol. But callable(...) doesn't bother going that far, if it finds anything at all it is satisfied, and every subclass of Parent will have something for __call__ -- either an attribute in a subclass, or the definition from Parent.

So while you can make actually calling the instance fail with any exception you want, you can't ever make callable(some_instance_of_parent) return False.

like image 122
Devin Jeanpierre Avatar answered Nov 01 '22 21:11

Devin Jeanpierre


It's a bad idea to change the public interface of the class so radically from the parent to the base.

As pointed out elsewhere, you cant uninherit __call__. If you really need to mix in callable and non callable classes you should use another test (adding a class attribute) or simply making it safe to call the variants with no functionality.

To do the latter, You could override the __call__ to raise NotImplemented (or better, a custom exception of your own) if for some reason you wanted to mix a non-callable class in with the callable variants:

class Parent(object):
    def __call__(self):
        print "called"

class Child (Parent):
    def __call__(self):
        raise NotACallableInstanceException()

for child_or_parent in list_of_children_and_parents():
   try:  
      child_or_parent()
   except NotACallableInstanceException:
      pass

Or, just override call with pass:

class Parent(object):
    def __call__(self):
        print "called"

class Child (Parent):
    def __call__(self):
        pass

Which will still be callable but just be a nullop.

like image 25
theodox Avatar answered Nov 01 '22 23:11

theodox