James K J
James K J

Reputation: 615

Subclassing from Dict violates basic rule of object oriented programming in python

I am requesting better insights on the author's comments for a particular section of code. To go much in detail, I will illustrate with an example.

 class DoppelDict(dict):           
     def __setitem__(self, key, value):
        super().__setitem__(key, [value] * 2)

 # case 1.
 dd = DoppelDict(one=1)
 print(dd)  # {'one':1}

 # case 2.
 dd['two'] = 2
 print(dd)  # {'one':1,'two':[2,2]}

The above example is picked from a book and the author comments 'Built-in behavior is a violation of a basic rule of object-oriented programming: the search for methods should always start from the class of the target instance (self), even when the call happens inside a method implemented in a superclass'.

I believe the author is trying to convey since python ignores special methods overridden by user-defined class, it is a violation of OOP. I wanted to know whether my explanation is correct?. Do you have any other explanation to author comments?.

Upvotes: 4

Views: 222

Answers (2)

Patrick Artner
Patrick Artner

Reputation: 51643

This is an implemetation detail problem - it highly depends on what the super-called contructor of the base class does - in this case it does not call __setitem__.

You can fix it though:

class DoppelDict(dict):
    # force it to use setitem instead of update
    def __init__(self, *kargs, **kwargs):
        # probably also should do something with kargs
        for k,w in kwargs.items():
            super().__setitem__(k,[w]*2)  # see Graipher comment - for less code duplication
                                          # one could use self[k] = w .. plus 1 function call
                                          # but less code duplication for the special logic
    def __setitem__(self, key, value):
        super().__setitem__(key, [value] * 2)

# case 1.
dd = DoppelDict(one=1)
print(dd)  # {'one': [1, 1]}

# case 2.
dd['two'] = 2
print(dd)  # {'one': [1, 1], 'two': [2, 2]}

In pythons dict-case it does not use __setitem__.


You can have the same problem in "fully" OOP languages, for example in C#:

public class Base
{
    public Base(Dictionary<string, int> d)
    {
        // if the base constructor internally uses SetItem(..) it works as expected
        // if you overload SetItem in the Child-Klasses:
        foreach (KeyValuePair<string, int> kvp in d)
            SetItem(kvp); 

        // if the base constructor does _not_ use SetItem(..) it does not work by
        // overloading child classes SetItem() method: 
        // foreach (KeyValuePair<string, int> kvp in d) 
        //    D[kvp.Key] = kvp.Value; 
    }

    public Dictionary<string, int> D { get; } = new Dictionary<string, int>();

    public override string ToString() 
        => string.Join(",", D.Select(kvp => $"{kvp.Key}={kvp.Value}"));

    protected virtual void SetItem(KeyValuePair<string, int> kvp) => D[kvp.Key] = kvp.Value;
}

public class Other : Base
{
    public Other(Dictionary<string, int> d) : base(d) { }

    // overridden implementation doubling the incoming value
    protected override void SetItem(KeyValuePair<string, int> kvp)
        => D[kvp.Key] = 2 * kvp.Value;
}

You can test this using

public static void Main(string[] args)
{
    Dictionary<string, int> d = new Dictionary<string, int> { ["a"] = 1, ["b"] = 3 };

    Base b = new Base(d);
    Other o = new Other(d);

    Console.WriteLine(b.ToString());
    Console.WriteLine(o.ToString());

    Console.ReadLine();
}

and commenting either one of the Base()-ctor implementations.

You either get (not using SetItem(..))

a=1,b=3
a=1,b=3

or (using SetItem(..))

a=1,b=3
a=1,b=6

as outputs.

Upvotes: 2

Rohit
Rohit

Reputation: 4158

I can't really comment on the "Built-in behavior is a violation of a basic rule of object-oriented programming:". But in your code there are two separate and very different things happening.

When you do

dd = DoppelDict(one=1)

This looks for __init__ in the MRO and since your class didn't override __init__ and so __init__ method of the super class which is dict is called.

However when you do

dd['two'] = 2

python looks for __setitem__ method in the MRO which you have overriden and hence it is called and you get the expected result.

Its all related to super and MRO. You can easily look at the MRO for any class by simply checking __mro__ attribute.

In[5]: a = 100
In[6]: a.__class__.__mro__
Out[6]: (int, object)

The above example is just for an builtin class but the same applies to any other custom class as well.

Upvotes: 5

Related Questions