3

Let's say I have a method with a few optional parameters.

def foo(a, b=1, c=2, d=3)

How do I go about calling it so that if my variables are None or empty strings the defaults are used?

Conditionals like the following seems like a horrible solution:

if b and not c and d:
    foo(myA, b = myB, d = myD)
elif b and not c and not d:
    ...

In Java I'd jump for a factory, but it seems like that's what defaults are supposed to avoid in this case.

1
  • In this case I wouldn't use optional parameters, but require them and just check if they have suitable values. But this obviously depends on other details of your code. Commented Aug 26, 2013 at 21:48

5 Answers 5

7

I would change foo so it replaces empty values with default ones.

def foo(a, b=None, c=None, d=None):
    if not b: b = 1
    if not c: c = 2
    if not d: d = 3

Note that this will treat all "false-y" values as defaults, meaning not only None and '' but also 0, False, [], etc. Personally I would tighten the interface up and use None and only None as a default value.

def foo(a, b=None, c=None, d=None):
    if b is None: b = 1
    if c is None: c = 2
    if d is None: d = 3
Sign up to request clarification or add additional context in comments.

6 Comments

This only checks the truth value of the variables. For example, if b were passed in with a value of 0, then this will still replace it. I only call this out as an error since the OP mentioned checking only for None and ''. If however, OP wants to check for all falsey values, then your solution works just as well (also check out mine for a slightly cleaner implementation using or)
This formulation (not b) considers 0 non-existent, which is probably not desirable.
@delnan According to the OP's example, it should be.
@ThijsvanDien Assuming OP fully comprehends what that example does. That's not an assumption I'm willing to make blindly.
I'd make it foo(a, b=None, c=None, d=None) to keep the arguments really optional.
|
3

Though I agree that changing the method is a better idea, here's an alternative that changes the calling part by using a dict of arguments, which is filtered and then unpacked:

d = {'b': myB, 'd': myD}
foo(myA, **{k: d[k] for k in d if d[k]})

Of course if d[k] can be replaced by if d[k] not in {None, ''} for example, which has a slightly different meaning (as pointed out by others).

4 Comments

Why not just **{k: v for k, v in d.items() if v}?
@abarnert There's multiple more or less equivalent ways of expressing this. In Python 2.7 d.items() returns a list rather than an iterator, which in some cases may be bad for performance reasons. It becomes d.iteritems() then, which is already less elegant. ;) This one works the same in both versions. [Looking at your reputation I assume I needn't explain that to you, but others might be interested.]
+1 Thanks for figuring out a way from the caller's point of view. While not ideal, this might be what needs to happen.
@ThijsvanDien: I think it's still more readable to say d.iteritems() if you're in 2.x and have a large dict (for a small one, d.items() is fine as-is…) than to explicitly look up d[k] twice. (Not that there's a real DRY issue here, much less a performance one, but it will look like one to many readers, and that's often enough reason to avoid it.)
2

If you want to catch ONLY None and '':

def foo(a, b, c, d):
    blacklist = set([None, ''])
    if b in blacklist:
        b = 1
    if c in blacklist:
        c = 2
    if d in blacklist:
        d = 3

If you want to catch all values v such that bool(v) is False, then:

def foo(a, b, c, d):
    b = b or 1
    c = c or 2
    d = d or 3

Or you could decorate the function with another function that does the assertions for you (which may or may not be overkill, based on your use case)

1 Comment

I mostly do it like this. But I still leave them defaulting to None, so you don't have to pass them if you don't want to.
2

You could call a function that filters out the variables you don't want passed down

def arg_filter(**kw):
    return dict((k,v) for k,v in kw.items() if v not in (None, ''))

foo(**arg_filter(a=1,b=None,c=''))

3 Comments

If you want to match the OP's not c and not d… logic, just use if v as your filter clause. Also, this is Python 2.7, so you can use a dict documentation. So: return {k: v for k, v in kw.items() if v}.
@abarnert - agreed. Any conditions can be used. This example matches the poster's request "if my variables are None or empty strings" by leaving out such things as 0 and False but that can easily be tweaked. The form was chosen on purpose so that it looks much like a regular function call. I find that more esthetically pleasing.
You seriously find dictionary comprehensions unpleasing compared to calling dict on an iterable over tuples?
1

Not fully tested, but should act as a base:

import inspect
from functools import wraps

def force_default(f):
    @wraps(f)
    def func(*args, **kwargs):
        ca = inspect.getcallargs(f, *args, **kwargs)
        da = inspect.getargspec(f)
        dv = dict(zip(reversed(da.args), reversed(da.defaults)))
        for k, v in ca.iteritems():
            if v is None or v == '':
                ca[k] = dv[k]
        return f(**ca)
    return func

@force_default
def foo(a, b=1, c=2, d=3):
    print a, b, c, d

foo(6, '', None, 'rabbit!')
# 6 1 2 rabbit!

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.