Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to default mutable arguments

In python, a well known edge case occurs if you directly make a mutable type a default argument:

def foo(x=[]): return x
y = foo()
y.append(1)
print foo()

The usual work-around is to default the argument to None and then set it in the body. However, there's 3 different ways to do this, 2 of which are basically the same but the third is quite different.

def foo(x=None):
    if x is None:
        x = []
    return x

This is what I usually see.

def foo(x=None):
    x = [] if x is None else x
    return x

Identical semantically. A line shorter, but some people complain that python's ternary is unnatural because it doesn't start with the conditional and recommend avoiding it.

def foo(x=None):
    x = x or []

This is the shortest. I only learned about this madness today. I know lisp so this is probably less surprising to me than some python programmers, but I never thought this would work in python. This behavior is different; if you pass something that is not None but evaluates false (like False) it will not override the default. It can't be used if the default doesn't evaluate false, so if you have a non-empty list or dict default it cannot be used. But empty lists/dicts are (in my experience) 99% of the cases of interest.

Any thoughts on which is the most pythonic? I realize there is an element of opinion here, but I'm hoping someone can give a good example or reasoning as to what is considered the most idiomatic. Compared to most communities python tends to strongly encourage people to do things a certain way so I'm hoping this question and its answers will be useful even if it's not totally black and white.

like image 607
Nir Friedman Avatar asked Oct 17 '22 10:10

Nir Friedman


2 Answers

I'd go with #1 because it's simpler; its "else" branch is implied. It is harder to misinterpret it.

I'd not go with #3 in this particular case: bool(x) is equally false for None, [], {}, (), 0 and a few other things. If by mistake I pass a 0 into a function that expects a list, it's better if the function fails fast, instead of mistaking the zero for an empty list!

In other cases c and x else y could be a convenient ternary operator, but you have to control the type of c; it's easier when it's a local variable and not a function parameter.

If you often find yourself substituting a value for None, white a function for that. Consider something like x = replace_none(x, []).

like image 73
9000 Avatar answered Oct 23 '22 00:10

9000


I'd say the first way is best in the general case. The first and second way are functionally equivalent, but the first will be easier to read for newcomers.

def foo(x=None):
    if x is None:
        x = []
    return x

The x or [] trick can only be used if:

  • The argument will not be mutated (since you replace an empty collection by a new one).
  • You do not care between different falsy values of the argument (because you lose any difference between [], {}, None, my-special-class-with-__bool__).
  • You think anyone reading your code knows how it works (or wants to go figure it out).

Ass a side note, the or trick can be used if the default evaluates to true: x or [1] will still be [1] if x is falsy. But you won't be able to use [] as argument, as it will be replaced by [1].

like image 1
Mark Avatar answered Oct 23 '22 00:10

Mark