Albert G Lieu
Albert G Lieu

Reputation: 911

Python functions call by reference, value or object

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]]

Upvotes: 0

Views: 76

Answers (2)

Green Cloak Guy
Green Cloak Guy

Reputation: 24691

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.

Upvotes: 2

Jmonsky
Jmonsky

Reputation: 1519

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]]

Upvotes: 0

Related Questions