Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python

super().__init__()

Why are we putting super().__init__() in the Hand class? I know that it creates an empty list so we can add stuff to that, but isn't the Hand list enough? I tried to run the program without this line and it still works... Just the stuff was appended to Hand itself. Also what confuses me is self after super().__init__(). To which instance(am I calling it right?) this self refers to? The new list created in super().__init__() or the Hand instance?

from dice import D6

class Hand(list):
    def __init__(self, size=0, die_class=None, *args, **kwargs):  #size - the number of dice to roll
        if not die_class:
            raise ValueError("You must provide a die_class")
        super().__init__() 

        for _ in range(size):
            self.append(die_class())
        self.sort()

    def _by_value(self, value):
        dice = []
        for die in self:
            if die == value:
                dice.append(die)
        return dice

class YatzyHand(Hand):
    def __init__(self, *args, **kwargs):
        super().__init__(size=5, die_class=D6, *args, **kwargs)


    @property
    def ones(self):
        return self._by_value(1)

    @property
    def twos(self):
        return self._by_value(2)

    @property
    def threes(self):
        return self._by_value(3)

    @property
    def fours(self):
        return self._by_value(4)

    @property
    def fives(self):
        return self._by_value(5)

    @property
    def sixes(self):
        return self._by_value(6)


    @property
    def total_dict(self):
        return {
            1: len(self.ones),
            2: len(self.twos),
            3: len(self.threes),
            4: len(self.fours),
            5: len(self.fives),
            6: len(self.sixes)
        }

Another thing that confuses me is how does python know that we want the dices to be sorted by the value even tho we didn't provide that information? I mean in

for _ in range(size):
            self.append(die_class())
        self.sort()

we're sorting an instance which is a list consisting non-numeric objects but with value attributes. How python knows that we want these objects to be sorted by value attribute?

1 Answer

Chris Freeman
MOD
Chris Freeman
Treehouse Moderator 68,468 Points

Good questions!

Why are we putting super().__init__() in the Hand class? I know that it creates an empty list so we can add stuff to that, but isn't the Hand list enough?

To clarify, neither __init__, or the super().__init__ creates the instance. The instance is created by the __new__() method then Hand.__init__() is called and then, if present, super().__init__ is called. The list.__init__() method does not appear to add anything to the instance. All of the list methods are added to the Hand instance in the __new__() method.

All of the self references within the Hand.__init__() method refer to the self parameter of the Hand.__init__() method. This is assigned a value when list.__new__() creates the instance then calls self.__init__(), which is the has been overridden to be Hand.__init__().

Also what confuses me is self after super().__init__(). To which instance (am I calling it right? [yes!] ) this self refers to? The new list created in super().__init__() or the Hand instance?

No instance is created in super().__init__(). All self references are to the Hand instance.

Another thing that confuses me is how does python know that we want the dices to be sorted by the value even tho we didn't provide that information?

The list sort method:

This method sorts the list in place, using only < comparisons between items

This means it calls the __lt__() method on each item in list. So for self.sort() to work, Die.__lt__() must be defined.

we're sorting an instance which is a list consisting non-numeric objects but with value attributes. How python knows that we want these objects to be sorted by value attribute?

For each pairwise comparison in the list of, say, item a and item b, Python blindly calls the a.__lt__(b) method. If it doesn't exist, or it can't properly compare a and b, then a TypeError: '<' not supported between instances.... is raised.

To see this in action, look at the modified code and its output:

class Die:
    def __init__(self, sides=2, value=None):
        if sides < 2:
            raise ValueError("Can't have fewer than two sides")
        self.sides = sides
        if not value:
            self.value = random.randint(1, sides)
        else:
            self.value = value
        print("in Die.__init__")
    def __int__(self):
        print("in Die.__int__")
        return self.value
    def __lt__(self, other):
        print("in Die.__lt__")
        return int(self.value) < int(other.value)
    def __add__(self, other):
        print("in Die.__add__")
        return int(self) + other
    def __radd__(self, other):
        print("in Die.__radd__")
        return self + other

class D20(Die):
    def __init__(self):
        print("in D20.__init__")
        super().__init__(sides=20)

