9

Given that Python allows for multiple inheritance, what does idiomatic inheritance in Python look like?

In languages with single inheritance, like Java, inheritance would be used when you could say that one object "is-a" of an another object and you want to share code between the objects (from the parent object to the child object). For example, you could say that Dog is a Animal:

public class Animal {...}
public class Dog extends Animal {...}

But since Python supports multiple inheritance we can create an object by composing many other objects together. Consider the example below:

class UserService(object):
    def validate_credentials(self, username, password):
        # validate the user credentials are correct
        pass


class LoggingService(object):
    def log_error(self, error):
        # log an error
        pass


class User(UserService, LoggingService):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if not super().validate_credentials(self.username, self.password):
            super().log_error('Invalid credentials supplied')
            return False
         return True

Is this an acceptable or good use of multiple inheritance in Python? Instead of saying inheritance is when one object "is-a" of another object, we create a User model composed of UserService and LoggingService.

All the logic for database or network operations can be kept separate from the User model by putting them in the UserService object and keep all the logic for logging in the LoggingService.

I see some problems with this approach are:

  • Does this create a God object? Since User inherits from, or is composed of, UserService and LoggingService is it really following the principle of single responsibility?
  • In order to access methods on a parent/next-in-line object (e.g., UserService.validate_credentials we have to use super. This makes it a little bit more difficult to see which object is going to handle this method and isn't as clear as, say, instantiating UserService and doing something like self.user_service.validate_credentials

What would be the Pythonic way to implement the above code?

Iain
  • 450
  • 2
  • 10

2 Answers2

9

Is Python's inheritance an “is-a” style of inheritance or a compositional style?

Python supports both styles. You are demonstrating the has-a relationship of composition, where a User has logging functionality from one source and credential validation from another source. The LoggingService and UserService bases are mixins: they provide functionality and are not intended to be instantiated by themselves.

By composing mixins in the type, you have a User that can Log, but who must add his own instantiation functionality.

This does not mean that one cannot stick to single inheritance. Python supports that too. If your ability to develop is hampered by the complexity of multiple inheritance, you can avoid it until you feel more comfortable or come to a point in design where you believe it to be worth the trade-off.

Does this create a God object?

The logging does seem somewhat tangential - Python does have its own logging module with a logger object, and the convention is that there is one logger per module.

But set the logging module aside. Perhaps this violates single-responsibility, but perhaps, in your specific context, it is crucial to defining a User. Descretizing responsibility can be controversial. But the broader principle is that Python allows the user to make the decision.

Is super less clear?

super is only necessary when you need to delegate to a parent in the Method Resolution Order (MRO) from inside of a function of the same name. Using it instead of hard-coding a call to the parent's method is a best-practice. But if you were not going to hard-code the parent, you don't need super.

In your example here, you only need to do self.validate_credentials. self is not more clear, from your perspective. They both follow the MRO. I would simply use each where appropriate.

If you had called authenticate, validate_credentials instead, you would have needed to use super (or hard-code the parent) to avoid a recursion error.

Alternative Code suggestion

So, assuming the semantics are OK (like logging), what I would do is, in class User:

    def validate_credentials(self): # changed from "authenticate" to 
                                    # demonstrate need for super
        if not super().validate_credentials(self.username, self.password):
            # just use self on next call, no need for super:
            self.log_error('Invalid credentials supplied') 
            return False
        return True
Robert Harvey
  • 198,589
  • 55
  • 464
  • 673
Aaron Hall
  • 5,895
  • 4
  • 25
  • 47
  • 1
    I disagree. Inheritance always creates a copy of the public interface of a class in its subclasses. This isn't a "has-a" relationship. This is subtyping, plain and simple, and therefore is inappropriate to the described application. – Jules May 23 '16 at 18:24
  • 1
    @Jules What do you disagree with? I said a lot of things that are demonstrable, and made conclusions that logically follow. You *are* incorrect when you say, "Inheritance always creates a copy of the public interface of a class in its subclasses." In Python, there is no copy - the methods are looked up dynamically according to the C3 algorithm method resolution order (MRO). – Aaron Hall May 23 '16 at 18:34
  • 1
    The point isn't about specific details of how the implementation works, it's about what the public interface of the class looks like. In the case of the example, `User` objects have in their interfaces not only the members defined in the `User` class, but also the ones defined in `UserService` and `LoggingService`. This *isn't* a "has-a" relationship, because the public interface is copied (albeit not via direct copying, but rather by an indirect lookup to the superclasses' interfaces). – Jules May 23 '16 at 18:41
  • 1
    Has-a means Composition. Mixins are a form of Composition. The User class *is* not a UserService or LoggingService, but it *has* that functionality. I think Python's inheritance is more different from Java's than you realize. – Aaron Hall May 23 '16 at 21:14
  • @AaronHall You are over-simplifying (this tends to contradict you [other answer](http://stackoverflow.com/a/27907511/124319), which I found by chance). From the point of view of subtype relationship, a User is both a UserService and a LoggingService. Now, the spirit here is to compose functionalities so that a User has such and such functionalities. Mixins in general do not require to be implemented with multiple-inheritance. However this is the usual way of doing so in Python. – coredump May 23 '16 at 21:53
  • 1
    Mixins are a special case of inheritance that provides for composition. "is-a" would be the complementing cases of inheritance that would require an abstract base class or a base class that one could fully instantiate. This is internally consistent. Whether it is helpful to engage in these sorts of semantic hair-splitting is another matter entirely. What I want to emphasize is that composition by delegation is not the only way to do it. – Aaron Hall May 23 '16 at 22:41
  • I don't believe that suggesting that you should avoid adding unwanted and inappropriate methods to the public interface of a class is "hair splitting". Simple delegation is a much cleaner and more general approach than abusing inheritance like this, and doesn't even require significant boilerplate, so I don't understand why anyone would recommend this approach when the alternative is clearly better. Or downvote an answer suggesting the alternative. – Jules May 24 '16 at 12:30
  • "The User class is not a UserService or LoggingService, but it has that functionality" -- and publically exposes it. See https://repl.it/CU5o for an example. In what way, therefore, is User *not* a UserService? In a duck-typed language, what is this difference between the statements "User *is-a* UserService" and "User publically exposes identical functionality to UserService (potentially along with additional functions)"? – Jules May 24 '16 at 12:39
  • These semantics aren't incredibly useful. "is-a" doesn't make sense because you're saying "the whole" is-a "non-self-standing part of some whole." Being able to test for functionality with `isinstance` doesn't mean we are required to say "is-a". – Aaron Hall May 24 '16 at 13:50
-1

Other than the fact that it allows multiple superclasses, Python's inheritance is not substantially different to Java's, i.e. members of a subclass are also members of each of their supertypes[1]. The fact that Python uses duck-typing makes no difference either: your subclass has all of the members of its superclasses, so is able to be used by any code that could use those subclasses. The fact that multiple inheritance is effectively implemented by using composition is a red herring: that automated copying of the properties of one class to another is the issue, and it doesn't matter whether it uses composition or just magically guesses how the members are supposed to work: having them is wrong.

Yes, this violates single responsibility, because you're giving your objects the ability to perform actions that are not logically part of what they're designed to do. Yes, it creates "god" objects, which is essentially another way of saying the same thing.

When designing object-oriented systems in Python, the same maxim that is preached by Java design books also applies: prefer composition to inheritance. The same is true of (most[2]) other systems with multiple inheritance.

[1]: you could call that an "is-a" relationship, although I don't personally like the term because it suggests the idea of modelling the real world, and object oriented modelling is not the same as the real world.

[2]: I'm not so sure about C++. C++ supports "private inheritance" which is essentially composition without needing to specify a field name when you want to use the inherited class's public members. It doesn't affect the public interface of the class at all. I don't like using it, but I can't see any good reason not to.

Jules
  • 17,614
  • 2
  • 33
  • 63