0

Does python pass by reference, value, or object? It is quite confusing even after reading some posts. I still don't understand how I got the weird result. Could anyone help explain how the weird result came into being and how to achieve the correct result?

I tried to rotate a 2D list in python. when no function is invoked as shown in case 1, I get the correct result. However, when I put the code in a function, it returns a weird result, which is not what I expected. Why is the output in case 2 [[9, 6, 3], [8, 5, 2], [7, 4, 1]]? Where did it come from?

case 1: 
matrix = [[1,2,3],[4,5,6],[7,8,9]]
matrix = matrix[::-1]
for i in range(len(matrix)):
    for j in range (len(matrix[0])):
        if i<=j:
            matrix[j][i],matrix[i][j] =matrix[i][j], matrix[j][i]

print(matrix)

case 2: 
def rotate(matrix) -> None:
    print('1',matrix)
    matrix = matrix[::-1]
    print('2',matrix)
    for i in range(len(matrix)):
        for j in range (len(matrix[0])):
            if i<=j:
                matrix[j][i],matrix[i][j] =matrix[i][j], matrix[j][i]
    print('3',matrix)

matrix = [[1,2,3],[4,5,6],[7,8,9]]
rotate(matrix)
print(matrix)


case 1 output:
[[7, 4, 1], [8, 5, 2], [9, 6, 3]]

case 2 output:
1 [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
2 [[7, 8, 9], [4, 5, 6], [1, 2, 3]]
3 [[7, 4, 1], [8, 5, 2], [9, 6, 3]]
[[9, 6, 3], [8, 5, 2], [7, 4, 1]]
2
  • 1
    See nedbatchelder.com/text/names.html. Every name is a reference to a value. Commented Jul 12, 2019 at 18:39
  • The only thing that matters is whether the arg you pass in is mutable or immutable. Commented Jul 12, 2019 at 18:47

2 Answers 2

2

Python passes by reference. If you pass an object to a function, and then modify that object, the object will remain modified when the function is over. For example:

def f(a):
    a[1] = 'b'

x = [1, 2, 3]
f(x)
print(x)
# [1, 'b', 3]

The problem is that a lot of things in python don't modify the object. Strings are almost entirely immutable, for example - most operations you can do on a string, and most functions you can call on strings, return a new (modifed) string without changing the original. This includes concatenation, reversal, and slicing.

Lists are significantly more mutable than strings, and can be modified directly either by changing individual indices (e.g. my_list[1] = 'b') or with methods like .append() or .extend(). But many of the things common to iterables as a whole, don't change the list itself. This includes concatenation, reversal, and slicing.

The assignment operator = definitely doesn't modify the object on the left side; rather, it replaces it.

So the reason you're not getting the same output is this line:

matrix = matrix[::-1]

This doesn't modify the original matrix - instead, it creates a new list by slicing matrix, and assigns it back to matrix. When you run the code outside of a function, there's only one matrix reference in the namespace, and that reference gets replaced, so you never see a difference. However, the matrix referred to inside the function is a separate namespace than the one outside the function. It started by pointing to the same object, but with this line, you make that reference point to a different object. You then proceed to modify the different object, leaving the original untouched. A (hopefully helpful) annotation:

# global scope
# matrix --> some object with address 0x001
def rotate(matrix):
    # local scope
    # matrix --> 0x001  (the reference that was passed in)
    matrix = matrix[::-1]  # the slice creates a new object 0x002
    # matrix --> 0x002
    ...

When you then proceed to modify matrix, you're modifying 0x002. Which is all well and good, but those changes aren't affecting 0x001.

There are methods to get around this - an in-place reversal, for example, instead of using slicing to get a reversed version of the original list:

def reverse_in_place(lst):
    for i in range(len(lst) // 2):
        lst[i], lst[-i-1] = lst[-i-1], lst[i]

But as @Jmonsky points out in their answer, it's more conventional to return a modified value than to modify the passed-in value, with a few specific exceptions.

Sign up to request clarification or add additional context in comments.

3 Comments

Hello, it looks like very well explained. According to your explanation, should the function rotate() in case 2 leave its input matrix unchanged? Which is not the case. Could you please explain how the result [[9, 6, 3], [8, 5, 2], [7, 4, 1]] came into being in case 2? Thank you very much.
Good question! That's because, while matrix wasn't modified, the elements inside of matrix were modified. Remember, it's a nested list: even though 0x001 and 0x002 are not the same object, (0x001)[0] and (0x002)[2] are the same object. Beause 0x002 is, fundamentally, a separate object that contains the same elements as 0x001 but in a different order. So modifying 0x002 won't change 0x001, because they're not the same object, but modifying (0x002)[0] will modify (0x001)[2], because those are the same object.
Thank you very much, Cloak. I understand it now. But it is still very complicated.
0

You can fix this by returning a rotated copy of the matrix instead of mutating it.

def rotate(matrix):
    print('1',matrix)
    matrix = matrix[::-1]
    print('2',matrix)
    for i in range(len(matrix)):
        for j in range (len(matrix[0])):
            if i<=j:
                matrix[j][i],matrix[i][j] =matrix[i][j], matrix[j][i]
    print('3',matrix)
    return matrix

mat = [[1,2,3],[4,5,6],[7,8,9]]
mat = rotate(mat)
print(mat)

Outputs

1 [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
2 [[7, 8, 9], [4, 5, 6], [1, 2, 3]]
3 [[7, 4, 1], [8, 5, 2], [9, 6, 3]]
[[7, 4, 1], [8, 5, 2], [9, 6, 3]]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.