Bonus Challenge Discussion

One rabbit hole I’ve been exploring of late, and one knows this has been discussed of long last, is a class without an __init__() method.

>>> class SortedList(list):
	def append(self, value):
		super().append(value)
		self.sort()

		
>>> a = SortedList([4,3,6,54,7,2,8,1,9,7,8,5,6,3,4,5,6,1,2])
>>> a.append(19)
>>> a
[1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 19, 54]
>>> isinstance(a, SortedList)
True
>>> isinstance(a, list)
True
>>> 

The only thing missing is a sorted list upon instantiation. Once we append a dummy (or real) value the list is sorted. We can remove the dummy value if needs be.

>>> b = SortedList([14,23,36,54,47,52,68,71,89,97,18,25,36,43,54,65,76,81,92])
>>> b.append(19)
>>> b
[14, 18, 19, 23, 25, 36, 36, 43, 47, 52, 54, 54, 65, 68, 71, 76, 81, 89, 92, 97]
>>> a
[1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 19, 54]
>>> 

We can still have multiple instances of a class without initializing each instance, it would appear. This is worth exploring as it looks to be a vector by which we can deliver to our objects behavior that is not found in the Standard Library.

If you are talking about this bit of code that @itroyjan1 posted:

class SortedList(list):
  def __init__(self):
    self.sort()
  
  def append(self, value):
    super().append(value)
    self.sort()
    
print(SortedList([4, 1, 5]))

There are two issues with it.
First, the error that is thrown is because the __init__(self) should should take in a value for the new list to have, so it should be __init__(self, lst). The new code would look like this:

class SortedList(list):
  def __init__(self, lst):
    self.sort()
  
  def append(self, value):
    super().append(value)
    self.sort()
    
print(SortedList([4, 1, 5]))

Now for the second issue. Is you run the above code you’ll notice an empty list is printed, this is because we are not creating a new list with the value that was passed in. To do this, we need to call the list class’s __init__ method to initialise the internal state of the object with the passed in list argument. This is done with the super method.

So the final code would look like @suraj95a’s in the second post.

Is it possible to create that SortedList in one step as we would for a normal list, without defining our own class or instance variable?

I’m not 100% sure what you mean by this, I’m guessing you mean like SortedList([1,2,3])? If yes then the above solution will achieve that if not then you’d have to reword it for me.

2 Likes

To say this is a class without an __init__ is slightly misleading. This is a class that does not declare an __init__, it does actually have one. In this case, it is list.__init__() that is being called to initialise the SortedList.

This is easily proven by the following:

print(SortedList.__init__ is list.__init__)

This is worth exploring as it looks to be a vector by which we can deliver to our objects behavior that is not found in the Standard Library.

I don’t understand this statement, how would not defining an initialiser allow you to add behaviour to an object?

If for whatever reason you really wanted to have the initial value list that is passed in be sorted without defining an init method then use a class decorator. Like so:

def sort_argument(func):
    def wrapper(lst):
        lst.sort()
        return func(lst)
    return wrapper
    
@sort_argument
class SortedList(list):
	def append(self, value):
		super().append(value)
		self.sort()
		
print(SortedList([15, 3, 2, 6, 9]))
2 Likes

Good points, all around. Thanks for that. As I said, this is a bit of a rabbit hole for me, and many others.

I found it enough to override repr(). I also found issues trying to modify init and not sure what would be the best practice.
I leave my code below:

class SortedList(list):

def append(self, value):
super().append(value)
return self.sort()

def repr(self):
return “{}”.format(sorted(self))

lista = SortedList([5, 2, 10])
lista.append(8)
print(lista)

Is this what they meant in the exercise? I used the missing function to address the missing key part of the exercise.

class Superdict(dict):
  fallback_value = "Item not found!"
  def __init__(self, dic):
    super().__init__(dic)
    self.dic = dic
  def __missing__(self,key):
    return self.fallback_value
  
1 Like

Without overriding the Parent Class list .append method with self.sort(), appending values to a SortedList object result in a normal append operation - where values are added to the end of a list without keeping values sorted. See below:

class SortedList(list):
  
  def __init__(self, lst):
    super().__init__(lst)
    self.sort()

new_list = SortedList([4, 1, 5])
print(new_list)
[1, 4, 5]
for x in range(3):
  new_list.append(x)
print(new_list)
[1, 4, 5, 0, 1, 2]

Inheriting and overriding the Parent Class list .__init__ with self.sort() doesn’t affect the behavior of the Parent Class’ .append method.

Keeping the overridden .append intact with self.sort() keeps the list constantly sorted. In other words, overriding the Parent Class .append method is still necessary here.

class SortedList(list):
  
  def __init__(self, value):
    super().__init__(value)
    self.sort()
  
  def append(self, value):
    super().append(value)
    self.sort()
  

new_list = SortedList([4, 1, 5])
print(new_list)
[1, 4, 5]
for x in range(3):
  new_list.append(x)
print(new_list)
[0, 1, 1, 2, 4, 5]
4 Likes

My solution:

# print(dir(list))
class SortedList(list):
  def append(self, value):
    super().append(value)
    self.sort()
    
  def __init__(self, values):
    super().__init__(values)
    self.sort()
  
# sl = SortedList([4, 1, 5])
# print(sl)
# sl.append(2)
# print(sl)

# print(dir(dict))
class NewDict(dict):
  fallback = 'No such a key found!'
  def __getitem__(self, key):
    try:
      return super().__getitem__(key)
    except KeyError:
      return self.fallback
      
  def __init__(self, values):
    super().__init__(values)

# nd = NewDict({"a": 1, "b": 2, "c": 3})
# print(nd["d"])

How do we get repr to represent the answer correctly? My code is:

class SortedList(list):
  def __init__(self, lst):
    super().__init__(lst)
    self.lst = lst
    self.lst.sort()
  
  def __repr__(self):
    return str(self.lst)

  def append(self, value):
    super().append(value)
    self.sort()


print(SortedList([99, 6, 7]))

Also, why do we need to use super().__init__(lst) in our initialization? When I tried removing that line I got an error message.

I fixed the above code so it works correctly now, and I’ve realized that super can take another clarifying argument (see When do I need to use super()?). I’ve also tried the dictionary example, and I found another bit of weird behavior.

class BetterDict(dict):
  def __init__(self, dictionary):
    self.dictionary = dictionary
    super().__init__(dictionary)

  def get(self, key):
    if key in self.keys():
      return self.dictionary[key]
    else:
      return "No key found!"

my_dict = BetterDict({5 : "five", 3 : "three"})

print(my_dict.get(6))
# returns "No key found!"
print(my_dict.get(5))
# returns 5

When I remove the second line in init, my second print test returns the error message. Why is this? What does super().__init__(dictionary) do, exactly?

Try adding another variable and calling back to the parent class

class SortedList(list):
  def __init__(self,lst):
    super().__init__(lst)
    self.sort()
  def append(self, value):
    super().append(value)
    self.sort()
    
lst_to_sort = SortedList([10,6,5,13])
print(lst_to_sort)
1 Like

@itroyjan1, yup that works! Thanks!!

Hi everyone,

I just finishe the bonus challenge on inheritance and polymorphism, and came up with a solution, see code below.

As you can see, I wanted to enter the instances (new_dict) name (which is “new_dict”) into the fallback value string that is returned, if a key is not found in the dictionary.

In order to do that, I had to pass a name into the constructor when initializing the object as FallbackDict instance.

My questions is, is there any way to refer to the initialized object’s name itself without passing a name along with the dictionary content?
It’s not a biggy, but I thought since the object (in my case test_dict) already has a name, avoid redundancy and refer to the objects name instead of passing along the name separately.

Hope you get what I meant, and thanks.

Code
class FallbackDict(dict):
 def __init__(self, name, new_dict):
  super().__init__(new_dict)
  self.new_dict = new_dict
  self.name = name
  self.fallback_value = "There is no such key in {}.".format(self.name)

 def __repr__(self):
  return self.name
 def get(self, key):
  if key not in self.new_dict:
   return print(self.fallback_value)
  else:
   return print(super().get(key))

test_dict = FallbackDict("test_dict", {"gorilla": "mamal", "trout": "fish", "blackbird": "bird"})
test_dict.get("gorilla")
test_dict.get("gorila")

/edit: fixed the code block

If you can say that it’s redundant then, aren’t you saying that you already know where the information already is, and could therefore use that and remove the thing you think is redundant?

Also, your method is named “get”, not “print”. Right now, the result is printed, and the caller gets nothing.

Not sure what exactly you think is redundant, the one thing I do see is that your get potentially makes two lookups when it could be making one.

Thank you for pointing that out.

What do you mean by that, would you elborate? How is the getter potentielly making two lookups when one would suffice?

About the redundancy, I was merely assuming that there might be a way to refer to the name of the object, but don’t know cause my attempts to do so (without the extra name argument passed to the constructor) resulted in the dict assigned to the object getting printed.
If there was a way, it would make passing the name upon initilization redundant, at least in this case, right?

