[Python-ideas] Allow syntax "func(arg=x if condition)"

28 views
Skip to first unread message

Peter O'Connor

unread,
Jan 13, 2021, 12:35:21 PM1/13/21
to Python-Ideas
I often find that python lacks a nice way to say "only pass an argument under this condition".  (See previous python-list email in "Idea: Deferred Default Arguments?")

Example 1: Defining a list with conditional elements
    include_bd = True
    current_way = ['a'] + (['b'] if include_bd else [])+['c']+(['d'] if include_bd else [])
    new_way = ['a', 'b' if include_bd, 'c', 'd' if include_bd]
    also_new_way = list('a', 'b' if include_bd, 'c', 'd' if include_bd)
    
Example 2: Deferring to defaults of called functions
    def is_close(a, b, precicion=1e-9): 
        return abs(a-b) < precision

    def approach(pose, target, step=0.1, precision=None):  
       # Defers to default precision if not otherwise specified:
        velocity = step*(target-pose) \
            if not is_close(pose, target, precision if precision is not None) \ 
            else 0 
        return velocity

Not sure if this has been discussed, but I cannot see any clear downside to adding this, and it has some clear benefits (duplicated default arguments and **kwargs are the scourge of many real world code-bases)

Peter O'Connor

unread,
Mar 20, 2021, 5:39:53 PM3/20/21
to Python-Ideas
bump!

Paul Bryan

unread,
Mar 20, 2021, 11:26:10 PM3/20/21
to Peter O'Connor, Python-Ideas
I've encountered the same issue, either matching the default values in the else clause (and hoping they won't later be changed) or having to revert to building a kwargs dict, and then in multiple `if` statements, conditionally including arguments.

I've also felt this same pain building dicts with conditionally-included items. I can't recall such pain with lists as illustrated, or with list comprehensions for that matter.

+0 on the suggested syntax. I can't comment on complexity to implement in the interpreter.

Caleb Donovick

unread,
Mar 22, 2021, 4:29:12 PM3/22/21
to Paul Bryan, Python-Ideas
Never needed this for lists but definitely had the pain for kwargs.  Seems very reasonable for that use case, +0.5.

In libraries I control I can make sure to use the same default values for functions and their wrappers.
However when wrapping functions I don't control there is not a great way to do this. And I end up
incrementally building up a kwargs dict. I suppose the same thing could occur with *args lists so it makes sense for
both positional and keyword arguments.

Yes one could do something like:
```
def fun(a, b=0): ...
def wraps_fun(args, b=inspect.signature(fun).parameters['b'].default): ...
```
But I would hardly call that clear.  Further it is not robust as would fail if `fun` is itself wrapped in way 
that destroys its signature.  E.g.:
```
def destroy_signature(f):
    # should decorate here with functools.wraps(f)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper
```

Caleb

Joao S. O. Bueno

unread,
Mar 22, 2021, 4:47:21 PM3/22/21
to Caleb Donovick, Paul Bryan, Python-Ideas
I've missed this feature on occasion as well. +1 for whatever that counts; 

_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/

Peter O'Connor

unread,
Nov 14, 2021, 12:16:36 PM11/14/21
to Caleb Donovick, Paul Bryan, Python-Ideas
On Mon, Mar 22, 2021 at 1:28 PM Caleb Donovick <dono...@cs.stanford.edu> wrote:
... One could do something like:

```
def fun(a, b=0): ...
def wraps_fun(args, b=inspect.signature(fun).parameters['b'].default): ...
```
But I would hardly call that clear.  

Caleb


