Python: list of objects vs list of integers behavior - python

I'm new to the language and I am a bit confused about references in Python.
Consider this code:
class A:
def __init__(self, x):
self.x = x
a = A(3)
v=[a]
print(f'obj before: v[0].x={v[0].x}')
a.x = a.x + 1
print(f'obj after: v[0].x={v[0].x}')
b = 3
w=[b]
print(f'int before: w[0]={w[0]}')
b = b + 1
print(f'int after: w[0]={w[0]}')
=====================
output:
obj before: v[0].x=3
obj after: v[0].x=4
int before: w[0]=3
int after: w[0]=3
Why do the obj and int versions of the code work differently?

a = A(3)
The variable a points to an object.
v=[a]
The first element of v points to the same object.
a.x = a.x + 1
Change the attribute "x" of the object.
v still contains the same object but its attribute has changed.
b = 3
The variable b points to the object 3.
w=[b]
The first element of w also points to the object 3.
b = b + 1
b now points to what you get when you perform addition on the object 3 and the object 1, which is the object 4.
w still contains the object 3. You never changed any attributes of this object and you never changed where the first element of w points to.

When you do this, you are modifying the object a:
a.x = a.x + 1
When you are doing this, you are changing what variable b refers to:
b = b + 1
In other words, there is a big difference between b and x in the above code: b is a variable and x is an attribute of a.
Assigning something to a variable does not modify any objects, and therefore affects only the variable to which the assignment was made*, whereas setting the value of an attribute modifies the object, which can be seen in any variable which references that object.
* There are also changes in refcounts affecting garbage collector, but is not relevant now.

Related

Why is numeric type deleted and list type not, when they are attributes to a class instance (which gets deleted)?

