Joooeey
Joooeey

Reputation: 3886

call Python method if it exists

How to call a subclass method from a base class method, only if the subclass supports that method? And what's the best way to do that? Illustrative example, I have an animal that protects my house: if someone walks by it will look angry, and it will bark if it can.

Example code:

class Protector(object):
    def protect(self):
        self.lookangry()
        if hasattr(self, 'bark'):
            self.bark()

class GermanShepherd(Protector):
    def lookangry(self):
        print u') _ _ __/°°¬'
    def bark(self):
        print 'wau wau'

class ScaryCat(Protector):
    def lookangry(self):
        print '=^..^='

I can think of lots of alternative implementations for this:

  1. Using hasattr as above.
  2. try: self.bark() except AttributeError: pass but that also catches any AttributeErrors in bark
  3. Same as 2 but inspect the error message to make sure it's the right AttributeError
  4. Like 2 but define an abstract bark method that raises NotImplementedError in the abstract class and check for NotImplementedError instead of AttributeError. With this solution Pylint will complain that I forgot to override the abstract method in ScaryCat.
  5. Define an empty bark method in the abstract class:

    class Protector(object):
        def protect(self):
            self.lookangry()
            self.bark()
        def bark(self):
            pass
    

I figured in Python their should usually be one way to do something. In this case it's not clear to me which. Which one of these options is most readable, least likely to introduce a bug when stuff is changed and most inline with coding standards, especially Pylint? Is there a better way to do it that I've missed?

Upvotes: 4

Views: 1340

Answers (3)

abarnert
abarnert

Reputation: 365935

You missed one possibility:

Define a bark method that raises NotImplementedError, as in your option 4, but don't make it abstract.

This eliminates PyLint's complaint—and, more importantly, eliminates the legitimate problem it was complaining about.


As for your other options:

  • hasattr is unnecessary LBYL, which is usually not Pythonic.
  • The except problem can be handled by doing bark = self.bark inside a try block, then doing bark() if it passes. This is sometimes necessary, but the fact that it's a bit clumsy and hasn't been "fixed" should give you an idea of how often it's worth doing.
  • Inspecting error messages is an anti-pattern. Anything that's not a separate, documented argument value is subject to change across Python versions and implementations. (Plus, what if ManWithSidekick.bark() does self.sidekick.bark()? How would you distinguish the AttributeError there?)

So, that leaves 2, 4.5, and 5.

I think in most cases, either 4.5 or 5 will be the right thing to do. The difference between them is not pragmatic, but conceptual: If a ScaryCat an animal that barks silently, use option 5; if not, then barking must be an optional part of protection that not all protectors do, in which case use option 4.5.

For this toy example, I think I'd use option 4.5. And I think that will be the case with most toy examples you come up with.

However, I suspect that most real-life examples will be pretty different:

  • Most real-life examples won't need this deep hierarchy.
  • Of those that do, usually either bark will either be implemented by all subclasses, or won't be called by the superclass.
  • Of those that do need this, I think option 5 will usually fit. Sure, barking silently is not something a ScaryCat does, but parse_frame silently is something a ProxyProtocol does.
  • And there are so few exceptions left after that, that it's hard to speak about them abstractly and generally.

Upvotes: 1

wiesion
wiesion

Reputation: 2455

I think 6.) could be that the Protector class makes just the basic shared methods abstract thus required, while leaving the extra methods to its heirs. Of course this can be splitted into more sub-classes, see https://repl.it/repls/AridScrawnyCoderesource (Written in Python 3.6)

class Protector(object):
  def lookangry(self):
    raise NotImplementedError("If it can't look angry, it can't protect")

  def protect(self):
      self.lookangry()


class Doggo(Protector):
  def bark(self):
    raise NotImplementedError("If a dog can't bark, it can't protect")

  def protect(self):
    super().protect()
    self.bark()


class GermanShepherd(Doggo):

  def lookangry(self):
    print(') _ _ __/°°¬')

  def bark(self):
    print('wau wau')


class Pug(Doggo):
  # We will not consider that screeching as barking so no bark method
  def lookangry(self):
    print('(◉ω◉)')


class ScaryCat(Protector):
  def lookangry(self):
      print('o(≧o≦)o')


class Kitten(Protector):
  pass


doggo = GermanShepherd()
doggo.protect()

try:
  gleam_of_silver = Pug()
  gleam_of_silver.protect()
except NotImplementedError as e:
  print(e)

cheezburger = ScaryCat()
cheezburger.protect()

try:
  ball_of_wool = Kitten()
  ball_of_wool.protect()
except NotImplementedError as e:
  print(e)

Upvotes: 1

bnaecker
bnaecker

Reputation: 6440

It seems to me you're thinking about inheritance incorrectly. The base class is supposed to encapsulate everything that is shared across any of the subclasses. If something is not shared by all subclasses, by definition it is not part of the base class.

So your statement "if someone walks by it will look angry, and it will bark if it can" doesn't make sense to me. The "bark if it can" part is not shared across all subclasses, therefore it shouldn't be implemented in the base class.

What should happen is that the subclass that you want to bark adds this functionality to the protect() method. As in:

class Protector():
    def protect(self):
        self.lookangry()

class GermanShepherd(Protector):
    def protect(self):
        super().protect() # or super(GermanShepherd, self).protect() for Python 2
        self.bark()

This way all subclasses will lookangry(), but the subclasses which implement a bark() method will have it as part of the extended functionality of the superclass's protect() method.

Upvotes: 3

Related Questions