Allow syntax "func(arg=x if condition)"
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)
bump! On Wed, Jan 13, 2021 at 9:32 AM Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
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)
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. On Sat, 2021-03-20 at 14:36 -0700, Peter O'Connor wrote:
bump!
On Wed, Jan 13, 2021 at 9:32 AM Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
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)
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/KM2Y3B... Code of Conduct: http://python.org/psf/codeofconduct/
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
I've missed this feature on occasion as well. +1 for whatever that counts; On Mon, 22 Mar 2021 at 17:30, Caleb Donovick <donovick@cs.stanford.edu> wrote:
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 _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/2EHOQD... Code of Conduct: http://python.org/psf/codeofconduct/
On Mon, Mar 22, 2021 at 1:28 PM Caleb Donovick <donovick@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): ...
On Mon, Nov 15, 2021 at 4:18 AM Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
On Mon, Mar 22, 2021 at 1:28 PM Caleb Donovick <donovick@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): ...
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
On Sun, Nov 14, 2021 at 9:51 AM Chris Angelico <rosuav@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
On Mon, Nov 15, 2021 at 4:57 AM Christopher Barker <pythonchb@gmail.com> wrote:
On Sun, Nov 14, 2021 at 9:51 AM Chris Angelico <rosuav@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
On Sun, Nov 14, 2021 at 2:50 PM Chris Angelico <rosuav@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 <https://marc.info/?l=python-ideas&m=131066811311230&w=2>"... which seem to be a bit of a Pandora's box (1) seems less controversial than (2).
participants (6)
-
Caleb Donovick
-
Chris Angelico
-
Christopher Barker
-
Joao S. O. Bueno
-
Paul Bryan
-
Peter O'Connor