are you talking about this?
yes you’re both copying the dict and keeping a reference to it, so that does raise the question of… which one of them did you mean to do, and, if you only meant to do one of them, then what is the reasoning/purpose of the other?

1 Like

I was talking about passing a name argument to the constructor of FallbackDict

class FallbackDict(dict):

  def __init__(self, *name*, new_dict):

Seems necessary if I wanted to call the instance name (as used when initilizing, i.e., test_dict) in the string that is returned when the key is not found in the dictionary. Was just wondering if you could adress that “given” instances name directly. Anyway, was just quick thought, passing the given name as additional argument works to the end I intended.

I tried and, at least for this task, I could use either

v1
class FallbackDict(dict):
  def __init__(self, name, new_dict):
    super().__init__(new_dict)
    #self.new_dict = new_dict
    self.name = name
    self.fallback_value = "There is no such key in {}.".format(self.name)

  def get(self, key):
    if key not in self:
      return self.fallback_value
    else:
      return super().get(key)

or

v2
class FallbackDict(dict):
  def __init__(self, name, new_dict):
    #super().__init__(new_dict)
    self.new_dict = new_dict
    self.name = name
    self.fallback_value = "There is no such key in {}.".format(self.name)

  def get(self, key):
    if key not in self.new_dict:
      return self.fallback_value
    else:
      return self.new_dict.get(key)

In v1, new_dict is also initialized in the parent class of FallbackDict(dict) via super().init and can thus be called via super().get(key) in the subclass get function, right?

In v2, new_dict is kept as instance variable in the form of self.new_dict, and can thus be called in the getter by directly refering to it.

Now I have 2 questions:

If I call this:

print(test_dict.get("gorilla"))
print(test_dict.get("gorila"))
print(test_dict)

both work for getting the key/fallback_value

  1. in the if statement in the getter, why is it sufficient to refer to the test_dict simply as “self” in v1, while v2 needs self.new_dict?

  2. why does print(test_dict) for v1 return the dictionary with all key/value pairs, while print(test_dict) fpr v2 returns an empty dictionary {}.

from what I understood, I’d go with v1 since it includes the parent class dict and also uses the parents class’ getter within the child class’ getter, while v2 uses the child class getter twice, right?

Uhh. You mean that you want to know the variable name. Not sure why you would want to do that in the first place, and it may not be applicable. The caller knows which dict they’re using.

…because those are the respective places where you put it?

same reason, one is an empty dict the other isn’t


More likely, you would supply the fallback value:

class FallbackDict(dict):
    def __init__(self, default, *args):
        self.default = default
        super().__init__(*args)
    def get(self, key):
        return super().get(key, self.default)
    def __getitem__(self, key):
        return self.get(key)

also, you probably shouldn’t override get, because its purpose specifically has to do with what to do when something is missing, which is meaningful even when there’s a fallback, instead you should probably be overriding __getitem__

the fallback message probably shouldn’t be some form of error message, dict already raises an exception as its default behaviour for missing keys. a string “error” could also be interpreted as a regular value. the reason for having a fallback value is so that you can treat existing and missing as the same thing, for example, if you’re counting something, then a fallback of 0 would be appropriate, because something you have not seen before starts with a value of 0

Thanks for this hint.
Just in case someone wants to know where the link in the Python doc is:
https://docs.python.org/3/library/stdtypes.html?highlight=dict#mapping-types-dict
In the section: d[key]

1 Like

I don’t understand why there’s a need to have super().__init__ for sorting the list. Why can’t we just use lst.sort() for example? This prints an empty list.

class SortedList(list):
  def __init__(self, lst):
    lst.sort()

slist = SortedList([4,2,1])
print(slist)

This prints None.

class SortedList(list):
  def __init__(self, lst):
    self.sorted_list = lst.sort()

slist = SortedList([4,2,1])
print(slist.sorted_list)

While this prints a sorted list.

class SortedList(list):
  def __init__(self, lst):
    super().__init__(lst)
    self.sort()

slist = SortedList([4,2,1])
print(slist)

Also, why doesn’t it print <__main__.SortedList object at whatever>?

The idea of this exercise is to leverage the parent class, list, and implement its .append() method.

class SortedList(list):
    super().append(self, value)
    self.sort()

The object we pass in is already a list. It won’t be sorted until we append to it.

>>> a = SortedList([88,32,11,54,67,24,9,91, 18])
>>> a.append(42)
>>> a
[9, 11, 18, 24, 32, 42, 54, 67, 88, 91]
>>> 
1 Like