class Hand(list):
    def __init__(self, size=0, die_class=None, *args, **kwargs):
        print("in Hand.__init__")
        #size - the number of dice to roll
        if not die_class:
            raise ValueError("You must provide a die_class")
        super().__init__() 
        for _ in range(size):
            self.append(die_class())
        self.sort()
    def _by_value(self, value):
        print("in Hand.__by_value__")
        dice = []
        for die in self:
            if die == value:
                dice.append(die)
        return dice
    def sort(self, *args, **kwargs):
        print(f"enter Hand.__sort__: {[int(x) for x in self]}")
        super().sort(*args, **kwargs)
        print(f"exit Hand.__sort__: {[int(x) for x in self]}")
>>> h5 = Hand(5, D20)
>>> h5 = Hand(5, D20)
in Hand.__init__
in D20.__init__
in Die.__init__
in D20.__init__
in Die.__init__
in D20.__init__
in Die.__init__
in D20.__init__
in Die.__init__
in D20.__init__
in Die.__init__
in Die.__int__
in Die.__int__
in Die.__int__
in Die.__int__
in Die.__int__
enter Hand.__sort__: [9, 2, 1, 15, 2]
in Die.__lt__
in Die.__lt__
in Die.__lt__
in Die.__lt__
in Die.__lt__
in Die.__lt__
in Die.__lt__
in Die.__int__
in Die.__int__
in Die.__int__
in Die.__int__
in Die.__int__
exit Hand.__sort__: [1, 2, 2, 9, 15]

Post back if you have more questions. Good luck!!

Thank you so much for as always very profesional answer! So if "The list.__init__() method does not appear to add anything to the instance. All of the list methods are added to the Hand instance in the __new__() method.", why is it there?

Another thing I think I don't have a clear view on is the flow while creating a Hand instance. As far as I'm concerned it goes like this:

  1. Hand.__new__() and that's when the instance inherits all of the list's methods and attributes (or maybe it's in the list.__new__() method??).
  2. Then it's passed into the Hand.__init__()
  3. And lastly it's passed to the super().__init__(), which could also be considered as list.__init__(), but I don't know for what reason.

(Maybe my incomprehension is caused by your few mistypes in "This is assigned a value when list.__new__() creates the instance then calls self.__init__(), which is the has been overridden to be Hand.__init__()")

Also one thing popped into my mind: in

for _ in range(size):
            self.append(die_class())

it's appending our instance with die_class(), for example with D20() and if i try to print this instance I get back for example [1, 1, 2, 4, 5], so it's a list with D20().value. How did it happen if we didn't explicitly called that we want this list to be filled with .value attributes instead of D20() instances?

Chris Freeman
Chris Freeman
Treehouse Moderator 68,468 Points

Why is super().__init__() there? As a programmer, you don't always know ahead of time if the parent's __init__() method does anything useful. Also, it's possible that the parent's __init__() may change in the future, so by adding the super(), the current file won't have to be changed if the parent changes.

There is another quirk: where are Hand.__init__ *args and **kwargs being used? Ideally, they would be included in the super().__init__(*args, **kwargs) call to pass any extra arguments to the parent classes' initialization methods.

I don't have a clear view on is the flow while creating a Hand instance. It goes like this:

  • Executing, say, Hand(5, D20)
  • Hand.__new__(5, D20) is called. Since it was not defined, the inherited version in list.__new__(5, D20) is run
  • list.__new__ creates a new instance. self now points to this instance
  • self.__init__(5, D20) is called. Since it exists in Hand, Hand.__init__(self, 5, D20) is run
  • Hand.__init__ calls super().__init__(), this is the parent's list version list.__init__
  • apparently list.__init__ doesn't do anything significant
  • when list.__init__ completes, flow returns to the super() call in Hand.__init__()
  • when Hand.__init__ completes, it returns None by default!. Flow returns to list.__new__
  • It is list.__new__ that returns the instance to the Hand(5, 20) call

Remember, just because a method comes from a parent class, it is executed as if it is in the current class. In other words, it's as if list.__new__ had been copied into Hand as Hand.__new__

I corrected the paramgraph below in my answer above:

All of the self references within the Hand.__init__() method refer to the self parameter of the Hand.__init__() method. The self parameter is assigned a value when list.__new__() creates the instance then calls self.__init__(self), which is the same as the method overridden by Hand.__init__().

When appending our Hand instance with die_class(), for example with D20(). How is the D20 value output when running print(hand_instance)? The value printed depends on whether there is a __str__ method present. If no __str__ method present, then __repr__ is run. If neither, than the location of the object in memory is printed. If class Die is updated to include the __repr__ method below, then instead of printing the memory location of the D20 object, it's value is printed.

class Die:
    def __repr__(self):
        return str(self.value)