11

I watched Raymond Hettinger's Pycon talk "Super Considered Super" and learned a little bit about Python's MRO (Method Resolution Order) which linearises a classes "parent" classes in a deterministic way. We can use this to our advantage, like in the below code, to do dependency injection. So now, naturally, I want to use super for everything!

In the example below, the User class declares it's dependencies by inheriting from both LoggingService and UserService. This isn't particularly special. The interesting part is that we can use the Method Resolution Order also mock out dependencies during unit testing. The code below creates a MockUserService which inherits from UserService and provides an implementation of the methods we want to mock. In the example below, we provide an implementation of validate_credentials. In order to have MockUserService handle any calls to validate_credentials we need to position it before UserService in the MRO. This done by creating a wrapper class around User called MockUser and having it inherit from User and MockUserService.

Now, when we do MockUser.authenticate and it, in turn, calls to super().validate_credentials()MockUserService is before UserService in the Method Resolution Order and, since it offers a concrete implementation of validate_credentials this implementation will be used. Yay - we've successfully mocked out UserService in our unit tests. Consider that UserService might do some expensive network or database calls - we've just removed the latency factor of this. There is also no risk of UserService touching live/prod data.

class LoggingService(object): """ Just a contrived logging class for demonstration purposes """ def log_error(self, error): pass class UserService(object): """ Provide a method to authenticate the user by performing some expensive DB or network operation. """ def validate_credentials(self, username, password): print('> UserService::validate_credentials') return username == 'iainjames88' and password == 'secret' class User(LoggingService, UserService): """ A User model class for demonstration purposes. In production, this code authenticates user credentials by calling super().validate_credentials and having the MRO resolve which class should handle this call. """ def __init__(self, username, password): self.username = username self.password = password def authenticate(self): if super().validate_credentials(self.username, self.password): return True super().log_error('Incorrect username/password combination') return False class MockUserService(UserService): """ Provide an implementation for validate_credentials() method. Now, calls from super() stop here when part of MRO. """ def validate_credentials(self, username, password): print('> MockUserService::validate_credentials') return True class MockUser(User, MockUserService): """ A wrapper class around User to change it's MRO so that MockUserService is injected before UserService. """ pass if __name__ == '__main__': # Normal useage of the User class which uses UserService to resolve super().validate_credentials() calls. user = User('iainjames88', 'secret') print(user.authenticate()) # Use the wrapper class MockUser which positions the MockUserService before UserService in the MRO. Since the class # MockUserService provides an implementation for validate_credentials() calls to super().validate_credentials() from # MockUser class will be resolved by MockUserService and not passed to the next in line. mock_user = MockUser('iainjames88', 'secret') print(mock_user.authenticate()) 

This feels quite clever, but is this a good and valid use of Python's multiple inheritance and Method Resolution Order? When I think about inheritance in the way that I learned OOP with Java this feels completely wrong because we can't say User is a UserService or User is a LoggingService. Thinking that way, using inheritance the way the above code uses it doesn't make much sense. Or is it? If we use inheritance purely just to provide code reuse, and not thinking in terms of parent->children relationships, then this doesn't seem so bad.

Am I doing it wrong?

4
  • It seems like there's two different questions here: "Is this sort of MRO manipulation safe/stable?" and "Is it inaccurate to say that Python inheritance models an "is-a" relationship?" Are you trying to ask both of those, or just one of them? (they're both good questions, just want to make sure we answer the right one, or split this into two questions if you don't want both)
    – Ixrec
    CommentedJan 1, 2016 at 23:17
  • I have addressed the questions as I read it, have I left anything out?CommentedJan 2, 2016 at 0:05
  • @lxrec I think you're absolutely right. I'm trying to ask two different questions. I think the reason this doesn't feel "right" is because I'm thinking about "is-a" style of inheritance (so GoldenRetriever "is-a" Dog and Dog "is-a" Animal) instead of this type of compositional approach. I think this is something I could open another question for :)
    – Iain
    CommentedJan 2, 2016 at 0:14
  • This also confuses me significantly. If composition is preferable to inheritance why not pass instances of LoggingService and UserService to the constructor of User and set them as members? Then you could use duck typing for dependency injection and pass an instance of MockUserService to the User constructor instead. Why is using super for DI preferable?CommentedSep 25, 2018 at 18:27

1 Answer 1

7

Using Python's Method Resolution Order for Dependency Injection - is this bad?

No. This is a theoretical intended usage of the C3 linearization algorithm. This goes against your familiar is-a relationships, but some consider composition to be preferred to inheritance. In this case, you composed some has-a relationships. It seems you're on the right track (though Python has a logging module, so the semantics are a bit questionable, but as an academic exercise it's perfectly fine).

I don't think mocking or monkey-patching is a bad thing, but if you can avoid them with this method, good for you - with admittedly more complexity, you have avoided modifying the production class definitions.

Am I doing it wrong?

It looks good. You have overridden a potentially expensive method, without monkey-patching or using a mock patch, which, again, means you haven't even directly modified the production class definitions.

If the intent was to exercise the functionality without actually having credentials in the test, you should probably do something like:

>>> print(MockUser('foo', 'bar').authenticate()) > MockUserService::validate_credentials True 

instead of using your real credentials, and check that the parameters are received correctly, perhaps with assertions (as this is test code, after all.):

def validate_credentials(self, username, password): print('> MockUserService::validate_credentials') assert username_ok(username), 'username expected to be ok' assert password_ok(password), 'password expected to be ok' return True 

Otherwise, looks like you've figured it out. You can verify the MRO like this:

>>> MockUser.mro() [<class '__main__.MockUser'>, <class '__main__.User'>, <class '__main__.LoggingService'>, <class '__main__.MockUserService'>, <class '__main__.UserService'>, <class 'object'>] 

And you can verify that the MockUserService has precedence over the UserService.

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.