Idea: Deferred Default Arguments?

Often when programming I run into a situation where it would be nice to have "deferred defaults". Here is an example of what I mean: def subfunction_1(a=2, b=3, c=4): return a+b*c def subfunction_2(d=5, e=6, f=7): return d*e+f def main_function(a=2, b=3, c=4, d=5, e=6, f=7): return subfunction_1(a=a, b=b, c=c) + subfunction_2(d=d, e=e, f=f) Here you can see that I had to redefine the defaults in the main_function. In larger codebases, I find bugs often arise because defaults are defined in multiple places, and somebody changes them in a lower-level function but fails to realize that they are still defined differently in a higher function. The only way I currently see to achieve this is not very nice at all, and completely obfuscates the signature of the function: def main_function(**kwargs): return subfunction_1(**{k: v for k, v in kwargs.items() if k in ['a', 'b', 'c']}) + subfunction_2(**{k: v for k, v in kwargs.items() if k in ['d', 'e', 'f']}) What I was thinking was a "deferred" builtin that would just allow a lower function to define the value (and raise an exception if anyone tried to use it before it was defined) def main_function(a=deferred, b=deferred, c=deferred, d=deferred, e=deferred, f=deferred): return subfunction_1(a=a, b=b, c=c) + subfunction_2(d=d, e=e, f=f) I assume this has been discussed before somewhere, but couldn't find anything on it, so please feel free to point me towards any previous discussion on the topic.

Hi Peter Interesting problem. We can already get something like your proposed solution by using None instead of deferred. You have to start with def subfunction_1(a=None, b=None, c=None): if a is None: a = 2 # similarly for b and c. return a+b*c You will loose the default values being shown when you do help(subfunction). There's another thread on PEP 505, active right now, that would allow you to write something like a = a OR 2 to provide the default values. https://mail.python.org/pipermail/python-ideas/2018-July/052071.html I hope this helps. Please let us know if that might work for you. -- Jonathan On Fri, Jul 20, 2018 at 10:03 AM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:

