import_that: XKCD guy flying with Python (Default)
[personal profile] import_that
One topic which comes up from time to time, usually generating a lot of heat and not much light, is the question of how Python passes values to functions and methods. Usually the question is posed as "Does Python use pass-by-reference or pass-by-value?".

The answer is, neither. Python, like most modern object-oriented languages, uses an argument passing strategy first named by one of the pioneers of object-oriented programming, Barbara Liskov, in 1974 for the language CLU. Liskov named it pass-by-object-sharing, or just pass-by-sharing, but the actual strategy is a lot older, being the same as how Lisp passes arguments. Despite this august background, most people outside of Python circles have never heard of pass-by-sharing, and consequently there is a lot of confusion about argument passing terminology.

Let's start by looking at how people get confused. Let's start by "proving" that Python is pass-by-value (also know as call-by-value), then we'll "prove" that Python is pass-by-reference (call-by-reference). It's actually neither, but if you think that there are only two ways to pass arguments to a function, you might be fooled into thinking Python uses both.

Python looks like pass-by-value

py> def add_one(arg):
...     arg = arg + 1
py> x = 23
py> add_one(x)
py> x

We pass a variable x to the function add_one, where it is assigned to the local variable arg and then one added to it. But the value of x doesn't change, so arg must have been a copy. This "proves" *cough cough* that Python is pass-by-value for ints.

Python looks like pass-by-reference

py> def append_one(arg):
...     arg.append(1)
py> x = []
py> append_one(x)
py> x

This time we pass a variable x to the function append_one, where it is assigned to local variable arg and then one appended to it. This time the value of x does change, so arg couldn't have been a copy. This "proves" *cough cough* that Python must be pass-by-reference for lists.

Seems perfectly reasonable. The only trouble is that it's nonsense. Python uses the same calling convention for ints and lists as well as everything else, and we can easily demonstrate that Python doesn't use either calling convention.

But Python actually isn't pass-by-value

The most obvious characteristic of pass-by-value is that the value is copied. In Python, all values are objects, and objects have an ID which is guaranteed to be unique during the lifetime of the object. So if an object is copied, the copy will get a different ID:

py> py> def test(arg):
...     print(id(arg))
py> x = 42
py> print(id(x))
py> test(x)

The two IDs are the same, so local variable arg is not a copy of x, it is the same object.

Nor is Python pass-by-reference

The classic test of pass-by-reference is to write a "swap" procedure that swaps the two passed-in arguments in-place. In Pascal, we would declare the arguments using the var keyword and write it like this:

procedure swap(var a: int, var b: int);
    tmp: int;
    tmp := a;
    a := b;
    b := tmp;

The Python equivalent, if it worked, would be like this:

def swap(a, b):
    a, b = b, a  # No need for a temporary variable.

Trouble is, it doesn't work! You can't write a swap procedure in Python. You can write a swap function, and return the swapped values, but that's cheating:

# This works, but doesn't satisfy the requirements.
def swap(a, b):
    return b, a

x, y = swap(x, y)

Or you can hard-code the names of the variables:

# The variables have to be passed-in, not hard coded.
def swap():
    global a, b
    b, a = a, b

The problem here is that there is no way in Python to refer to the variable itself, only to the object assigned to that variable. (You can pass a string containing the variable name, and then mess about with globals(), but that's against the rules. The rules state that you have to pass the variable into the function, not the variable's name.) That's quite different from the situation in Pascal, which does support pass-by-reference. In the Pascal swap procedure above, if you pass two variables swap(x, y); the local variables a and b don't just copy the values of x and y but actually become those variables for the duration of the procedure call.

In Python, the parameters to a function or method are always local to that function or method. Any assignments to local variables inside the function only affect the local variable, not the caller's variable. That explains why the add_one function earlier didn't modify the global x value: arg = arg + 1 is an assignment, so it assigns a new value to the local arg and does not touch the x it originally came from.

In the second example, append_one doesn't assign a new value to the local variable. Instead it calls the list append method, which modifies the list object in place. Since the global variable x and the local variable arg are both bound to the same list object, they both see the same changes when the append method is called.

To summarise:

Pass by value

The value of the passed in variable is copied into the function's local scope and assigned to the function parameter. Re-assigning or modifying the local parameter does not effect the caller.

Pass by reference

A reference to the passed in variable is passed to the function. The function's parameter becomes identical to the variable passed in. The caller sees the result of either re-assigning or modifying the local parameter.

Pass by (object) sharing

A reference to the value of the passed in variable is passed to the function, resulting in the value being shared by the caller and the function. Re-assigning the local parameter does not effect the caller, but in-place modifications to the value are seen by the caller.

Python is neither pass by reference nor pass by value, but pass by sharing.

A substantially different version of this post appeared here.


import_that: XKCD guy flying with Python (Default)
Steven D'Aprano

May 2015

345678 9

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags