Yesterday I tweeted my excitement about the way JavaScript handles the evaluation of default values for function parameters:
I want to expand on this tweet, and discuss in more detail the two snippets of code I gave, comparing the behavior of this feature in JavaScript vs. Python.
Background: How Python Does Defaults
Back in the Day™, especially in my college era, I wrote a lot of Python. I'm still a big fan of Python, though I don't get to use it too much these days.
Anyway, there was a day when I was working on some big Python project, and I was using a default value for one of the parameters to a certain function. This was years ago, so I don't remember any details, but the important thing is that the default value was a dict
. Let's imagine it was something like this:
def doSomething(o={'name':'Ken'}):
o['timesSeen'] = o.get('timesSeen') or 0
o['timesSeen'] += 1
return o
Here's what I expected to see when I ran the function multiple times:
> print(doSomething())
{'name': 'Ken', 'timesSeen': 1}
> print(doSomething())
{'name': 'Ken', 'timesSeen': 1}
> print(doSomething())
{'name': 'Ken', 'timesSeen': 1}
Here's what I saw instead:
> print(doSomething())
{'name': 'Ken', 'timesSeen': 1}
> print(doSomething())
{'name': 'Ken', 'timesSeen': 2}
> print(doSomething())
{'name': 'Ken', 'timesSeen': 3}
The difference, of course, being that the 'timesSeen'
entry is being incremented each time.
My actual code was much subtler than this, and the effects not quite so obvious, so it eventually took me over a day, IIRC, to figure out what was happening. And the answer is: the default value is evaluated only once, when the function is declared!
The object that serves as the default value for the o
parameter is evaluated when the def
statement is first evaluated, and only then, rather than (as I expected) each time the function is called without providing a value for o
. As such, the default object becomes a shared reference across multiple runs of the function, and changes made to it in a given run are carried over into the next run.
This evaluated-only-once behavior is reflected in a different way in the sample code I included in my tweet:
This code runs a function that prints a timestamp, using the current time (provided by datetime.datetime.now()
) as a default value. It runs this function 4 times, with a 1-second pause (time.wait(1)
) in between each run. If the default value was being reevaluated each time the function was called, you'd expect to see the timestamp's second field increase by 1 each time the function was called. What we see instead is the same exact timestamp print 4 times.
So this is how it works in Python. It's mostly fine once you know about it, easy enough to work around, but not intuitive, and IMHO less valuable than if the expression was re-evaluated each time. It works fine for primitives like numbers and strings, or in cases where you're only reading from the object and never updated it, but if you want to update an object and use it later, don't use default object values.
The New Fancy: ES6 Default Parameter Values
The ECMAScript 2015 Specification, known colloquially as ES6, defined default function parameters, which gave JavaScript a similar functionality to default parameter values in Python (and several other languages).
But there is a crucial difference between Python and JavaScript default parameters: JS default expressions are evaluated each time the function is run!
To demonstrate, let's try the doSomething()
function from above in JavaScript:
function doSomething(o={name:'Ken'}) {
o.timesSeen = o.timesSeen || 0
o.timesSeen += 1
return o
}
And let's see how it behaves on multiple runs:
> doSomething()
{name: "Ken", timesSeen: 1}
> doSomething()
{name: "Ken", timesSeen: 1}
> doSomething()
{name: "Ken", timesSeen: 1}
Hey! It does what I expected before! Awesome! Instead of evaluating the default value expression when the function is defined, it's lazy-evaluated only as needed, which aligns much more naturally with my expectations, I don't know about yours.
To circle back again to my tweet, let's implement the timestamp-printing function in JavaScript:
The output now shows what we expected last time: the new Date
timestamp's second field is incremented each time the function is called, because the new Date
expression is reevaluated each time the function is called! Awesome!
Final thoughts
It should be clear by now that as much as I love Python, I strongly prefer the way JavaScript handles default parameter values. I love that the default expression is lazy evaluated: if it includes a function call, like new Date
, that function call is lazily evaluated, allowing it to reflect the current state of the system with any updates made since the last time you called the function.
(Disclaimer: Side-effects can bite you, try to use them sparingly!)
But what are your thoughts? Do you prefer Python's way of doing things? Are your expectations about default expressions different than mine?
I'm absolutely sure that this was an intentional move by the maintainers of Python, especially since this behavior was carried over from Python 2.x to the world of Python 3.x, when several other large syntax changes were made, so I'm curious if anyone knows their reasoning.
Top comments (1)
I'm gonna bring this up when someone talks py vs js next time :D.
Nice article.