On Fri, Jul 20, 2018 at 7:03 PM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
Hmm. This might be something where a bit of a helper could, well, help. Is this a pattern that you make use of a lot? Suppose you write your main function like this: @combines(subfunction_1, subfunction_2) def main_function(sf1, sf2): return subfunction_1(**sf1) + subfunction_2(**sf2) or even like this: @precombine def main_function(subfunction_1, subfunction_2): return subfunction_1 + subfunction_2 The decorator could then generate a new function, with a proper signature (available to help()), that hides the messy details of filtering kwargs - and in the case of precombine, actually calls the function as well. (Obviously you can't use this if you might want to call a subfunction conditionally.) Another option would be to have all your subfunctions absorb and ignore all unexpected arguments. def subfunction_1(a=2, b=3, c=4, **_): return a+b*c def subfunction_2(d=5, e=6, f=7, **_): return d*e+f def main_function(**kw): return subfunction_1(**kw) + subfunction_2(**kw) This doesn't help with the signature, but it's an easy thing to do. You'd have to document your parameters separately from your functions; appropriate for something like the subprocess module (where a bunch of functions basically pipe their args straight into Popen), not appropriate for a lot of other places. On the plus side, both of these ideas work with existing Python versions, so you wouldn't have to wait for 3.8 or 3.9 :) ChrisA

Excellent point! I am not fully convinced by the syntax yet, but having proposed something is already very valuable and I do not have a better proposal. As I had to defer defaults countless times, and each times grumped about it, I hope something will come out of this...

That's a problem I've also encountered countless times. But the solution you propose only covers the use cases where we always pass the arguments. If your main function looks like the following, you wouldn't be able to set them as deferred: def main_function(action, a=2, b=3): if action== "product": return a * b if action== "complicated_action": return complicated_action(a=a, b=b) ... Maybe we could make deferred a keywork (only in this context), like this: def main_function(action, deferred a=2, deferred b=3): if action== "product": return a * b # will return 6 if action== "complicated_action": return complicated_action(a=a, b=b)# will return the same as complicated_action() ... deferred variables would just be classic variables some 'deferred' flag, which when they are passed to a function with a default value, are redefined to that value. And probably, when we use them (read them or assign a new value to them), they would lose this deferred flag. Le 20/07/2018 à 11:03, Peter O'Connor a écrit :

A crazy idea, didn't think much about it yet: def subfunc(c,a=0,b=1): Blabla def function(c,d=3, from args(subfunc) import a,b): Blabla return anotherfunc(a+b+c+d,subfunc(c,a,b))

Excellent contributions. I'm going to try to (partially) consolidate what we've got. REVIEW ======= I'll start by reviewing the situation regarding default arguments. There are two basic patterns for default arguments. The first is --- def fn(a=EXP): # body of function --- The second is --- def fn(a=None): if a is None: a = EXP # body of function --- Here, EXP is any Python expression. A fairly gotcha is to use a list, or some other mutable object, as EXP. This happens when you write --- def fn(a=[]): # body of function --- because then EXP = '[]' which will be evaluated just once, and every call fn() will be using the same list! To avoid this you should use the second pattern. I think there may be an example of this in the standard Python tutorial. (An aside. You probably need the second pattern if you EXP is, say, ([], []). Although technically immutable, this value has mutable members. And can't be hashed, or use as a dictionary key or element of a set.) WHEN TO USE None ================= If you want something mutable as the 'default argument' you have to use the second pattern. If your default argument is immutable then you can if you wish use the second pattern. But you don't have to use the second pattern. When you use the second pattern, the expression f(None) means 'create for me the default argument' (and raise an exception if there isn't one). Think about it. For immutable EXP, fn() is the same, whether fn is coded using the first pattern or the second. But the value of fn(None) depends very much on which pattern is used to code fn(). So here's the big conclusion (drum roll): === fn should be coded using the second pattern if we wish to pass None as a sentinel argument to fn. === SUMMARY ========= My suggestion was to use the second pattern to solve Peter O'Connor's original problem. It can be done now, but is a bit verbose, and looses useful information in help(fn). Brice Parent's suggestion was to introduce a keyword deferred, like so --- def fn(deferred a=EXP): # body of function --- which I like to think of as a syntactic shorthand for the second pattern. I sort of think we've now got a reasonable answer for Peter's problem. What do you think, Peter? And Brice, are you happy with my interpretation of your deferred keyword? --- Jonathan

On Fri, Jul 20, 2018 at 1:30 PM, Jonathan Fine <jfine2358@gmail.com> wrote:
I think the problem with the "None" approach (second pattern) is that it forces the writer of the subfunction to write their defaults in a more awkward way in anticipation of other functions which defer their defaults to it. Translated to the original example, it would become: def subfunction_1(a=None, b=None, c=None): if a is None: a=1 if b is None: b=2 if c is None: c=3 return a+b*c def subfunction_2(d=None, e=None, f=None): if d is None: d=5 if e is None: e=6 if f is None: f=7 return d*e+f def main_function(a=None, b=None, c=None, d=None, e=None, f=None): return subfunction_1(a=a, b=b, c=c) + subfunction_2(d=d, e=e, f=f) subfunction_1 may be written by someone totally different from the author of main_function, and may even be in a different codebase. For the author of subfunction_1, it makes no sense to use the "None" approach instead of python's normal default mechanism (since all arguments here are immutables). On Fri, Jul 20, 2018 at 1:30 PM, Jonathan Fine <jfine2358@gmail.com> wrote:

