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 Object-Oriented Python Dice Roller Comparing and Combining Dice

Why do we need magic methods here?

I don't understand why we need magic methods here. Maybe I don't understand when or why we need magic methods to begin with. Why do they have to be defined inside of the class and what is this value "other"? Is it a special/reserved word? I don't understand to what "other" is referring in our def arguments.

6 Answers

Chris Freeman
MOD
Chris Freeman
Treehouse Moderator 68,457 Points

The class D6 inherits from class Die which, by default, inherits from the built-in class object. This base object class which doesn't have a robust sense of "value" or comparison. Without any overridden magic methods, it would compare on the object ID value, which would almost certainly be False. The magic methods are called to resolve how to treat the object when an operator is present (+, /, *, ==, !=, <=, >=, etc.).

Given thing1 operator thing2, the "operator" magic method of thing1 is called to resolve the expression. thing2 would be "other" in this case. if this operator magic method isn't found then *reverse" version of the method (for example, 'radd" instead of "add") of thing2 is called to resolve the expression. Here, thing1 would be the "other".

Josh Keenan
Josh Keenan
20,315 Points

The man, the myth, the legend.

Thank you. So, do operands behave differently within classes than outside of them and therefore need fine tuning?

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

The operators are the same everywhere, in that they trigger the same magic methods. For example, plus will always trigger the __add__ method. It's the magic method behavior that differs between classes.

Edit: meant to say operator instead of operand.

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

As for operands thing1 and thing2, they behave as their class magic methods are defined regardless of where they're being being used whether inside their own class or not.

All built-in types (int, list, string, set, dict, etc.) all have their magic methods predefined making expressions like 10 + 15 work everywhere.

Most user-defined classes, like Django models and class views, etc, aren't typically used in math expressions. That's why creating magic methods for classes is a fairly rare task. It's when users create a class that will be used in math expressions that the magic methods will need to be defined.

Sometimes creating the required magic methods is trivial by subclassing an built-in class which inherits all of the parent class methods. For example, Die, the parent class to D6 could have subclassed the built-in int class to get all of the int magic methods, but then the lesson on creating the magic methods from scratch would have been missed.

If not subclassing when creating a new class that will be used with operators, then all of the magic methods need to explicitly created.

Let's walk through an example of parsing an addition expression:

# thing1 is instance of Die class
# thing2 is instance of Die class
thing1 + thing2
# looks at operand thing1 first
thing1.__add__(thing2)  # self=thing1, other=thing2
int(thing1) + thing2
thing1.__int__() + thing2  # self=thing1
thing1.value + thing2  # the value is an int ("int1")
int1 + thing2  # int is built-in type implemented in C
# since int doesn't know how to add itself to a thing2 it calls: 
thing2.__radd__(int1)  # self=thing2, other=int1
# which calls
thing2.__add__(int1)
int(thing2) + int1
thing2.__int__() + int1  # self=thing2
thing2.value + int1  # value is an int ("int2")
int2 + int1
# the built-in int class knows how to add itself to another int 
sum_of_int1_int2 # int1 + int2
Natalie Tan
Natalie Tan
25,519 Points

rather than go through setting up all these magic methods, could we not just have used 'd6.value' ?

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

Natalie Tan, using d.value where d = D6(), would depend on what you plan to do. Without the magic methods you would be limited to always using including the attribute name in your code d.value, or if __int__() was defined, calling int(d) to give you the numeric value for that object.

The purpose of the magic methods is to allow using the simple object reference d in whatever context you wish and have the object know how to behave in that context, whether it be adding, or in comparisons to other objects.

Please post back if you need more details!

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

Creating D6 and Die by sub-classing the int class is more complicated than a simple inheritance due to int being an immutable class. Sub-classing immutable classes is covered later in the coursework. Below is one way to sub-class int. Note the print statements are only to show which methods are run.

dice_from_int_class.py
import random


class Die(int):
    def __new__(cls, sides=2, value=0):
        print("running Die.__new__")
        if not sides >= 2:
            raise ValueError("Must have at least 2 sides")
        if not isinstance(sides, int):
            raise ValueError("Sides must be a whole number")
        return int.__new__(cls, int(value or random.randint(1, sides)))

    def __init__(self, sides=2, value=0):
        print("running Die.__init__")
        super().__init__()

