I understand the classic example when a list gets changed, and the other variable that points to it also “changes”.
But why does it happen also when change occurs inside a function? Shouldn’t a function have a separate scope that prevents this?
# classic example
x = ['a','b','c']
y = x
print(y) # ['a', 'b', 'c', 'D']
# shouldn't a function have a separate scope?
x = ['a','b','c']
print(x) # ['a', 'b', 'c', 'D']
There is one list object [‘a’, ‘b’, ‘c’] , that’s referred to by three different names: arr1 , arr2, and mlist. Calling myfunc() doesn’t copy the list object, just makes the local name refer to the same object.
Wouldn’t you want to retain the option of having the function work on the original object? Then you have the choice of having it do that, or you can have the function create a copy of the original and work on the copy instead. It’s much more flexible as it is now.
If you want that, you can do so easily with the list.copy() method:
>>> a = [1, 2, 3]
>>> b = a.copy()
>>> assert a == b and a is not b
a == b verifies that the content is the same, a is not b that they are different objects. You can do that
at call by myfunc(x.copy()) if the calling code wants to preserve x
or within myfunc() by mlist = mlist.copy() if you want myfunc() to behave as a ‘pure’ function (no side effects)
But (and one of the reasons this isn’t the default behaviour), that copy() takes time. Not noticeably so for our toy examples, but imagine a million-element list. That performance impact, and the ability to have ‘impure functions’ with side effects as @Quercus mentioned, make it preferable that the developer explicitly copies the data.
Variables in Python don’t “point to” objects (i.e., you don’t have to write any code to “follow” the pointer, and you can’t “dereference a null pointer” - that’s where NameError and UnboundLocalError come from - and you can’t “dereference an invalid pointer” because they can’t be invalid in the first place). Instead, they name objects.
The function’s name for the object that was passed is a separate name for that object, even if it’s spelled the same way. However, it’s still naming the same object.
Assignment in Python doesn’t copy values. = is an assignment. So is passing an argument to a function. So are:
“augmented” assignments like +=
the walrus operator (:=), which works as an operator in an expression but still binds a name
setting the iteration variable for a for or while loop, generator expression or dict/set/list comprehension
any use of the as keyword: an explicitly renamed import, giving a name to an exception caught in an except block, or giving a name to a resource used in a with block
other uses of import
creating a function with def
creating a class with class
No; this denies you the option not to copy, which is essential in many cases (for example, built-in list methods like .append). Python doesn’t have to support separate pass-by-value and pass-by-reference schemes, while a programmer who needs a copy writes code that makes a copy, rather than relying on a rule that says a copy is made in certain circumstances. This is in accordance with the Zen: “implicit is better than explicit”, and “simple is better than complex”.