I like this approach too - it just needs a cleaner syntax.  Python could make functions more "object like" by having fields for args (though I'm sure that would inspire some controversy):

def fun(a, b=0): ...
def wraps_fun(args, b=fun.args.b.default): ...
 

Chris Angelico

unread,
Nov 14, 2021, 12:51:59 PM11/14/21
to Python-Ideas
Functions ARE objects, so they can't really be more "object-like" :)
But they have their argument defaults in a slightly different way:
func.__defaults__ is a tuple of default values for the rightmost N
arguments, and func.__kwdefaults__ is a mapping from name to default
for keyword-only arguments. If you want to be able to look up any
argument (positional-only, pos-or-kwd, keyword-only) by name, you need
something that digs through the function's details and gives back that
mapping - and that's what inspect.signature does.

So yes, it's not exactly clear... but it's also not really something
you should be doing a lot of. Also, it's entirely possible that future
versions of Python will have a concept of optional arguments that
don't *have* defaults, so the entire idea of passing the default
wouldn't work.

Currently, the only way to truly say "maybe pass this argument", is to
use *args or **kwargs.

spam(*(1,) * use_eggs)
spam(**{"eggs": 1} if use_eggs else {})

Still clunky, but legal, and guaranteed to work in all Python
versions. It's not something I've needed often enough to want
dedicated syntax for, though.

ChrisA
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/FMXRNUWN5ROCIACJN74ANP3UDJM3SGEL/

Christopher Barker

unread,
Nov 14, 2021, 12:59:22 PM11/14/21
to Chris Angelico, Python-Ideas
On Sun, Nov 14, 2021 at 9:51 AM Chris Angelico <ros...@gmail.com> wrote:
 Also, it's entirely possible that future
versions of Python will have a concept of optional arguments that
don't *have* defaults,

As noticed earlier in this thread, it seems it's currently possible to do that with functions written in C. (see bisect in Python 3.8). Sorry to be too lazy to go see how that's done, but as you've been digging into this code I figured you'd already know.

So how hard would it be to expose that functionality in pure Python? Maybe that's worth pursuing to get around the "no perfect sentinel" problem.

-CHB

--
Christopher Barker, PhD (Chris)

Python Language Consulting
  - Teaching
  - Scientific Software Development
  - Desktop GUI and Web Development
  - wxPython, numpy, scipy, Cython

Chris Angelico

unread,
Nov 14, 2021, 1:19:34 PM11/14/21
to Python-Ideas
On Mon, Nov 15, 2021 at 4:57 AM Christopher Barker <pyth...@gmail.com> wrote:
>
> On Sun, Nov 14, 2021 at 9:51 AM Chris Angelico <ros...@gmail.com> wrote:
>>
>> Also, it's entirely possible that future
>> versions of Python will have a concept of optional arguments that
>> don't *have* defaults,
>
>
> As noticed earlier in this thread, it seems it's currently possible to do that with functions written in C. (see bisect in Python 3.8). Sorry to be too lazy to go see how that's done, but as you've been digging into this code I figured you'd already know.
>

That's technically true, although it would perhaps be more accurate to
say that C-implemented functions just accept *args and **kwargs and do
the work from there. The signatures exposed in Python code are
synthesized.

> So how hard would it be to expose that functionality in pure Python? Maybe that's worth pursuing to get around the "no perfect sentinel" problem.
>

From a technical standpoint? Not actually that difficult, and I
effectively implemented that as a side effect of PEP 671. You have to
fiddle around with the function's dunders to invoke that behaviour,
but it can be done.

From a syntactic standpoint? Here be dragons. Or rather, here be
endless bikesheddings.

>>> def spam(ham=...):
... try:
... print("Ham is", ham)
... except UnboundLocalError:
... print("Ham wasn't set")
...
>>> spam.__defaults_extra__ = (' ((unset)) ',)
>>> spam(42)
Ham is 42
>>> spam()
Ham wasn't set
>>>

It's a hack (FWIW the value in the tuple is what inspect.signature()
will show for the default), but what happens is that there truly isn't
a default for that argument now. The problem is, there's no really
awesome syntax for it, and an endless array of slightly-okay syntax
options, so we could argue this till next millennium :)

I'm not currently pushing for any system of "optional argument with no
default", but if PEP 671 is accepted, or at least gets enough
interest, it might inspire someone to take parts of it and come up
with such a proposal.

ChrisA
_______________________________________________
Python-ideas mailing list -- python...@python.org
To unsubscribe send an email to python-id...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python...@python.org/message/76J3W2SDBYIJDD4GJQZPV7C4NDRDRP5Z/

Peter O'Connor

unread,
Jan 19, 2022, 8:03:52 AMJan 19
to Chris Angelico, Python-Ideas
On Sun, Nov 14, 2021 at 2:50 PM Chris Angelico <ros...@gmail.com> wrote:

spam(*(1,) * use_eggs)
spam(**{"eggs": 1} if use_eggs else {})

Still clunky, but legal, and guaranteed to work in all Python
versions. It's not something I've needed often enough to want
dedicated syntax for, though.

ChrisA

The thing is that I find myself dealing with duplicated defaults all the time - and I don't know a good way to solve the problem.  The "**{"eggs": 1} if use_eggs else {}" is obviously problematic because
* It is immune to type-inspection, pylint, mypy, IDE-assisted-refactoring, etc,
* If trying to pass down the argument it actually looks like spam(**{"eggs": kwargs["eggs"]} if "eggs" in kwargs else {}) which is even messier

A lot of the time, my code looks like this: 

    def main_demo_func_with_primative_args(path: str, model_name: str, param1: float=3.5, n_iter: float=7): 
        obj = BuildMyObject(
            model_name = model_name, 
            sub_object = MySubObject(param1=param1, n_iter=n_iter)
        ) 
        for frame in iter_frames(path): 
            result = obj.do_something(frame)
            print(result)

ie I have a main function with a list of arguments that are distributed to build objects and call functions.

The problem is I am always dealing with duplicated defaults (between this main function and the default values on the objects).  You define a default, duplicate it somewhere else, change the original, forget to change the duplicated, etc...

* It makes sense to define the default values in one place.
* It makes sense for this place to be on the objects which use them (ie at the lowest level)
* It makes sense to be able to modify default values from the top level function.
But the above 3 things are not compatible in current python (at least not in a clean, pythonic way!)

The only ways I know of to avoid duplication are: 
* Define the defaults as GLOBALS in the module of the called function/class and reference them from both places (not always possible as you don't necessarily control the called code).  Also not very nice because you have to define a new global for each parameter of each low-level object (a a different sort of duplication).
* Messy dict-manipulation with kwargs (see above)
* Messy and fragile default inspection using inspect module 

The only decent ways I can think of to avoid duplicated-defaults are not currently supported in Python:

1) Conditional arg passing (this proposal):
        def main_func(..., param_1: Optional[float] = None, n_iter: Optional[int] = None):
            sub_object = MySubObject(param1=param1 if param1 is not None, n_iter=n_iter if n_iter is not None)

2) Ability to cleanly reference defaults of a lower-level object: 
         def main_func(..., param_1: float=MySubObject.defaults.param1, n_iter: int=MySubObject.defaults.n_iter):
             sub_object = MySubObject(param1=param1, n_iter=n_iter)

3) "Deferred defaults"... which seem to be a bit of a Pandora's box

(1) seems less controversial than (2). 

 
Reply all
Reply to author
Forward
0 new messages