Mimic Reference Parameters In Python

In my recent project, I had the joy of migrating C# code that lovingly relies on reference parameters. Enter Python, the cool kid who casually shrugs and says, ‘Yeah, we don’t do that here’. Yes, Python does not support reference parameters. But fear not! In this article, we’ll explore how to sweet-talk Python into mimicking those reference parameter.

Reference Parameters: The Alias Masters of Memory

A variable identity card with 'Also Known As' in the functions
mimic reference-parameters in python

A reference parameter is like a backstage pass to the same storage location as the variable handed over during a function call. Unlike regular parameters, which are content to copy values and mind their own business, reference parameters dive straight into the action. They point directly to the memory of the original variable, meaning they don’t just observe—they participate.

Think of a reference parameter x as an alias for the variable y that was passed into the function. It’s like calling someone by a nickname—they’re still the same person, but now you have two ways to refer to them. Any changes you make to x inside the function are instantly reflected in y because, under the hood, they’re really just two names for the same thing. Adjust x, and y adjusts too. Break x, and, well, y is broken right alongside it.

This behavior is incredibly handy in many programming languages, such as C# and C++, where reference parameters shine. They allow developers to modify variables directly without the need to juggle return values or extra steps. It’s like handing over the keys to your house instead of just giving someone a map—efficient, but it does require trust and careful handling.

While languages like C# and C++ embrace reference parameters as a core feature, others, like Python, prefer to take a more hands-off approach. Python avoids the direct memory-sharing drama and instead focuses on passing objects around in its own unique way

Local variables, parameters and arguments

A function’s local variable is a variable that is defined within the function body and can only be accessed or used within that specific function. These variables are created when the function is called and are destroyed once the function execution is complete. This ensures that their scope is limited to the function in which they are declared, making them inaccessible from outside the function.

A function’s parameter is a specific type of local variable that serves as a placeholder for the value provided during the function’s invocation. When a function is defined, parameters are declared within the parentheses following the function name. These parameters enable the function to accept input values and operate on them.

A function’s argument is the actual value or expression provided to a function’s parameter when the function is called. This value is passed to the function and assigned to the corresponding parameter at the start of the function’s execution. The argument essentially provides the input data that the function will process.

For example, consider the following code snippet:

local variable,parameter and argument

 def sumSquare(x1 , x2):
   sum = x1 + x2
   return sum * sum

 #print x1  # will raise <code>NameError: name 'x1'  is not defined</code>
 #print x2  # will raise <code>NameError: name 'x2'  is not defined</code>
 #print sum # will raise <code>NameError: name 'sum' is not defined</code>

 print sumSquare(1, 1+1) # <== A
 print sumSquare(3, 2+2) # <== B

In the sumSquare function, x1 and x2 are parameters, while sum is a local variable. The key thing to remember is that both parameters and local variables are confined to the function body—they can only be accessed and used within that function.

Let’s take a closer look at how this works in action:

In A1, we call the sumSquare function with the arguments 1 and 1+1. Here, the initial value of parameter x1 becomes the result of the expression 1 (which is simply 1), and the initial value of parameter x2 becomes the result of the expression 2+2, which evaluates to 4. So, the parameters are set to x1 = 1 and x2 = 4 when the function starts.

In A2, we call the same function, but with different arguments: 3 and 2+2. This time, the initial value of x1 is 3, and the initial value of x2 is again 4 (since 2+2 still equals 4). These values are passed into the function when it’s invoked.

In both cases, the values passed as arguments are assigned to the parameters x1 and x2 as the function begins its execution.

Python exclusively uses value parameters. A value parameter, such as x, is a local variable within the function, and its initial value is set to the result of the expression passed as an argument during the function invocation.

The argument passed can be any valid expression. But what happens if the argument is a variable? In this case, any changes made to the parameter inside the function will not affect the original variable outside the function. This is because Python passes the value of the expression, not the reference to the original variable.

Consider the following code:

def double(x1):
   print '[Enter  double] x1={0}'.format(x1)
   x1 = 2 * x1
   print '[Exit   double]x1={0}'.format(x1)

 x2 = 2
 print '[Before double] x2={0} '.format(x2)
 double(x2)
 print '[After  double] x2={0} '.format(x2)

The output will be:

 [Before double] x2=2 
 [Enter  double] x1=2 
 [Exit   double] x1=4 
 [After  double] x2=2 

As we can see, Even though the value of x1 changed within the double function, this does not impact the value of x2, which was passed as the argument to the function.

def parseInt(ss):
     return int(ss)

 print parseInt("2") * parseInt("a")

Reference parameters can often simplify code and make our intentions clearer.

Reference parameters can often simplify code and make our intentions clearer. As we’ve discussed, a reference parameter x acts as an alias for the y variable passed into the function. This means that any modifications made to x inside the function are automatically reflected in y.

Now, let’s explore a scenario where we need to convert a string to an integer.

The motive for reference parameter – without reference parameter
def parseInt(ss):
     return int(ss)

 print parseInt("2") * parseInt("a")

When running the code, an exception will be raised: ValueError: invalid literal for int() with base 10: 'a'. This happens because the string 'a' cannot be directly converted to an integer, as it’s not a valid numeric value.

To handle such cases more gracefully, it would be helpful if the function could return a boolean indicating whether the given string represents a valid number. Additionally, it would be useful if the function could update the second parameter with the converted integer value if the conversion is successful. This way, the function can provide feedback on the validity of the input and allow for better error handling without interrupting the flow of the program.

The motive for reference parameter – refactored with reference parameter
 def tryParseInt(ss: str, vv: int):
     try:
         vv = int(ss)
         return True
     except:
         vv = 0
         return False

 v1 = 0
 v2 = 0
 if tryParseInt("2",v1) and tryParseInt("3",v2):
     print v1*v2 # will print 0

Unfortunately, this approach does not work as expected because Python only supports value parameters. This means that when a value is passed to a function, Python creates a copy of the argument and assigns it to the parameter. As a result, any changes made to the parameter inside the function will not affect the original variable that was given as the argument. The original variable remains unchanged outside the function.

It’s important to note that while we cannot directly change the value of the variable that was passed as an argument, we can still modify its attributes (if the variable is a mutable object, like a list or dictionary). This is because the reference to the object is passed, allowing us to modify its contents, but not the reference itself.

In the next section, we will use this observation to mimic reference parameters. By working with mutable objects, we can simulate the behavior of reference parameters, where changes made inside the function affect the original variable outside the function.

As we can see, we cannot directly change the value of the variable that was passed as an argument, as it is essentially a copy in the context of the function. However, an important observation here is that while we cannot modify the value itself, we can change the attributes or properties of the object if it’s mutable. This insight allows us to work around the limitation of reference parameters.

By leveraging this concept, we can design a class that will enable us to simulate the behavior of reference parameters. This class will allow us to modify the internal state of the object passed as an argument, effectively overcoming the issue of not being able to directly alter the value. In the following example, we will demonstrate how to use this approach to achieve the desired functionality.

Reference Parameter With Holder
 class Holder:
     def __init__(self, value=None):
         self.value = value

With this class at hand, let’s modify the previous function:

 class Holder:
     def __init__(self,value=None):
         self.value = value

 def tryParseInt(ss,vv):
     try:
         vv.value = int(ss)
         return True
     except:
         return False

 v1 = Holder(0) ; v2 = Holder(0)
 if tryParseInt("2",v1) and tryParseInt("3",v2):
     print v1.value*v2.value # print 6

This approach works because, rather than changing the value of the parameter itself, we are modifying the value of its attribute. Since we’re not directly altering the parameter’s value, but instead adjusting an internal property or attribute, the function can still achieve the desired effect

Disadvantages of Mimicking Reference Parameters In Python

Although Python does not natively support reference parameters, we can still replicate this behavior with a little creativity and effort. While Python’s default approach is to pass arguments by value, we can mimic the functionality of reference parameters by using a class to hold the values. The Holder class, for example, is a versatile tool that can simulate reference parameter behavior by allowing us to modify the internal state of an object passed into a function.

This workaround becomes especially useful when migrating code from languages like C# or C++, which do support reference parameters. In these languages, we can directly modify the value of an argument within a function, but Python’s behavior would normally prevent that. By using the Holder class, we can bridge this gap, enabling us to achieve similar functionality and ensure a smoother transition when porting code to Python.

But as always there is no magic without a price, While using a Holder class to mimic reference parameters in Python can be a useful workaround, there are a few disadvantages to consider:

Increased Complexity: Introducing a Holder class adds unnecessary complexity to the code. Instead of relying on Python’s straightforward argument passing, you now have to manage an extra class and handle its attributes, which can make the code harder to read and maintain.

Less Pythonic: This approach goes against Python’s design philosophy of simplicity and readability. Python encourages passing objects by reference (for mutable objects) and using return values for changes, so mimicking reference parameters may feel like an unnatural workaround.

Potential for Side Effects: Using a Holder class to simulate reference parameters can introduce side effects. Since the original object is modified through the class, it can lead to unintended consequences if the object is shared across multiple parts of the program.

Performance Overhead: Creating and managing additional classes adds some overhead. While this may not be significant in small-scale applications, it could impact performance in larger systems where such workarounds are used extensively.

Debugging Challenges: Since the function’s behavior changes by modifying the attributes of a Holder object, debugging can become trickier. Tracking changes to the object’s state across different parts of the program might require additional effort.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top