In the below code sample I would expect both a and b to be deleted at the end of each loop.
What happens is that a is deleted but not b. Why is that? And how can I make sure that b is also deleted at the end of each loop?
class bag:
a = 5
b = []
def funcie(self):
self.a = self.a + 1
self.b.append(1)
for i in range(5):
inst = bag()
inst.funcie()
print(inst.a)
print(inst.b)
# del inst
output:
6
[1]
6
[1, 1]
6
[1, 1, 1]
6
[1, 1, 1, 1]
6
[1, 1, 1, 1, 1]
EDIT: So this post explains why the b list keeps growing in each loop i.e. I should have declared the b list in the __init(self)__ function.
However it doesn't explain why the a variable gets overwritten at the end of each loop whilst the b variable doesn't.
bag.a (a class attribute) is not being overridden, it's being shadowed by the instance's a (an instance attribute) for inst specifically.
Python's general rule is that reading will read from outer/shadowed scopes if there is no inner/shadowing scope hiding it. An inner/shadowing scope is created by assignment, not merely mutation (which is just reading from the variable then asking it to mutate itself). There are some subtle distinctions between how scopes and attributes work, so I'm going to focus on attributes only (since that's what you asked about).
When you do self.a = self.a + 1 on a new instance of bag, the self.a you read comes from the class attribute, but on writing to self.a, you create a new shadowing instance attribute. The class attribute (bag.a == 5) still exists, and is unmodified, but from the instance's point of view, it only sees the instance attribute (inst.a == 6). If you added print(bag.a), you'd see if never changed.
By contrast, self.b.append(1) reads the class attribute and asks it to modify itself in place, which changes object bound to the class attribute itself.
If you want to modify a class attribute without creating an instance attribute, you have to operate on the class itself. For example, you could change:
self.a = self.a + 1
to:
type(self).a = self.a + 1 # Or type(self).a = type(self).a + 1
or more simply use += to avoid repeating more complex stuff:
type(self).a += 1
Numbers differ from the list in that Python uses their value and stores the result of computations in a new place. So the result of a = a+1 is stored in a different location in memory each time. Lists remain in the same memory location and are updated in place. The following code:
class bag:
a = 5
b = []
def funcie(self):
self.a += 1
self.b.append(1)
inst = bag()
print("bag memory ids: ", id(bag.a), id(bag.b))
print("inst memory ids: ", id(inst.a), id(inst.b))
inst.funcie()
print("ids after 1x funcie:", id(inst.a), id(inst.b))
inst.funcie()
print("ids after 2x funcie:", id(inst.a), id(inst.b))
has this output, where you see the id of inst.a changing, while inst.b retains the same id:
bag memory ids: 9789120 139707042123328
inst memory ids: 9789120 139707042123328
ids after 1x funcie: 9789152 139707042123328
ids after 2x funcie: 9789184 139707042123328
So the updated value of a is stored in a different location, without altering the original value.

Why are only mutable variables accessible in nested functions?

here is a simple (useless) function:
def f(x):
b = [x]
def g(a):
b[0] -= 1
return a - b[0]
return g
it works fine. let's change it a tiny bit:
def f(x):
b = x
def g(a):
b -= 1
return a - b
return g
Now it gives an error saying that b is undefined! Sure, it can be solved using nonlocal, but I'd like to know why does this happen in the first place? Why are mutables accessable and immutables aren't?
Technically, it has nothing to do with mutable or immutable (the language does not know whether a type is "mutable" or not). The difference here is because you are assigning to the variable in one case and just reading from it in the other case.
In your second example, the line b -= 1 is the same as b = b - 1. The fact you are assigning to the variable b makes a local variable named b, separate from the outside variable named b. Since the local variable b has not been assigned to when evaluating the right side of the assignment, reading from the local b there is an error.
In your first example, the line b[0] -= 1 is the same as b[0] = b[0] - 1. But that is not a variable assignment. That is just a list element access. It is just syntactic sugar for b.__setitem__(0, b.__getitem__(0) - 1). You are not assigning to the variable b here -- all you're doing is reading from the variable b two times (in order to call methods on the object it points to). Since you are only reading from b, it uses the external variable b.
If in the first example, with your mutable list, you did a variable assignment like you did in the second example, it would equally create a local variable, and the reading of that local variable before assignment would also not work:
def f(x):
b = [x]
def g(a):
b = [b[0] - 1]
return a - b[0]
return g

Python OOP instances & classes mutability

I have been doing some readings and thought about this code:
def change(c, n: int) -> None:
c.x = n
class Value:
x = 5
m = Value()
change(Value, 3)
print(m.x)
change(m, 1)
change(Value, 2)
print(m.x)
The output of this code is:
3
1
So what I assumed is for the 3, m & Value are aliased but changing m's attribute breaks this. I couldn't confirm this by running id() - it turned out m and value always had different ids.
Can someone explain what's going on?
When you are changing the value for Value you are changing the x value shared by all the value instances.
When you are changing the value for m, you are doing it for m and m alone, essentially overriding the class x with a new instance x. You can see it with
k = Value()
print(k.x) # 2

Python: Why is scope of variable referencing a List is different than a variable referencing any other data structure or data type?

I have found that the scope of a variable referencing a List is different than the variable referencing a Tuple or Integer or String. Why does it happen ?
1) When I am using an Integer, String or Tuple:-
>>> def foo(anInt, aStr, aTup):
anInt += 5
aStr += ' hi'
aTup += (12,)
print (anInt,aStr,aTup)
>>> anInt, aStr, aTup = 5, 'Fox', (11,)
>>> foo(anInt, aStr, aTup)
10 Fox hi (11, 12)
>>> print (anInt, aStr, aTup)
5 Fox (11,)
2) When i am using an List:-
>>> def foo(aList):
aList.append(2)
print (aList)
>>> aList = [1]
>>> foo(aList)
[1, 2]
>>> print (aList)
[1, 2]
In the first case changes in the values of anInt, aStr, aTup is limited to the scope of function while in case of a List scope changes.
'
It is not a question of scope. The scope does not differ between types. It can not differ, in fact: a variable is just a named reference to a value, completely type-agnostic.
But some types in Python are mutable (once created, a value can be changed), and list is one of them. Some are immutable, and changing a value of these types requires creating a new object. += works in two different ways for mutable and immutable types. For immutable types a += b is equivalent to a = a + b. A new object is created and the a variable now refers to it. But the objects of mutable types are modified "in place", quite as your list. You may want to read this.
Now let's have a look at scopes. Here you have global objects that are passed to a function. The function uses them as its parameters, not as global objects (yes, aList inside the function is not the same variable as aList outside it, here is more information on scopes). It uses other references to the same objects, and it can not modify the variables, it can just modify objects the variables refer to. But only the mutable objects.
You may be surprised if you compare the results of two following code samples.
>>> a = 1; a1 = a; a1 += 1; a
1
>>> a = [1]; a1 = a; a1 += [1]; a
[1, 1]
What is the difference? The only difference is that int is an immutable type, and the list is a mutable one. After assigning a to a1 they always refer a single object. But the += operator creates a new object in case of int.
Here is a good example of what looks like a difference in scope, when using a variable referring to an int, vs a list.
Note the 'prev' variable:
1 class Solution:
2 def convertBST(self, root: TreeNode) -> TreeNode:
3
4 if root == None:
5 return root
6
7 prev = 0
8 def traverse(node):
9 if node.right:
10 traverse(node.right)
11 node.val += prev
12 prev = node.val
13 if node.left:
14 traverse(node.left)
15
16 traverse(root)
17 return root
This errors with the following message:
UnboundLocalError: local variable 'prev' referenced before assignment
node.val += prev
There is no error, if instead I replaced these lines with this code:
line 7: prev = [0]
line 11: node.val += prev[0]
line 12: prev[0]= node.val
making one believe that prev = 0 was invisible within the traverse function, while prev = [0] is visible!
But in fact, prev = 0 is also visible and can be used within the traverse function, if no assignment to it takes place i.e. UnboundLocalError occurs on line 11, only if line 12 is present.
Because of the immutability of an int, line 12 causes the variable prev within the traverse function to point to a new memory location, and this then "hides" the variable prev = 0 defined outside, causing the error.
But when a new int is assigned to prev[0], an element in a mutable list, the pointer for the first element of the list can be updated to point to the new int (both inside and outside the traverse function); no separate scope (ie local variable) is created, the original prev variable remains visible within the traverse function, and prev[0] can be used on line 11 before the assignment on line 12.
The difference is that the immutable types int, string and tuple are passed by value. When the function updates the value, it is updating a local copy.
The list is passed by reference. When you use append, it is updating the original list.

