I’ve taught myself Python over the past year-and-a-bit, and would consider myself an intermediate Python user at this point, but never studied computing at school/university. As such, my knowledge of theory is a little weak. Python is the only language I know.
I’m trying to wrap my head around the Liskov Substitution Principle so that I can write better, more object-oriented code.
Question 1: The Python data model
As we know, in Python 3, all custom classes implicitly inherit from
object. But the docstring of
object includes this line:
When called, it accepts no arguments and returns a new featureless instance that has no instance attributes and cannot be given any.
With this in mind, how can any python classes that accept a nonzero number of parameters for their constructors be said to comply with the LSP? If I have a class
Foo, like so:
class Foo: def __init__(self, bar): self.bar = bar
then, surely this class definition (and all others like it) violates the contract specified by
Foo cannot be used interchangeably, as
object accepts exactly 0 parameters to its constructor, while
Foo accepts exactly 1.
As Raymond Hettinger tells us, the Python dictionary is an excellent example of the open/closed principle. The
dict class is “open for extension, closed for modification” — if you wish to modify some of the prepackaged behaviours of a
dict, you’re advised to inherit from
collections.Counter, however, is a direct subclass of
dict. While it is mostly an extension of
dict rather than a modification of behaviours already defined in
dict, this isn’t true for
collection.Counter.fromkeys. With a standard
dict, this classmethod is an alternative constructor for a dictionary, but the method is overridden in
Counter so that it raises an exception. Here is the comment explaining why this is the case:
# There is no equivalent method for counters because the semantics # would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2). # Initializing counters to zero values isn't necessary because zero # is already the default value for counter lookups. Initializing # to one is easily accomplished with Counter(set(iterable)). For # more exotic cases, create a dictionary first using a dictionary # comprehension or dict.fromkeys().
The explanation makes sense — but surely this violates the LSP? According to the Wikipedia article, the LSP states that “New exceptions cannot be thrown by the methods in the subtype, except if they are subtypes of exceptions thrown by the methods of the supertype.”
dict.fromkeys does not throw a
I’m interested in whether these examples do, in fact, break the LSP. However, I’m also interested in why they break the LSP, if indeed they do. In what situations is enforcing the LSP necessary/advisable/useful? In what situations is worrying about the LSP more of a meaningless distraction? Etc.