class D6(Die):
    def __new__(cls, value=0):
        print("running D6.__new__")
        return super().__new__(cls, sides=6, value=value)

    def __init__(self, value=0):
        print("running D6.__init__")
        super().__init__(sides=6, value=value)
$ python
Python 3.6.3 (default, Oct  3 2017, 21:45:48) 
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dice_int import D6, Die
>>> d1 = D6()
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
>>> d1
5
# make a list of 6 instances of D6
>>> die = [D6() for _ in range(6)]
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
running D6.__new__
running Die.__new__
running D6.__init__
running Die.__init__
>>> die
[6, 2, 6, 4, 3, 4]
>>> type(die[0])
<class 'dice_int.D6'>
# compare instances using the inherited methods from int
>>> die[0] >= die[2]
True
>>> die[0] > die[2]
False
Josh Keenan
Josh Keenan
20,315 Points

Other is an argument being passed in as a comparison with the actual value of the Di and is not a reserved word, anything could have been used.

If operands are the same everywhere, how come their behavior has to be defined by a magic method within a class? That is the part I don't really understand. Like, with __radd__ helping to make addition work with items on either side of the + operator, but outside of a class, I can get "25" when I add 10+15 or 15+10.

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

I misread your previous comment as "operator" instead of "operand". I've updated my comment above and add new comment on operators

Iulia Maria Lungu
Iulia Maria Lungu
17,357 Points

Chris Freeman , wouldn't this have been simpler if the class would have extended int class ? The operations would have been already defined, right? What am I missing? Or is re-defining the __magic_methods__ the whole point of the video?

Thinking twice what I said would not work because when extending not mutable types like int we'd have to use the __new__ method instead of__init__, but I have a feeling that somehow one can make use of the int class methods here instead of writing them down by hand. Is this doable?

Update: Just seen this answer of yours from above:

Sometimes creating the required magic methods is trivial by subclassing an built-in class which inherits all of the parent class methods. For example, Die, the parent class to D6 could have subclassed the built-in int class to get all of the int magic methods, but then the lesson on creating the magic methods from scratch would have been missed. How would this look like?

I tried to do it but did not manage to make it work

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

Great question Iulia Maria Lungu! It is certainly possible to build the Die and D6 class by inheriting from int by it's a more advanced concept. The difficulty lies in int is an immutable class type. In addition to defining the __init__ method, the __new__ method also needs to be defined. The __new__ method creates the class instance, and the __init__ method initializes it. For mutable classes, the default __new__ method inherited from the base class object is usually sufficient.

I'll add a comment to my answer above showing how you can create Die and D6 by subclassing int.

I'm curious, and not quite sure how to work out the answer. Is it more efficient, in BigO time/space complexity to subclass init, or to list out the magic methods? It seems like there are many repetitive calls in the subclassing example above, but maybe there are just as many the other way? How do the memory requirements compare, to pull in an entire class, vs bringing in individual pieces. I could really see that going either way. Which is a more stable build option? Does subclassing the built in give a more stable base, or does it go the opposite direction, and give more opportunity for unexpected answers?

It obviously doesn't matter for this project, but I'm thinking about how this might be applied elsewhere.

Chris Freeman
Chris Freeman
Treehouse Moderator 68,457 Points

The complexities of including math and comparison operators is independent of where the magic method resides. The repetitive calls are part of the structure of python. The built in types int, float, str, etc., are all optimized for performance when using operators. Most class definition is for structured data and rarely needs math support. Also, since __init__ is only run once per class instantiation there is little benefit in trying to optimize it.

Listing out the magic methods to override parent methods with the methods of the same functionality would be confusingly redundant. If a method is not present, it is looked for in the namespace of the first parent. So it’s a one-look-up delay.

Pulling in pieces or “entire classes” really only involves bring in a reference to a name space dictionary. So space is a non-issue. Use it if you need to augment a parent method.

Adding methods that aren’t explicitly needed isn’t DRY (don’t repeat yourself) and could provide a path to introduce errors. If a local Methodist needed, a complicated method will often refer to the parent’s method using super() to do most of the work and then tweak the result locally.

Post back if you need more help. Good luck!!!

Thanks, Chris Freeman.

All of that is helpful and makes sense. I appreciate your gracious answers!