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 Advanced Objects Math

James Estrada
seal-mask
.a{fill-rule:evenodd;}techdegree
James Estrada
Full Stack JavaScript Techdegree Student 25,866 Points

Does Python try to convert any instance to an, e.g., int or float when doing math?

When we modified add to be:

def __add__(self, other):
        if '.' in self.value:
            return float(self) + other
        return int(self) + other

Is python trying to convert NumString (self) to a float or int? I get the same result if I get self's value:

def __add__(self, other):
        if '.' in self.value:
            return float(self.value) + other
        return int(self.value) + other

Here python would be converting a string (self.value) to a float or int right? Which one is the best approach?

Dave StSomeWhere
Dave StSomeWhere
19,870 Points

Please show the class declaration.

1 Answer

Cooper Runstein
Cooper Runstein
11,850 Points

The dunder methods float and str are called when you do str(self) or float(self), and you've set them to point to self.value. I would use the first method because add will work no matter what you make str and float point to. If for some reason you decided to make the value of your class be self.value + something else, your add function will use a different value than if you were to simply check for the int value of your class, and that could create a bug that would be hard to solve.

James Estrada
seal-mask
.a{fill-rule:evenodd;}techdegree
James Estrada
Full Stack JavaScript Techdegree Student 25,866 Points

Could you give me an example where

If for some reason you decided to make the value of your class be self.value + something else, your add function will use a different value than if you were to simply check for the int value of your class, and that could create a bug that would be hard to solve.

would be a problem?

Cooper Runstein
Cooper Runstein
11,850 Points

Sure, say you're creating some sort of sales app, and you create a class to represent a product you're selling:

class Product:
    def __init__(self, value, **kwargs):
        self.value = value
        for key, value in kwargs.items():
            setattr(self, key, value)

    def __int__(self):
        return int(self.value)

    def __add__(self, other):
        if '.' in self.value:
            return float(self.value) + other
        return int(self.value) + other

#I can easily look up the cost of an object 
car = Product('50000')
int(car)
#50000

#and I can add extras to the car:
fancy_paint = 500
car + fancy_paint # 50500

I'm only going to work with int for now, but float will function the same way for this.

Lets say that this functionality works great, but then we're told that we need the cost of our object to reflect the price of the item times the sales tax, but only if the place we're selling it in actually has a sales tax. So we use the **kwargs to pass in a tax, and alter the int method so when someone goes to look up the price they get the total cost of the item :

class Product:
    def __init__(self, value, **kwargs):
        self.value = value
        for key, value in kwargs.items():
            setattr(self, key, value)

    def __int__(self):
        try:
            return int(self.value * self.tax)
        except AttributeError:
            return int(self.value)

    def __add__(self, other):
        if '.' in self.value:
            return float(self.value) + other
        return int(self.value) + other

This works great for looking up items, for instance:

# with a 10% sales tax
car = Product('50000', tax=1.1)
int(car) #55000

#with no sales tax
car = Product('50000')
int(car) #50000

But, the problem arises if we keep the add method the same, say a customer asks for the cost of a car in a place with 10% sales tax, as above, we tell them it's $55000. And then they ask how much fancy paint is and we tell them that's $500 (Pretend that has no sales tax for simplicity). When we punch it in we get a problem:

car = Product('50000', tax=1.1)
fancy_paint = 500.

car + fancy_paint #50500

We should have got back 55500, but because the add method was relying on self.value, not the returned number, we ended up with a different number on paper than from our program.

Now there are two possibilities, we could put the try/except block into the init, or we could go through and add a try/except block to add, the second one is probably not a good idea, because there are other dunder methods that we'd need to change as well.

The first option is a good one in this scenario, but not if it gets more complex, for instance what if we need to create a class that gets the sales tax free cost of the car so we can reduce the car when we put it on sale by a percentage of the non-taxed value? I don't want int or str to return this non-tax value, so I can't store self.value as the argument value * the argument tax, i'd need to create a taxed_value, and a non_taxed value, and that could get really messy the bigger the project.

def __init__(self, value, **kwargs):
    self.base_value = value
        for key, value in kwargs.items():
            setattr(self, key, value)
    try:
        self.taxed_value = self.base_value * self.tax
    except AttributeError:
        self.taxed_value = self.base_value

def returnNotTaxed(self)
    return self.base_value

This also gets much harder to troubleshoot, in a huge class, what if one single dunder method calls self.base_value instead of self.taxed_value?

Calling int or float on self is a good practice, because it will make sure the values externally match those that the dunder methods use. Even if int(Product) returns the wrong value, all the dunder methods will have the same bug, and when you find the issue, it can be resolved in one place rather than in every single dunder.

James Estrada
seal-mask
.a{fill-rule:evenodd;}techdegree
James Estrada
Full Stack JavaScript Techdegree Student 25,866 Points

Incredible explanation Cooper Runstein! I ran your code and I understood why I should use self instead of self.value.

I had to modify this in order to run correctly on my side:

def __int__(self):
        try:
            return int(int(self.value) * self.tax)
        except AttributeError:
            return int(self.value)

Thank you very much for taking your time to clarify to a stranger an issue that many have been wondering, but nobody has explained it as good as you!

Great example. And thank you for all of the details. I played with all of the code that Cooper provided and it all works like described. But, when said

but because the add method was relying on self.value, not the returned number, we ended up with a different number on paper than from our program.

if we take self.value and run it through the int() method why doesnt it then take our 50,000 value and multiply it by tax just like its supposed to in the int method and then return the 55,000?

Cooper Runstein
Cooper Runstein
11,850 Points

Tyler -- because self.value is just a property, it's just a special variable attached to an object in the type string. The int method has some default functionality when used on strings, for instance:

my_var = '50.5'
int(50.5) #50

The int method looks at the string, goes "okay, this is a string, I need to turn this into an integer". Simple enough, you probably knew that, and you probably realize there's a lot of code running under the hood to make this happen. However if I do something like:

class Product:
    def __init__(self, value):
        self.value = value
a = Product('50.5')
int(a)

Int takes in the object and gets confused because it's looking at an object that doesn't fit a type it knows how to deal with. So it'll throw a TypeError. int exists to fix that very problem, you're giving your class a way to act like a type that int understands. This you probably understand this too, so heres how it fits into your question -- if I run:

def __add__(self, other):
        if '.' in self.value:
            return float(self.value) + other 
        return int(self.value) + other

No matter what I do in the dunder int method, int is going to look at self.value and go "Ah, a string! I can handle this, here, I stripped the period and turned this into a number". In order to have int return anything other than the result of the default operation on the built in type string, it needs to see an object, and go "oh no, I can't handle this, let me send this off to the int method and let it do its thing". When you run int(self), you're triggering the int method, when you run int(self.value) you're triggering the built in functionality. In order to add a special feature, like multiplying my a tax value, you need to be triggering the int method.

Awesome, thank you Cooper. Makes complete sense. Thank you very much for the help

Cooper Runstein sorry to bother but if I may ask one more... self is referring to the instantiated object. self.value is returning the value attribute correct. So when we just use self how does python know we want the 50000 number instead of the 1.1 which was specified as the tax attribute. They're both attributes yet python immediately goes to that value of 50000 that we had defined as the value attribute. Thanks again!