Python reference model

I have a hard time to understand the python reference model
def changer(a,b):
a = 2
b[0] = 'spam'
X = 1
L = [1,2]
changer(X,L)
X,L
(1,['spam',2])
here comes my question, for assignment b[0] = 'spam' : I want to know how python modify the mutable object in this way(instead of create a new string objects and link the variable b to that object which will not affect the original object pointed by L)
thanks
a and b are both references to objects.
When you reassign a you change which object a is referencing.
When you reassign b[0] you are reassigning another reference contained within b. b itself still references the same list object that it did originally, which is also the list that was passed into the changer function.
Variables name are pointers to a special memory address ,so when you pass L and X to function the function does not create a new address with a,b just changed the labels !, so any changes on those actually change the value of that part of memory that X,L point to. So for refuse that you can use copy module :
>>> from copy import copy
>>> def changer(a,b):
... i = copy(a)
... j = copy(b)
... i = 2
... j[0] = 'spam'
...
>>> X = 1
>>> L = [1,2]
>>> changer(X,L)
>>> X,L
(1, [1, 2])
In Python, lists are mutable, and integers are immutable. This means that Python will never actually change the value of an integer stored in memory, it creates a new integer and points the variable at the new one.
The reason for this is to make Python's dynamic typing work. Unlike most languages, you can create a variable and store an int in it, then immediately store a string in it, or a float, etc.
MyVariable = 10 # This creates an integer and points MyVariable at it.
MyVariable = "hi" # Created a new string and MyVariable now points to that.
MyVariable = 30 # Created a new integer, and updated the pointer
So this is what happens in your code:
MyVar = 1 # An integer is created and MyVar points to it.
def Increase(num):
num = num + 1 #A new integer is created, the temp variable num points at it.
Increase(MyVar)
print(MyVar) # MyVar still points to the original integer
This is a 'feature' of dynamically typed languages ;)

Categories