1

Is there any way to use the value of an argument to dynamically set the default of a keyword argument? I was thinking of something like this:

def foo(lst, r = 0, l = len(lst)):    #I am referring to the "l = len(lst)" part
    print r, l


foo([1,2,3])

This gives the following traceback:

Traceback (most recent call last):
  File "C:/Python27/quicksort.py", line 25, in <module>
    def foo(lst, r = 0, l = len(lst)):
NameError: name 'lst' is not defined

One could do something similar to this:

def foo(lst, r = 0, l = None):
    if l is None:
        l = len(lst)
    print r, l

But I am hoping for a more elegant solution. Can anybody help?

FooBar
  • 334
  • 2
  • 10
  • 2
    Maybe shorten it to `l = l or len(lst)` in the function body. – cs95 Dec 29 '17 at 18:05
  • 4
    The values of the default arguments are evaluated when the function is defined, not when it is called. See the answers to [this question](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) – Patrick Haugh Dec 29 '17 at 18:08
  • 1
    Interesting. In Ruby, default argument values for optional parameters are evaluated when the method is called, and the order of evaluation is defined (left-to-right), so that the OP's code would work as intended. I never thought about whether Python might be different. – Jörg W Mittag Dec 29 '17 at 18:11
  • @cᴏʟᴅsᴘᴇᴇᴅ This won't allow you to pass `l=0` with a non-empty list. – user2390182 Dec 29 '17 at 18:55
  • @schwobaseggl good point. – cs95 Dec 29 '17 at 18:58

4 Answers4

1

Is there any way to use the value of an argument to dynamically set the default of a keyword argument?

Unfortunately no. This can be shown with a simpler example:

>>> def func(a, b=a):
    return a, b

Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    def func(a, b=a):
NameError: name 'a' is not defined

As said in the comments, Python keyword argument values are evaluated when the function is defined, but the value of any arguments are not know until the function is called. Thus, something like this in Python simply isn't feasible.

However, you can make your code more compact by using the ternary operator:

def foo(lst, r=0, l=None):
    l = l if l is not None else len(lst)
    print(r, l)
Christian Dean
  • 22,138
  • 7
  • 54
  • 87
1

Default parameters are evaluated at definition time, not when calling the function. Hence, the other params aren't known yet and cannot be referenced. You can achieve the desired behaviour with the following oft-seen pattern:

def foo(lst, r=0, l=None):    
    if l is None:
        l = len(lst)
    # or shorter
    # l = len(lst) if l is None else l
    # or even shorter
    # l = (l, len(lst))[l is None]
    print r, l
user2390182
  • 72,016
  • 6
  • 67
  • 89
  • @ChristianDean Nah, that will make it impossible to explicitly pass `0` for `l`. – user2390182 Dec 29 '17 at 18:24
  • Shoot! Your right, I missed that. OTHO, if the OP can insure that `l` will always be a positive integer, then `l = l or (len)` could be used. – Christian Dean Dec 29 '17 at 18:25
  • @ChristianDean True, I used to do that a lot. Nowadays, I prefer being explicit. Especially at work, I will always use the two-liner `if`. It is the clearest, everybody knows immediately what's going on. – user2390182 Dec 29 '17 at 18:28
  • 1
    Yup, that's probably the safest option. In fact, the buggy-ness of using `or` and `and` was one of the reasons conditional expressions were added to the language. See [PEP 308](https://www.python.org/dev/peps/pep-0308/). Anways, nice answer +1. – Christian Dean Dec 29 '17 at 18:33
1

But I am hoping for a more elegant solution. Can anybody help?

How about

def foo(lst, r=0, **kwargs):
    # Here we use `get`: dict.get(key, default = None)  
    print(r, kwargs.get('l', len(lst)))

def foo(lst, **kwargs):  
    print(kwargs.get('r', 0), kwargs.get('l', len(lst)))

def foo(lst, **kwargs):  
    r, l = kwargs.get('r', 0), kwargs.get('l', len(lst))
    print(r, l)

Reference

*args and **kwargs?
https://www.tutorialspoint.com/python/dictionary_get.htm

Tai
  • 7,684
  • 3
  • 29
  • 49
0

In this case i think we have to apply the principle Simple is better than complex, why don't just do:

def foo(lst, r = 0):
    print r, len(lst)

As it is been said, there is no way of doing what you want in Python, that is all...

lpozo
  • 598
  • 2
  • 9
  • 1
    This answer does not work if the OP wants to pass in `l` later on. – Tai Dec 29 '17 at 18:43
  • That is not what was asked, I think. The real answer is that there is no way of doing that in Python, at least for now. – lpozo Dec 29 '17 at 18:49
  • 1
    I mean in your way this problem is not solved. I think the reason that they want to set a default value is that they might want to pass something else later on. Otherwise, you also don't need `r = 0` as a argument and just return `print 0, len(lst)` – Tai Dec 29 '17 at 18:52
  • @Ipozo My code was a minimal example (as recommended by SO). I was obviously not trying to simply print the length of a list... – FooBar Jan 04 '18 at 14:31
  • Ok, there is no problem, what you want to do with your lst is not a problem (print is just an example, I know), what matters is that you can not do what you ask in the way you wanted to do it, so you have to process lst inside the foo function. My point here is: why to get complex? If my answer is not good for you, I'll understand. Thanks – lpozo Jan 04 '18 at 16:57