On the Variability of Function Parameters

Bing’s answer:
In Python, the behavior of changing function parameters depends on the variability of the parameters. Let’s explore in detail.
Immutable objects: These objects, when passed to a function, do not affect the actual parameters when modified internally by the function. Common immutable objects include integers, floating-point numbers, strings, and tuples.
Variable objects: When these objects are passed to a function, modifications to the formal parameters within the function may affect the actual parameters. Common mutable objects include lists, dictionaries, and collections.

My test:

def func1():
def func2(a):
def func3():
def func4(a):


Why is this picture variable?

def dw(image):

I am sorry, what is your question? Do you have a problem you want to solve? Or do you just want to know why specific design decisions were made (in python or in opencv)?

func2 and func4 don’t do anything because the body of those functions is just an expression whose value is discarded.

In the case of dw, a reference to an image is passed in and that image is mutable. Drawing on the image changes it.

To emphasize, as Matthew states, your func2 and func4 don’t mutate the object refereed to by a; in fact, they don’t even create a new object named a. What you might have intended to do instead was a += ['456'] rather than just +, which for a mutable object will typically mutate it in place.

Here’s a much clearer and more instructive example I whipped up, which illustrates the difference between mutating the original object and creating a new object assigned to the same (local) name:

def iadd(x, y):
    x += y
    return x

immutable_original = 1
mutable_original = [1]

immutable_returned = iadd(immutable_original, 2)
mutable_returned = iadd(mutable_original, [2])

print(f"{immutable_original=}, {immutable_returned=}, {mutable_original=}, {mutable_returned=}")

Carefully examine it and think it through. What do you expect it to print? Then, test it. Does it print what you expected? If not, why?

I just want to know why one of these two parameters is changed and the other is not, and how to distinguish them?

You’re right, I made changes to the code:
def func2(a):
def func4(a):
The result is the same, of course I know to use return, but my problem is that even if I don’t return, the image object is still changed

Well, I don’t know much about OpenCV, but I can only presume that the purpose of the line() method of an OpenCV image object is to add a line to that image, just like any of the matplotlib.pyplot methods that add a line, plot, etc. to an Axes (e.g. ax.plot(), etc). If you want to make a copy of that image before modifying it, there should be a copy() method, e.g. image_copy = image.copy().

I’m just signed up to answer this post :wink:

Let’s break your test code in chunks:

def func4(a):


def func4(a):

After the call to this function the a variable will be a new local variable that POINTS to the same value as a variable outside of the function. You have to remember that in Python variables are references to values. It doesn’t matter if the value in question is mutable or not.

   ... = a+['456']

Ah, there lies the main offender of your problem. Here we are adding two variables that both points to lists. In Python documentation for lists they state that the list’s + operator creates a COPY with a value that corresponds to the old values concatenated.
So, the expression after +-ing is a fresh new value that has no connections with the old value.

   a = ...

After our fresh new value was made, this code assigns it to our, remember, LOCAL variable a.

   #end of function func4

At the end the local a variable is destroyed. Because a pointed to a value that had only ONE owner (the local a variable), the value is destroyed too.

And that’s the story of the func4 function. As for this code:

def dw(image):

I don’t know about the library it comes from, but still I can give you the answer :wink:
You are calling a method of a value. If what you are telling about the picture having a new content is right, then it means the line(...) method must change the value INTERNALLY inside this method. And that’s the reason the image is changed and preserved in the outside code.

In other words your test code and the code with dw(...) function touched a TWO different aspects of Python coding.

I hope my explanation now makes all the things clear for you :smiley:


Thank you, I remember, the variable a in these two functions is not actually the same variable.

Based on my experience, many special object types that are passed as parameters to a function will be directly changed. For example, pymxs.runtime.objects, as well as those mentioned earlier, and so on. Is there still a clear boundary between these two methods? Do you have any official documents or similar? When encountering new objects, we can only try them one by one? Is there a way to distinguish it?

It should indeed be copied, otherwise it will be changed. I didn’t encounter any difficulties at work, I was just curious.

I’m afraid the answer to all your questions is “no” :frowning: You could theoretically read the source code of a library and find it out, however it sounds like an even more time-consuming task. Unless someone have stated clearly in the library’s documentation if this-or-that is a non-mutable object, then the code reading is the only way to be 100% sure :frowning: :frowning: :frowning:

Edit: However there can be some clues: If an object offers a way to use it in constructions like x + y (eg. they have got + - * / operators overloaded) then it is a good clue that the object in question may be non-mutable, because these operators should return a new object.

There is one more thing: all Python object are actually mutable. The fact that something is called ‘non-mutable’ means they refuse to provide ways to change its internal state, by not using anythings that could suggest or require to modify it.

1 Like

Just so you know, this is a horribly confused explanation. At its core, it doesn’t distinguish between parameters and arguments. There’s no such thing as a “variable object”, and whether an argument is mutable or not only indicates what a function could do with an argument, not what it actually does. (You can pass a mutable object to a function without the object being mutated.)

A function parameter is just a name:

def f(x):
    return x

The parameter x is a name used to refer to a value (mutable or not) inside the body of the function.

A function argument is a value provided when you call the function:

z = 5
y1 = f(3)
y2 = f(z)
y3 = f(z**2 + 5*z + 9)

The three arguments passed to f are 3, 5, and 59. Each expression is evaluated to produce a single value to be used as the argument.

Once the argument is evaluated, it is assigned to the corresponding parameter. It’s as if you wrote

x = 3
y1 = x
x = z
y2 = x
x = z**2 + 5*z + 9
y3 = x

It’s only when you look at how the parameter gets used inside the function that the issue of whether the argument is mutable or not comes into play.

def f1(x: list[int]):
    return x + [4,5,6]

def f2(x: list[int]):

Both functions produce a list that ends with [4, 5, 6]. The first one does so by creating a new list using the value of x, but without modifying x. The second one does so by modifying x directly, with no new list created.

y = f1([1,2,3])
assert y == [1,2,3,4,5,6]

z = [1,2,3]
y = f1(z)
assert y == [1,2,3,4,5,6] and z == [1,2,3]

y = f2([1,2,3])
assert y is None

In the third example, we can’t observe that the argument was modified, because we don’t have another reference to it, and the list is immediately thrown away. This shows that a mutating function doesn’t require a variable to be used as its argument; it’s the value that matters. But we can fix that:

z = [1,2,3]
y = f2(z)
assert y is None and z == [1,2,3,4,5,6]