Details the default parameters in Python functions

  • 2020-04-02 14:44:47
  • OfStack


import datetime as dt
 
def log_time(message, time=None):
  if time is None:
    time=dt.datetime.now()
  print("{0}: {1}".format(time.isoformat(), message))

I recently discovered a nasty bug in a piece of Python code that USES default parameters incorrectly. If you already know all about default parameters and just want to laugh at my silly mistake, please skip to the end of this article. Well, I wrote this code, but I'm pretty sure I was possessed that day. You know, sometimes that's the way it is.

This article is just a summary of the basics of standard and default arguments for Python functions. Alert you to possible pitfalls in your code. If you're new to Python and you're starting to write some functions, I'd really recommend you take a look at the official Python manual on functions. Here's the link: (link: https://docs.python.org/3/tutorial/controlflow.html#defining-functions) and (link: https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions).
Just a quick review of the function

Python is a powerful object-oriented language that pushed this programming paradigm to its zenith. However, object-oriented programming still relies on the concept of functions, which you can use to process data. Python has a broader concept of callable objects, meaning that any object can be called, which means that data is applied to it.

Functions are callable objects in Python, and at first glance, they behave like functions in other languages. They take some data, called parameters, process it, and then return the result (None if there is no return statement)

Arguments are declared as placeholders (when defining a function) to represent the objects that are actually passed in when the function is called. In Python you don't need to declare the types of arguments (for example, as you do in C or Java) because Python philosophy relies on polymorphism.

Remember, Python variables are references, the memory address of the actual variable. This means that Python functions always "address" (a C/C++ term is used here), and that when you call a function, instead of copying the value of an argument to replace the placeholder, you point the placeholder to the variable itself. This leads to a very important result: you can change the value of the variable inside the function. (link: http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables) has a very good visual interpretation, the mechanism of reference.

References play a very important role in Python and are the backbone of Python's fully polymorphic approach. On this very important topic, please click this link: http://lgiordani.com/blog/2014/08/21/python-3-oop-part-4-polymorphism) for better explanation.

To check if you understand this basic feature of the language, follow this simple code (the variable ph stands for placeholder)
 


>>> def print_id(ph):
... print(hex(id(ph)))
...
>>> a = 5
>>> print(hex(id(a)))
0x84ab460
>>> print_id(a)
0x84ab460
>>>
>>> def alter_value(ph):
... ph = ph + 1
... return ph
...
>>> b = alter_value(a)
>>> b
6
>>> a
5
>>> hex(id(a))
'0x84ab460'
>>> hex(id(b))
'0x84ab470'
>>>
>>> def alter_value(ph):
... ph.append(1)
... return ph
...
>>> a = [1,2,3]
>>> b = alter_value(a)
>>> a
[1, 2, 3, 1]
>>> b
[1, 2, 3, 1]
>>> hex(id(a))
'0xb701f72c'
>>> hex(id(b))
'0xb701f72c'
>>>

If you're not surprised by what's happening here, you've mastered one of the most important parts of Python, and you can safely skip the explanation below.

The print_id() function shows that the placeholders inside the function are exactly the same as the variables passed in at runtime (their memory addresses are the same).

Both versions of alter_value() are intended to change the value of the incoming parameter. As you can see, the first alter_value() does not change the value of a as successfully as the second alter_value(). Why is that? They actually behave the same, trying to modify the value of the original variable passed in, but in Python, some variables are immutable, and integers are in this column. On the other hand, lists are not immutable, so the function does what its name promises. In the (link: https://docs.python.org/3.4/reference/datamodel.html), you can find more details about the immutable types.

There is more to be said about functions in Python, but these are the basics of standard arguments.
Default parameter value

Sometimes you need to define a function to accept an argument, and the function behaves differently when the argument is present or not. If a language doesn't support this, you have only two choices: the first is to define two different functions and decide which one to call each time, and the second is that both are possible, but neither is optimal.

Python, like other languages, supports default parameter values, meaning that function parameters can be specified when invoked or left blank to automatically accept a predefined value.

A very simple (and useless) example of default values is as follows:
 


def log(message=None):
  if message:
    print("LOG: {0}".format(message))

This function can be run with one argument (it can be None)


 
>>> log("File closed")
LOG: File closed
>>> log(None)
>>>

But it can also run without arguments, in which case it accepts the default value set in a function prototype (in this case None)
 


>>> log()
>>>

You can find more interesting examples in the standard library, for example, in the open () function (see (link: https://docs.python.org/3.4/library/functions.html#open)
 


open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

Function prototypes can demonstrate that calls such as f = open('/etc/hosts') hide many parameters (mode, buffering, encoding, etc.) by passing in default values, and make a typical application of this function very simple to use.

As you can see from the built-in open() function, we can use either the standard or the default arguments in the function, but the order in which both appear in the function is fixed: first the standard arguments are called, then the default arguments are called.
 


def a_rich_function(a, b, c, d=None, e=0):
  pass

The reason is obvious: if we can put a default parameter in front of the standard parameter, the language will not understand whether the default parameter has been initialized. For example, consider the following function definition
 


def a_rich_function(a, b, d=None, c, e=0):
  pass

What arguments are passed when we call the function a_rich_function(1, 2, 4, 5)? Is it d=4, c=5 or c=4, e=5? Because d has a default value. So the definition of this order is forbidden, and if you do, Python throws a SyntaxError
 


>>> def a_rich_function(a, b, d=None, c, e=0):
... pass
...
 File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>>

The default parameter is evaluated

The default parameters can be raised by normal values or by the result of a function call, but the latter technique requires a special caveat

A normal value is hard-coded, so no value is required except at compile time, but the function call is expected to evaluate at run time. So we could write it this way
 


import datetime as dt
 
def log_time(message, time=dt.datetime.now()):
  print("{0}: {1}".format(time.isoformat(), message))

Each time we call log_time() we expect it to provide the current time correctly. The tragedy is that it doesn't work: the default parameter is evaluated at definition (for example, when you first import the module), and the result of the call is as follows
 


>>> log_time("message 1")
2015-02-10T21:20:32.998647: message 1
>>> log_time("message 2")
2015-02-10T21:20:32.998647: message 2
>>> log_time("message 3")
2015-02-10T21:20:32.998647: message 3

If the default value is assigned to an instance of a class, the result will be more strange, you can read in (link: http://docs.python-guide.org/en/latest/writing/gotchas/). According to the.. The usual solution is to replace the default parameter with None and check the parameter values inside the function.
 
conclusion

Default parameters greatly simplify the API, and you need to focus on its only "failure point," which is the timing of the evaluation. Surprisingly, one of Python's most basic things, arguments and references to functions, is one of the biggest sources of errors, sometimes for experienced programmers as well. I recommend taking the time to learn about references and polymorphism.
Related reading:

      OOP concepts in Python 2.x, Part 2       Python 3 OOP Part 1, Objects and types       Dig up Django class-based views       Python duplicates From Iterators to Cooperative Multitasking       OOP concepts in Python 2. X, Part 1


Related articles: