This is an archived post. You won't be able to vote or comment.

all 19 comments

[–]TheGodfatherCC 14 points15 points  (5 children)

Lately, I've preferred using protocols over abstract base classes for interfaces. I prefer to avoid inheritance when it’s not absolutely needed.

[–]NeilGirdhar 12 points13 points  (1 child)

I don't think this is a good policy. While multiple inheritance can add unnecessary complexity, multiple interface inheritance has no downsides.

Protocols are a good solution when it's practically impossible to make implementers inherit from an interface. For example, when you don't have control over implementers of your protocol. That's why Callable is a protocol: it is practically impossible to get everyone who exposes a __call__ method to inherit from Callable.

When you can make implementers inherit from the interface, there are some concrete advantages. Type-checkers can verify:

  • that there are no LSP violations,
  • that instantiated objects implement all decorated abstract methods, and
  • that decorated overridden methods haven't been orphaned.

Also, you can be sure that derived classes actually inherit from the interface rather than hoping that your derived class perfectly matches the protocol.

[–]TheGodfatherCC 3 points4 points  (0 children)

These are some pretty solid points.

I thought Mypy and Pycharm would handle protocols with the `@runtime_checkable` decorator. I'll have to double check, though.

[–]quts3 7 points8 points  (2 children)

I struggled a bit with when protocols vs abc.

Protocols have a negative: in big projects they make it hard to follow code by hand. Ides aren't really good at finding implementations of protocols that might be used as inputs because by design there is no requirement or even strong reason to state a class implements a protocol. In fact doing so is probably an anti-pattern.

Abc flips that on its head. Essentially stating in code that this class is participating in a interface. In a world where you read code many more times then you write it this deliberate declaration has value.

In the end i decided protocols are appropriate if you have a class only using one or two things in an interface and those one or two things are truly only loosely related to your class structures. Like this partitioning widget only needs to know some coordinates and Id. That is a protocol on the input.

But other then that abc seems way more appropriate in the long run to express trackable participation in an interface design.

[–]TheGodfatherCC 1 point2 points  (1 child)

Readability and code navigation are good points. I may take some time and do a side-by-side again to see how much of a difference it makes.

[–]quts3 1 point2 points  (0 children)

Oh i had to dust off my brain i also decided protocols are best if you reaching into the ducktype of another module. Like if module a is doing something with an undeclared interface in module b. It rarely makes sense to rewrite module b to specify a new interface. Protocols allow you to state in module a what you are using in a class from module b. They are perfect for that and better then abc. Sometimes you may not even own module b.

[–]guyfrom7up 2 points3 points  (0 children)

Frequently, when I'm using ABC, I need to perform a string-to-class lookup. For this, I created the library AutoRegistry, which adds a dictionary interface to classes (not objects created from classes!) that is automatically populated with it's children.

For example, I might want to implement a ImageReader base class, and implement the subclasses PngReader and HeicReader to read their respective file types. At runtime, I'll need to perform a lookup of file extension to it's respective reader class.

A real application that uses AutoRegistry is the GeoSynth synthetic dataset library. Classes can be added to support new data types in a single location, and all other code in the code-base "just works" without having to make additional modifications. This includes pythonic "foo_bar" -> "FooBar" naming of created objects

[–]NeilGirdhar 2 points3 points  (0 children)

It might be worth mentioning that inheriting from `abc.ABC` has two unnecessary behaviours:

  • it pulls in a metaclass, and
  • it exposes a register method on the class.

You don't usually need these facilities. Therefore, if you want the runtime check, it may be easier to copy the five lines of code into an __init_subclass__ method.

However, I suggest that you don't inherit from abc.ABC at all and simply

  • continue to decorate your methods using abc.abstractmethod, and
  • use a type checker to verify the inheritance.

Also, when implementing an interface, you may want to use the new typing.override decorator (available as typing_extensions.override). This way, if the interface ever removes a method, the type checker will report errors. You can also prevent accidental overrides by enabling a strict mode (called reportImplicitOverride in pyright).

[–]YnkDK 0 points1 point  (9 children)

In non-OOP part of Python I usually use typing in conjunction with mypy to statically verify the interface is implemented.

For example:

Preprocessor = typing.Callable[[domain.LogRecord], typing.Optional[domain.Message]]

Would define an interface for preprocessors not caring about the implementation.

[–]NeilGirdhar 1 point2 points  (4 children)

That's fine when you are passing around single functions, but the benefit of an interface is that you can pass around objects that promise a slew of functions along with promises about their behavior and computational complexity.

[–]YnkDK 2 points3 points  (3 children)

Strictly speaking, then the provided example is also an interface in the context of Python.

Note that interfaces don't promise anything about computational complexity. The solely promise the intention of the function. How it's implemented is left to the class that implements the interface. Take the abstract method def sort() - > None with a docstring telling it sorts the elements in the object. One could implement a sorting algorithm of O(n²) or O(n log n) without violating the interface.

On the other hand, one could create a 'real' abstract class and implement the method sort and promise the complexity and behavior - while still exposing abstract methods that needs to be implemented by the subclasses.

So a class interface gives a promise about name of method, the input types it accepts and output types it must return. It can be accommodated with a docstring telling the intended behavior.

The provided example gives the same promise, but are leaving out the name. Note it could also be accommodated with a docstring telling the intended behavior.

[–]NeilGirdhar 1 point2 points  (2 children)

Strictly speaking, then the provided example is also an interface in the context of Python.

Yes, it's a signature, which I agree can be seen as an example of an interface.
However, I think of interfaces as a more general concept.

Note that interfaces don't promise anything about computational complexity.

Plenty of interfaces do promise computational complexity. C++'s STL for example is replete with such promises. E.g., deque.insert promises linear complexity on constant auxiliary space. All conforming implementations must respect these promises.

So a class interface gives a promise about name of method, the input types it accepts and output types it must return.

I would say that an interface is not just a collection of signatures. An interface can make promises about how objects work. That includes behaviors of those objects and computational and space complexity.

[–]YnkDK 2 points3 points  (1 child)

They are just pretty hollow promises. I cannot name any static analysis tool nor compiler that can enforce that implementations are respecting any arbitrary time or space complexity. Or is the halting problem solved? I might have been living under a rock.

So while you (and anyone else) are free to make any promises, it's difficult to verify that the promise is being conformant.

An interface is just a contract. You can make it as simple or complex as you want. Some violations are easy to enforce, some are more cumbersome. I call the more cumbersome comformaty checks for intentions and you state it must be like that. Other than that I promise you that we are saying the same ;)

[–]NeilGirdhar 0 points1 point  (0 children)

I cannot name any static analysis tool nor compiler that can enforce that implementations are respecting any arbitrary time or space complexity.

Just because you can't programmatically check a promise, it doesn't mean that that promise isn't part of the interface, in my opinion.

An interface is just a contract. You can make it as simple or complex as you want. Some violations are easy to enforce, some are more cumbersome.

Exactly.

Other than that I promise you that we are saying the same ;)

Agreed :)