Hi Peter You make the very good point, that
Good point. To rephrase, what should we do if we want to use a third party or legacy function, which begins === def fn(a=1, b=2, c=3): # function body === We can solve this by defining a function decorator. Suppose we have a function fix_it, whose argument and return value are both functions. The basic specification of fix_it is that --- fixed_fn = fix_it(fn) --- is in practice equivalent to --- def fixed_fn(a=None, b=None, c=None): if a is None: a = 1 if b is None: b = 2 if c is None: c = 3 # function body for fn # or if you prefer return fn(a, b, c) --- An aside. We can code fix_it by using https://docs.python.org/3/library/inspect.html ===
You could also use with new code, like so: --- @fix_it def fn(a=1, b=2, c=3): # function body --- I think this helps solve your problem. Is there, Peter, anything else that would be left to do (except, of course, write the fix_it function). Thank you again for your problem and comments. -- Jonathan

Ah, right, the fix_it(fcn) is a nice idea. It might also be a good idea, if we're making an external library anyway, to have a "deferred" object to avoid overloading "None" (which may mean something else than "differ argument"). I implemented the decorator here <https://github.com/petered/peters_example_code/blob/master/peters_example_co...>, and it can be used as: from deferral import deferrable_args, deferred @deferrable_args def subfunction_1(a=2, b=3, c=4): return a+b*c @deferrable_args def subfunction_2(d=5, e=6, f=7): return d*e+f def main_function(a=deferred, b=deferred, c=deferred, d=deferred, e=deferred, f=deferred): return subfunction_1(a=a, b=b, c=c) + subfunction_2(d=d, e=e, f=f) assert main_function() == (2+3*4)+(5*6+7) assert main_function(a=8) == (8+3*4)+(5*6+7) I still think it would be nice to have this as a built-in python feature, for a few reasons: - When using non-differable functions (say in other codebases), we have to do a bunch of "func = deferrable_args(func)" at the top of the module (or we can just do them at runtime, but then we're doing inspection every time, which is probably slow). - It adds a layer to the call stack for every deferrable function you're in. - To avoid annoying errors where you've defined an arg as deferred but forgot to wrap the function in question. On Fri, Jul 20, 2018 at 3:39 PM, Jonathan Fine <jfine2358@gmail.com> wrote:

Hi Peter You wrote: On Fri, Jul 20, 2018 at 3:43 PM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
Oh, well done Peter. Thank you. About 15 lines of code, and the same again for comments. And all for the good of the community (and scratching your own itch). I think we've now got pretty much the right basic ideas for solving your original problem. And your original problem is fairly widespread. So what, do you think, are the next steps? -- Jonathan

On Fri, Jul 20, 2018 at 5:41 PM, Steven D'Aprano <steve@pearwood.info> wrote:
I don't know about these low level things, but I assume it'd be implemented in C and wouldn't have the same cost as entering a new function in Python. I imagine it just being a small modification of the mechanism that Python already uses to assign values to arguments when a function is called. Is that not the case? On Fri, Jul 20, 2018 at 5:41 PM, Steven D'Aprano <steve@pearwood.info> wrote:

On Fri, Jul 20, 2018 at 11:03:12AM +0200, Peter O'Connor wrote:
Perhaps you mean duplicate, or repeat, or copy. But surely they're not redefined -- then they would have different values. Being able to redefine the defaults in a wrapper function is a feature. Putting aside the terminology, I think this is a minor annoyance: DRY violations when setting default values.
Changing function defaults in production code without a period of deprecation and warnings is a no-no. Default values are a part of the function API, and changing the public API of a function without warning is asking for trouble. But during development, there are no such constraints (your only users are your development team) and it is a real nuisance keeping defaults in sync across multiple functions and/or classes. I've raised this issue in the past: https://mail.python.org/pipermail/python-list/2016-September/714546.html and I still don't have a great solution for it.
There are lots of other ways to solve this. None of which are great.
https://mail.python.org/pipermail/python-ideas/2011-July/010678.html -- Steve

On 20 July 2018 at 22:45, Steven D'Aprano <steve@pearwood.info> wrote:
FWIW, I tend to handle this problem the same way I handle other DRY problems with magic constants: give the default value a name and either export it directly, or export an API for retrieving it. If that results in name sprawl ("But now I have 15 defaults to export!"), then I take it as a hint that I may not be modeling my data correctly, and am missing a class definition or two somewhere. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

