Background
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 object
? object
and Foo
cannot be used interchangeably, as object
accepts exactly 0 parameters to its constructor, while Foo
accepts exactly 1.
Question 2: collections.Counter
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.abc.MutableMapping
or collections.UserDict
instead.
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 NotImplementedError
; collections.Counter
does.
Comments
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.