Python Object-Oriented Python Advanced Objects Multiplication

Serdar Halac
Serdar Halac
6,166 Points

Why does this code re-set the self.value attribute from a string to an int?

So I answered the question fairly easily, but I still don't understand why the code was coded as such:

def iadd(self, other): self.value = self + other return self.value

when in init : def init(self, value): self.value = str(value)

It's clear that init sets the value attribute to a STRING (since this whole class is supposed to be a string representation of a number). But the iadd method calls the add method, which returns an int (return int(self) + other) or float, so that means that in turn the iadd method sets self.value, which as as I just pointed out was initialized to a STRING, to the result of the add function which is a FLOAT/INT. Doesn't that completely change the class' purpose entirely?

It gets weirder because I ran some of this code on sublime text and the Mac terminal and did:

numTest = NumString(8)

int(type(numTest.value))

print(type(numTest))

print(numTest)

which yielded:

<class 'str'>

<class '__main__.NumString'>

8

Which makes sense. Right? the value attribute is a class string, and the instance itself is an instance object of the NumString class.

But then continuing the code above and doing:

numTest += 7

print(numTest)

print(type(numTest.value))

It yields:

15

Traceback (most recent call last):

File "treetest5.py", line 44, in <module>

print(type(numTest.value))

AttributeError: 'int' object has no attribute 'value'

So ok, it sets numTest to numTest + 7 which is equal to 15, so thats what the new value attribute is. But then asking for the type of the value attribute, it no longer says class string! It now just says int object has no attribute value!

So it seems that the code provided by TreeHouse DOES change the class' purpose entirely, as far as I know (changing the value from a string representation of an int to a straight up int), but I don't know why. I also have no idea why print(type(numTest.value)) didn't just return <class 'int'>, and I really want to know!

So please help out, because I can pass the assignment and did, but have no clue why that's the way the Iadd and imul methods are supposed to be done. As far as I know they break the purpose of the class itself and such code would only be suitable for a class where the value itself is not supposed to be a string but an int.

Programming wisdom would be greatly appreciated.

numstring.py
class NumString:
    def __init__(self, value):
        self.value = str(value)

    def __str__(self):
         return self.value

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

    def __float__(self):
        return float(self.value)

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

    def __radd__(self, other):
        return self + other

    def __iadd__(self, other):
        self.value = self + other
        return self.value

    def __mul__(self, other):
        if '.' in self.value:
            return float(self) * other
        return int(self) * other

    def __rmul__(self, other):
        return self * other

    def __imul__(self, other):
        self.value = self*other
        return self.value

2 Answers

Chris Howell
MOD
Chris Howell
Python Web Development Treehouse Moderator 40,175 Points

Hi Serdar Halac

So I do see where you are confused here. It does look like there is in-fact a side effect with the code in this section. I think the overall take away Kenneth wanted you to have was how to use the the magic methods and what they went to.

But if you are curious of how to improve upon this class the problems are with the return of each of the magic method methods.

A few examples, we will only look at addition:

# Lets make NumString of 8.
ns1 = NumString(8)

Now we know, anytime we do ANYTHING with this class. At the end of it, we would like to keep our NumString class type. We dont want to lose it to another class type like int or str.

So lets start out with our print of ns1 from above.

# Print the type of ns1
print(type(ns1))

We should get...

>>> <class 'numstring.NumString'>

Because we haven't done anything except create the class.

Now we will perform some math and check our type again.

# Now lets do some math which should give us the issue.
ns1 += 8
# And re-print
print(type(ns1))

We should get...

>>> <class 'int'>

This seems confusing because its not what we expected back. But it is in-fact what we told the code to do if you look a bit closer.

When we called the += we really called the __iadd__ method on our class.

So lets go look at what is happening.

numstring.py

class NumString:

    # ... other methods ...

    def __init__(self, value):
        self.value = str(value)

    def __iadd__(self, other):
        self.value = self + other
        return self.value

When we say ns1 += 8, it is shorthand for: ns1 = ns1 + 8.

We are setting the variable ns1 to whatever is returned by the right side of the equals sign. Inside the __iadd__ method we can see that it is returning self.value which has become an int type before we returned it.

So to fix the behavior of our NumString class, we have to change the return statements to our magic methods that affect this behavior to either return self which would be considered mutable or a completely new instance of NumString which would be immutable. (Thanks Kenneth Love for your input!)

Your refactored __iadd__ method would look like this then:

class NumString:
    # ... other methods ...

    def __iadd__(self, other):
        self.value = self + other
        return NumString(self.value)

Try making that one change, then testing the ns1 += 8 and see what your type is.

Hope that helps, if not let me know.

Serdar Halac
Serdar Halac
6,166 Points

Thank you so much for your detailed and much better formatted answer! Really helps. And it seems to answer what the alternative would be to make it work as intended. Thanks.

One quick comment though, would recreating a new instance each time we increment the value property mean that the code would eventually lead to a lot of memory being used up by the program at once as it runs? Assuming it increments the variable a number of times? If for example we add a loop where the self.value gets added to itself, say, a million times, would that not create a million instances? Leading to memory leak issues? I know Python has automatic garbage disposal, so to speak, but it seems like those instances would keep existing until the code ends. Also when you say: "either return self which would be considered mutable or a completely new instance of NumString which would be immutable"

What is meant by this? Why would self and a new instance of NumString be different in terms of the mutability, since they're the same type of class instance? And why does returning a new instance imply immutability and self imply mutability?

Pardon my ignorance! Thank you for taking the time to answer.

Chris Howell
Chris Howell
Python Web Development Treehouse Moderator 40,175 Points

In Python there is something called Garbage Collection, it goes quite more in-depth than this.

But to simply answer your question, once you have 0 references to an object, it will get garbage collected. So Python will automatically take care of free'ing that memory back up for you. But if for whatever reason you were remembering every reference to every newly created instance. Yes, you would eventually run out of memory. But even at that, it would take you awhile to do that today, memory isnt a huge issue today. It was in the early days!

As for the Mutable and Immutable The best way I can define it would be: Each instance of an object is holding its own "state" right. A mutable object can change that state and it will still remain the SAME object (by same object, I mean its the exact same object existing in the same spot in memory). However, an Immutable object is one that exists in memory and if its changed it will become a new address in memory. Even though its based off the old state, its a completely new object with new data.

In Python there are data types like this:

Lists, Sets, Dicts are mutable types

int, float, string, tuples are immutable types.

Chris Howell
Chris Howell
Python Web Development Treehouse Moderator 40,175 Points

So if we change just two methods on NumString you can check what happens.

NOTE: I am using Python 3

class NumString:
    # .. other methods .. 

    def __iadd__(self, other):
        # Returns current instance, self, considered(mutable)
        self.value = self + other
        return self

    def __imul__(self, other):
        # Returns entirely new instance, considered(immutable)
        self.value = self * other
        return NumString(self.value)

then you can run this after you change these two.

ns1 = NumString(8)
print('NS1 ID: ', id(ns1))
ns1 += 4 # uses __iadd__ method
print('NS1 ID: ', id(ns1)) # should be same as above

print('=' * 20) # separate 

ns2 = NumString(8)
print('NS2 ID: ', id(ns2))
ns2 *= 4  # uses __imul__ method
print('NS2 ID: ', id(ns2)) # should be different id
Serdar Halac
Serdar Halac
6,166 Points

Thank you so much. Really appreciate the effort to answer my question :) Clears it up.