It might be stupid, but how about solving this problem using the following: from . import other_func, SomeClass def my_func(a=other_func.defaults.a, b=other_func.defaults.b, c=SomeClass.some_method.defaults.c): ... or def my_func(a=None, b=None, c=None): # or use some sentinel value instead of None if a is None: a = other_func.defaults.a if b is None: b = other_func.defaults.b if c is None: c = SomeClass.some_method.defaults.c ... or even def my_func(a=None, b=None, c=None): if a is None: a = default(other_func, "a") if b is None: b = default(other_func, "b") if c is None: c = default(SomeClass.some_method, "c") ... I used *.defaults.* but it might be something else, as well as the function I named 'default' which might be anything else. I prefer the first, as it's both short and easy to read, but I'm not sure about the implications about such a thing. And it probably has already been proposed for other use cases. The second and third versions are more verbose, but probably easier to implement, specially the third which should already be doable using something like import inspect def default(function, argument): return inspect.signature(function).parameters[argument].default.

I agree this is a problem, which I have seen solved by removing the method signature, which is unfortunate:
def flexible_method(**kwargs): # Read the code to find out the expected parameters
I have an @override decorator to handle this type of pattern. It will perform the null-coalescing with properties found in a special "kwargs" parameter. "kwargs" is assigned a dict that has a copy of the method arguments. The value of a callee's argument is, in order, * a not None value provided by the caller or * a not None value found in the kwargs dict or * the default value provided by the method declaration or * None I was not clear on where you wanted to define your defaults. Either like this:
or like this:
both are identical except for where you declare the default values. https://github.com/klahnakoski/mo-kwargs

https://github.com/petered/peters_example_code/blob/master/peters_example_co... Allowing to write: from deferral import deferrable_args, deferred @deferrable_args def f(x, y=2, z=3): return (x,y,z) f(5, deferred, 7) == (5,2,7) (I'd rename "deferrable_args" to simply "deferrable") The api chosen in deferall.py is a deferall.deferred, one could also use None or Ellipsis ? That looks nice : from deferral import elideferrable @elideferrable def f(x, y=2, z=3): return (x,y,z) f(5, ..., 7) == (5, 2, 7) from deferral import nonedeferrable @nonedeferrable def f(x, y=2, z=3): return (x,y,z) f(5, None, 7) == (5, 2, 7) Le mar. 24 juil. 2018 à 14:26, Kyle Lahnakoski <klahnakoski@mozilla.com> a écrit :

Hi Peter Interesting problem. We can already get something like your proposed solution by using None instead of deferred. You have to start with def subfunction_1(a=None, b=None, c=None): if a is None: a = 2 # similarly for b and c. return a+b*c You will loose the default values being shown when you do help(subfunction). There's another thread on PEP 505, active right now, that would allow you to write something like a = a OR 2 to provide the default values. https://mail.python.org/pipermail/python-ideas/2018-July/052071.html I hope this helps. Please let us know if that might work for you. -- Jonathan On Fri, Jul 20, 2018 at 10:03 AM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:

