
On 25.10.2021 15:44, Chris Angelico wrote:
On Mon, Oct 25, 2021 at 11:53 PM Marc-Andre Lemburg <mal@egenix.com> wrote:
On 25.10.2021 14:26, Chris Angelico wrote:
On Mon, Oct 25, 2021 at 11:20 PM Marc-Andre Lemburg <mal@egenix.com> wrote:
On 25.10.2021 13:53, Chris Angelico wrote:
On Mon, Oct 25, 2021 at 10:39 PM Marc-Andre Lemburg <mal@egenix.com> wrote:
I would prefer to not go down this path.
"Explicit is better than implicit" and this is too much "implicit" for my taste :-)
For simple use cases, this may save a few lines of code, but as soon as you end up having to think whether the expression will evaluate to the right value at function call time, the scope it gets executed in, what to do with exceptions, etc., you're introducing too much confusion with this syntax.
It's always possible to be more "explicit", as long as explicit means "telling the computer precisely what to do". But Python has default arguments for a reason. Instead of simply allowing arguments to be optional, and then ALWAYS having code inside the function to provide values when they are omitted, Python allows us to provide actual default values that are visible to the caller (eg in help()). This is a good thing. Is it "implicit"? Yes, in a sense. But it's very clear what happens if the argument is omitted. The exact same thing is true with these defaults; you can see what happens.
The only difference is whether it is a *value* or an *expression* that defines the default. Either way, if the argument is omitted, the given default is used instead.
I guess I wasn't clear enough. What I mean with "implicit" is that execution of the expression is delayed by simply adding a ">" to the keyword default parameter definition.
Given that this alters the timing of evaluation, a single character does not create enough attention to make this choice explicit.
If I instead write:
def process_files(processor, files=deferred(os.listdir(DEFAULT_DIR))):
def process_files(processor, files=deferred("os.listdir(DEFAULT_DIR)")):
@deferred(files="os.listdir(DEFAULT_DIR)")
Ahhh, okay. Now your explanation makes sense :)
This does deal with the problem of function calls looking like function calls. It comes at the price of using a string to represent code, so unless it has compiler support, it's going to involve eval(), which is quite inefficient. (And if it has compiler support, it should have syntactic support too, otherwise you end up with weird magical functions that don't do normal things.)
The decorator version would not need eval, since the decorator would actually rewrite the function to include the parameter defaulting logic right at the top of the function and recompile it. For the object version, the string would have to be compiled as well and then executed at the top of the function somehow :-) I think for the latter, we'd need a more generic concept of deferred execution in Python, but even then, you'd not really save typing: def process_files(processor, files=defer os.listdir(DEFAULT_DIR)): if deferred(files): files = eval(files) ... The details are more complex than the above, but it demonstrates the idea. Note that eval() would evaluate an already compiled expression encapsulated in a deferred object, so it's not slow or dangerous to use. Now, it may not be obvious, but the key advantage of such deferred objects is that you can pass them around, i.e. the "defer os.listdir(DEFAULT_DIR)" could also be passed in via another function.
It's also extremely verbose, given that it's making a very small difference to the behaviour - all it changes is when something is calculated (and, for technical reasons, where; but I expect that intuition will cover that).
It is verbose indeed, which is why I still think that putting such code directly at the top of the function is the better way to go :-)
That's what I want to avoid though. Why go with the incredibly verbose version that basically screams "don't use this"? Use something much more akin to other argument defaults, and then it looks much more useful.
That's fair, but since the late binding code will have to sit at the top of the function definition anyway, you're not really saving much.
def add_item(item, target=>[]):
vs.
def add_item(item, target=None): if target is None: target = []
It doesn't always have to sit at the top of the function; it can be anywhere in the function, including at the use site. More importantly, this is completely opaque to introspection. Tools like help() can't see that the default is a new empty list - they just see that the default is None. That's not meaningful, that's not helpful.
You'd typically write about those defaults in the doc string, though. At least that's how I document such more involved defaults.
It also pollutes the API with a fake argument value, such that one might think that passing None is meaningful. If, in the future, you change your API to have a unique sentinel object as the default, people's code might break. Did you document that None was an intentional parameter option, or did you intend for the default to be "new empty list"? For technical reasons, the default currently has to be a single value, but that value isn't really meaningful. A function's header is its primary documentation and definition, and things should only have perceived meaning when they also have real meaning. (Case in point: positional-only args, where there is no keyword argument that can masquerade as that positional arg. Being forced to name every argument is a limitation.)
None in this case is used as sentinel, because you are expecting a sequence, so it's clear that None doesn't work as a valid argument. The discussion around a separate standard sentinel, which can never be used as valid argument, is what brought us to this thread, AFAIR :-)
The purpose of late-evaluated argument defaults (I'm wondering if I should call them LEADs, or if that's too cute) is to make the function's signature truly meaningful. It shouldn't be necessary to warp your code around a technical limitation.
That's a good argument, but with e.g. the deferred objects, you could have the same: simply make the repr(deferred object) include the expression, e.g. "deferred(os.listdir(DEFAULT_DIR))". help() would then output this when printing out the function signature. Ditto for the decorator, since this could add generated text to the doc-string of the function. -- Marc-Andre Lemburg eGenix.com Professional Python Services directly from the Experts (#1, Oct 26 2021)
Python Projects, Coaching and Support ... https://www.egenix.com/ Python Product Development ... https://consulting.egenix.com/
::: We implement business ideas - efficiently in both time and costs ::: eGenix.com Software, Skills and Services GmbH Pastor-Loeh-Str.48 D-40764 Langenfeld, Germany. CEO Dipl.-Math. Marc-Andre Lemburg Registered at Amtsgericht Duesseldorf: HRB 46611 https://www.egenix.com/company/contact/ https://www.malemburg.com/