Reputation: 290
I was quite surprised to see that Python - at least partially - seems to violate some oo / SOLID principles.
Example:
class FractionTuple(tuple):
def __new__(cls, p, q):
# Ensure that p and q are integers
if not isinstance(p, int) or not isinstance(q, int):
raise TypeError("Both elements must be integers")
# Create the tuple of length 2
return super().__new__(cls, (p, q))
From a code reuse perspective this seems like a practical way to go - however it violates the substitutability principle: preconditions to use the derived class FractionTuple
are strengthened compared to the base class tuple
.
The Liskov substitution principle (LSP) is a criterion in object-oriented programming on conditions for modeling a subclass. It states that a program that uses objects of a base class must also work correctly with objects of the derived subclass without changing the program. In terms of individual methods this means that when a method is overwritten by a derived class, the preconditions may only be weakened and the postconditions may only be strengthened. A common example that violates LSP is a base class ellipse
and a derived class circle
.
Is this violation common practice in the Python community, i.e. is code re-use preferred oder LSP? Or is the FractionTuple an exception?
Upvotes: -1
Views: 215
Reputation: 522501
As you quote:
“If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T”
The important point here is: objects of type. In other words, once you have an instance, that instance must behave in a way compatible with objects of its super type. How you get such an instance is a different story.
So, this must work:
def foo(bar: T):
bar.baz()
This function expects an instance of type T
. You may also pass it an instance of type S
, as long as it correctly implements baz
. This is what the LSP is about.
The LSP says nothing about constructing an instance from a type. Of course this instantiation will widely differ between different types. If all types would also be required to implement the exact same constructor, there would be very little room to let them implement any customisation whatsoever. Example:
class Shape(ABC):
@abstractmethod
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * (self.radius ** 2)
class Square(Shape):
def __init__(self, length):
self.length = length
def area(self):
return self.length ** 2
The instance method area
must be compatible, but of course a Circle
needs different constructor arguments than a Square
needs different constructor arguments than a Polygon
.
In more practical terms, if you're implementing a, say, session storage backend, you may implement several alternative classes, like a database-backed storage, an in-memory storage and a file-backed storage. These objects can all present the same instance interface to store and retrieve session information, but of course will take very different constructor arguments. The database-backed store takes a database connection, the in-memory store probably takes nothing, and the file-store takes a path to a local directory.
Upvotes: 1