On Fri, Jul 20, 2018 at 7:03 PM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
Hmm. This might be something where a bit of a helper could, well, help. Is this a pattern that you make use of a lot? Suppose you write your main function like this: @combines(subfunction_1, subfunction_2) def main_function(sf1, sf2): return subfunction_1(**sf1) + subfunction_2(**sf2) or even like this: @precombine def main_function(subfunction_1, subfunction_2): return subfunction_1 + subfunction_2 The decorator could then generate a new function, with a proper signature (available to help()), that hides the messy details of filtering kwargs - and in the case of precombine, actually calls the function as well. (Obviously you can't use this if you might want to call a subfunction conditionally.) Another option would be to have all your subfunctions absorb and ignore all unexpected arguments. def subfunction_1(a=2, b=3, c=4, **_): return a+b*c def subfunction_2(d=5, e=6, f=7, **_): return d*e+f def main_function(**kw): return subfunction_1(**kw) + subfunction_2(**kw) This doesn't help with the signature, but it's an easy thing to do. You'd have to document your parameters separately from your functions; appropriate for something like the subprocess module (where a bunch of functions basically pipe their args straight into Popen), not appropriate for a lot of other places. On the plus side, both of these ideas work with existing Python versions, so you wouldn't have to wait for 3.8 or 3.9 :) ChrisA

Excellent point! I am not fully convinced by the syntax yet, but having proposed something is already very valuable and I do not have a better proposal. As I had to defer defaults countless times, and each times grumped about it, I hope something will come out of this...

That's a problem I've also encountered countless times. But the solution you propose only covers the use cases where we always pass the arguments. If your main function looks like the following, you wouldn't be able to set them as deferred: def main_function(action, a=2, b=3): if action== "product": return a * b if action== "complicated_action": return complicated_action(a=a, b=b) ... Maybe we could make deferred a keywork (only in this context), like this: def main_function(action, deferred a=2, deferred b=3): if action== "product": return a * b # will return 6 if action== "complicated_action": return complicated_action(a=a, b=b)# will return the same as complicated_action() ... deferred variables would just be classic variables some 'deferred' flag, which when they are passed to a function with a default value, are redefined to that value. And probably, when we use them (read them or assign a new value to them), they would lose this deferred flag. Le 20/07/2018 à 11:03, Peter O'Connor a écrit :

A crazy idea, didn't think much about it yet: def subfunc(c,a=0,b=1): Blabla def function(c,d=3, from args(subfunc) import a,b): Blabla return anotherfunc(a+b+c+d,subfunc(c,a,b))

Excellent contributions. I'm going to try to (partially) consolidate what we've got. REVIEW ======= I'll start by reviewing the situation regarding default arguments. There are two basic patterns for default arguments. The first is --- def fn(a=EXP): # body of function --- The second is --- def fn(a=None): if a is None: a = EXP # body of function --- Here, EXP is any Python expression. A fairly gotcha is to use a list, or some other mutable object, as EXP. This happens when you write --- def fn(a=[]): # body of function --- because then EXP = '[]' which will be evaluated just once, and every call fn() will be using the same list! To avoid this you should use the second pattern. I think there may be an example of this in the standard Python tutorial. (An aside. You probably need the second pattern if you EXP is, say, ([], []). Although technically immutable, this value has mutable members. And can't be hashed, or use as a dictionary key or element of a set.) WHEN TO USE None ================= If you want something mutable as the 'default argument' you have to use the second pattern. If your default argument is immutable then you can if you wish use the second pattern. But you don't have to use the second pattern. When you use the second pattern, the expression f(None) means 'create for me the default argument' (and raise an exception if there isn't one). Think about it. For immutable EXP, fn() is the same, whether fn is coded using the first pattern or the second. But the value of fn(None) depends very much on which pattern is used to code fn(). So here's the big conclusion (drum roll): === fn should be coded using the second pattern if we wish to pass None as a sentinel argument to fn. === SUMMARY ========= My suggestion was to use the second pattern to solve Peter O'Connor's original problem. It can be done now, but is a bit verbose, and looses useful information in help(fn). Brice Parent's suggestion was to introduce a keyword deferred, like so --- def fn(deferred a=EXP): # body of function --- which I like to think of as a syntactic shorthand for the second pattern. I sort of think we've now got a reasonable answer for Peter's problem. What do you think, Peter? And Brice, are you happy with my interpretation of your deferred keyword? --- Jonathan

On Fri, Jul 20, 2018 at 1:30 PM, Jonathan Fine <jfine2358@gmail.com> wrote:
I think the problem with the "None" approach (second pattern) is that it forces the writer of the subfunction to write their defaults in a more awkward way in anticipation of other functions which defer their defaults to it. Translated to the original example, it would become: def subfunction_1(a=None, b=None, c=None): if a is None: a=1 if b is None: b=2 if c is None: c=3 return a+b*c def subfunction_2(d=None, e=None, f=None): if d is None: d=5 if e is None: e=6 if f is None: f=7 return d*e+f def main_function(a=None, b=None, c=None, d=None, e=None, f=None): return subfunction_1(a=a, b=b, c=c) + subfunction_2(d=d, e=e, f=f) subfunction_1 may be written by someone totally different from the author of main_function, and may even be in a different codebase. For the author of subfunction_1, it makes no sense to use the "None" approach instead of python's normal default mechanism (since all arguments here are immutables). On Fri, Jul 20, 2018 at 1:30 PM, Jonathan Fine <jfine2358@gmail.com> wrote:

Hi Peter You make the very good point, that
Good point. To rephrase, what should we do if we want to use a third party or legacy function, which begins === def fn(a=1, b=2, c=3): # function body === We can solve this by defining a function decorator. Suppose we have a function fix_it, whose argument and return value are both functions. The basic specification of fix_it is that --- fixed_fn = fix_it(fn) --- is in practice equivalent to --- def fixed_fn(a=None, b=None, c=None): if a is None: a = 1 if b is None: b = 2 if c is None: c = 3 # function body for fn # or if you prefer return fn(a, b, c) --- An aside. We can code fix_it by using https://docs.python.org/3/library/inspect.html ===
You could also use with new code, like so: --- @fix_it def fn(a=1, b=2, c=3): # function body --- I think this helps solve your problem. Is there, Peter, anything else that would be left to do (except, of course, write the fix_it function). Thank you again for your problem and comments. -- Jonathan

Ah, right, the fix_it(fcn) is a nice idea. It might also be a good idea, if we're making an external library anyway, to have a "deferred" object to avoid overloading "None" (which may mean something else than "differ argument"). I implemented the decorator here <https://github.com/petered/peters_example_code/blob/master/peters_example_co...>, and it can be used as: from deferral import deferrable_args, deferred @deferrable_args def subfunction_1(a=2, b=3, c=4): return a+b*c @deferrable_args def subfunction_2(d=5, e=6, f=7): return d*e+f def main_function(a=deferred, b=deferred, c=deferred, d=deferred, e=deferred, f=deferred): return subfunction_1(a=a, b=b, c=c) + subfunction_2(d=d, e=e, f=f) assert main_function() == (2+3*4)+(5*6+7) assert main_function(a=8) == (8+3*4)+(5*6+7) I still think it would be nice to have this as a built-in python feature, for a few reasons: - When using non-differable functions (say in other codebases), we have to do a bunch of "func = deferrable_args(func)" at the top of the module (or we can just do them at runtime, but then we're doing inspection every time, which is probably slow). - It adds a layer to the call stack for every deferrable function you're in. - To avoid annoying errors where you've defined an arg as deferred but forgot to wrap the function in question. On Fri, Jul 20, 2018 at 3:39 PM, Jonathan Fine <jfine2358@gmail.com> wrote:

Hi Peter You wrote: On Fri, Jul 20, 2018 at 3:43 PM, Peter O'Connor <peter.ed.oconnor@gmail.com> wrote:
Oh, well done Peter. Thank you. About 15 lines of code, and the same again for comments. And all for the good of the community (and scratching your own itch). I think we've now got pretty much the right basic ideas for solving your original problem. And your original problem is fairly widespread. So what, do you think, are the next steps? -- Jonathan

On Fri, Jul 20, 2018 at 5:41 PM, Steven D'Aprano <steve@pearwood.info> wrote:
I don't know about these low level things, but I assume it'd be implemented in C and wouldn't have the same cost as entering a new function in Python. I imagine it just being a small modification of the mechanism that Python already uses to assign values to arguments when a function is called. Is that not the case? On Fri, Jul 20, 2018 at 5:41 PM, Steven D'Aprano <steve@pearwood.info> wrote:

On Fri, Jul 20, 2018 at 11:03:12AM +0200, Peter O'Connor wrote:
Perhaps you mean duplicate, or repeat, or copy. But surely they're not redefined -- then they would have different values. Being able to redefine the defaults in a wrapper function is a feature. Putting aside the terminology, I think this is a minor annoyance: DRY violations when setting default values.
Changing function defaults in production code without a period of deprecation and warnings is a no-no. Default values are a part of the function API, and changing the public API of a function without warning is asking for trouble. But during development, there are no such constraints (your only users are your development team) and it is a real nuisance keeping defaults in sync across multiple functions and/or classes. I've raised this issue in the past: https://mail.python.org/pipermail/python-list/2016-September/714546.html and I still don't have a great solution for it.
There are lots of other ways to solve this. None of which are great.
https://mail.python.org/pipermail/python-ideas/2011-July/010678.html -- Steve

On 20 July 2018 at 22:45, Steven D'Aprano <steve@pearwood.info> wrote:
FWIW, I tend to handle this problem the same way I handle other DRY problems with magic constants: give the default value a name and either export it directly, or export an API for retrieving it. If that results in name sprawl ("But now I have 15 defaults to export!"), then I take it as a hint that I may not be modeling my data correctly, and am missing a class definition or two somewhere. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

It might be stupid, but how about solving this problem using the following: from . import other_func, SomeClass def my_func(a=other_func.defaults.a, b=other_func.defaults.b, c=SomeClass.some_method.defaults.c): ... or def my_func(a=None, b=None, c=None): # or use some sentinel value instead of None if a is None: a = other_func.defaults.a if b is None: b = other_func.defaults.b if c is None: c = SomeClass.some_method.defaults.c ... or even def my_func(a=None, b=None, c=None): if a is None: a = default(other_func, "a") if b is None: b = default(other_func, "b") if c is None: c = default(SomeClass.some_method, "c") ... I used *.defaults.* but it might be something else, as well as the function I named 'default' which might be anything else. I prefer the first, as it's both short and easy to read, but I'm not sure about the implications about such a thing. And it probably has already been proposed for other use cases. The second and third versions are more verbose, but probably easier to implement, specially the third which should already be doable using something like import inspect def default(function, argument): return inspect.signature(function).parameters[argument].default.

I agree this is a problem, which I have seen solved by removing the method signature, which is unfortunate:
def flexible_method(**kwargs): # Read the code to find out the expected parameters
I have an @override decorator to handle this type of pattern. It will perform the null-coalescing with properties found in a special "kwargs" parameter. "kwargs" is assigned a dict that has a copy of the method arguments. The value of a callee's argument is, in order, * a not None value provided by the caller or * a not None value found in the kwargs dict or * the default value provided by the method declaration or * None I was not clear on where you wanted to define your defaults. Either like this:
or like this:
both are identical except for where you declare the default values. https://github.com/klahnakoski/mo-kwargs

https://github.com/petered/peters_example_code/blob/master/peters_example_co... Allowing to write: from deferral import deferrable_args, deferred @deferrable_args def f(x, y=2, z=3): return (x,y,z) f(5, deferred, 7) == (5,2,7) (I'd rename "deferrable_args" to simply "deferrable") The api chosen in deferall.py is a deferall.deferred, one could also use None or Ellipsis ? That looks nice : from deferral import elideferrable @elideferrable def f(x, y=2, z=3): return (x,y,z) f(5, ..., 7) == (5, 2, 7) from deferral import nonedeferrable @nonedeferrable def f(x, y=2, z=3): return (x,y,z) f(5, None, 7) == (5, 2, 7) Le mar. 24 juil. 2018 à 14:26, Kyle Lahnakoski <klahnakoski@mozilla.com> a écrit :
participants (9)
-
Brice Parent
-
Chris Angelico
-
Grégory Lielens
-
Jonathan Fine
-
Kyle Lahnakoski
-
Nick Coghlan
-
Peter O'Connor
-
Robert Vanden Eynde
-
Steven D'Aprano