On Thu, Dec 9, 2021 at 7:07 PM Brendan Barnwell <brenb...@brenbarn.net> wrote:
>
> On 2021-12-08 23:12, Chris Angelico wrote:
> > On Thu, Dec 9, 2021 at 5:52 PM Brendan Barnwell <brenb...@brenbarn.net> 
> > wrote:
> >>         To try stating this in yet another way, currently if I have:
> >>
> >> def f(a=<some code here>)
> >>
> >>         <some code here> must be something that evaluates to a first-class
> >> object, and the "argument default" IS that first-class object --- not
> >> bytecode to generate it, not some behavior that evaluates the
> >> expression, no, the default value is itself an object.  This would not
> >> be the case for late-bound defaults under the PEP.  (Rather, as I
> >> phrased it in another post, there would not "be" a late-bound default at
> >> all; there would just be some behavior in the function to do some stuff
> >> when that argument isn't passed.)
> >
> > The VALUE is a first-class object - that's the result of evaluating
> > the expression. With early-bound defaults, that's the only thing that
> > gets saved - not the expression, just the resulting value. (Which can
> > be seen if you do something like "def f(x=0x100):", which will show
> > the default as 256.)
>
>         Right, but that's what I'm saying.  To me it is not a default unless
> there is a value that gets saved.  Otherwise it is just behavior in the
> function.
>
> > Remember, a late-bound default is most similar to this code:
> >
> > def f(a=<optional>):
> >      if a was omitted: a = <some code here>
> >
> > And in that form, the code isn't available as a first-class object.
> > That's why I say that it is parallel to every other partial expression
> > in Python. Until you evaluate it, there is no first-class object
> > representing it. (A code object comes close, but it's more than just
> > an expression - it also depends on its context. A function requires
> > even more context.)
>
>         Yes, but that's the point.  To me that code is quite a different 
> matter
> from "a late-bound default" as I conceive it.  I get the impression that
> you really do see that code as "a late-bound default" but to me it is
> not at all.  It just behavior in the function.  It's true that the
> result is to assign a certain value to the variable, but that alone
> doesn't make it "a default" to me.

Fair enough. To me, it's all defaults; or rather, the only thing that
is truly an aspect of the function is whether the parameter is
mandatory or optional.

In a sense, that's all you need. You could write every function to
simply have mandatory parameters and optional parameters, and then
have everything done as "behaviour in the function". Having the open
mode default to "r" is really just the function's behaviour - if you
omit the parameter, it's going to do this.

Function default arguments are a convenience for the common cases, and
also an aid to documentation. For instance:

str.encode(self, /, encoding='utf-8', errors='strict')

If you just call s.encode(), you know exactly what it'll do. And if
you call s.encode("ISO-8859-1"), you know that it'll assume strict
error handling. This is a good thing. But in a sense, we could just
write it as:

str.encode(self, /, [encoding], [errors])

and leave the rest in the body.

What defines what belongs in the body and what belongs in the signature?

> 1. A function is distinct from other kinds of expression in that some
> things happen when you define it, and it also "saves" some things for
> later when you call it.

A lot of things. It knows its context, for instance. A function isn't
just a block of code or an expression - it's a thing that exists in a
particular world.

> 2. When you define it, it saves two things: some code to be run when
> it's called (i.e., the function body) and some values to be used if some
> arguments aren't provided.  (It probably saves some other stuff too like
> the docstring but these are the relevant ones for our purposes.)  It
> stores those values with a mapping to their corresponding arguments
> (that is if you do `def f(a=1, b=2)` it stores that 1 goes with a and 2
> with b).

A lot is saved when you compile it, which happens before it's defined.
At definition time, I believe that all it has to do is gather the
previously-saved things, prepare the defaults, and save the context
(closure cells) if required.

(At a technical level, positional and pos-or-kwd arguments are stored
in a tuple, kwonly are stored in a dict. But same difference.)

> 3. Those values that are saved to be used later are the argument
> defaults.  That's it.  The only thing that can "be an argument default"
> is a thing that is saved when the function is defined and is (maybe)
> retrieved later when it's called.  Everything that isn't a value is
> BEHAVIOR.  You can do other things to the function at def time (like
> replace it with another one using a decorator, effectively augmenting it
> somehow) but argument defaults are values, they're not behavior.

That clearly defines the way things currently are. Is that actually
how things must be, or only how it is?

>         From that perspective, there is all the difference in the world 
> between
> what we currently have, which you apparently think of as sort of a
> "manual" late-bound default, and a real late-bound default, which would
> be a value that is stored at def time.  If the effect of writing the
> signature a certain way (e.g., `=>[]` instead of `=[]`) is not to store
> a value but to somehow manipulate the bytecode of the function body, I
> don't consider that an argument default; it's a behavioral modification
> more akin to a decorator that wraps the original function.

If it is storing a value, it's manipulating the bytecode of the
surrounding function. Code has to go somewhere.

>         Part of the reason I feel this way is because what we currently have 
> is
> in no way restricted to specifying default values.  What if I have this:
>
> def f(a=<optional>):
>      if a was omitted and random.random() < 0.5: a = <some code here>
>
> . . . or perhaps more realistically:
>
> def f(a=<optional>, b=<optional>, c=<optional>):
>         if a was omitted and 0 < b <= 5 and not c.some_boolean_attr:
>                 a = <some code here>
>
>         Now what "is the default"?  Is there one?  There is no clear
> distinction between code in the function body that defines a "late-bound
> default" and code that just does something else.  In the former case the
> behavior is random.  In the latter case it may be that you can say in
> English what the "default" is, but I don't consider that an ARGUMENT
> default.

And in cases like these, it probably shouldn't be put in the default,
because it is just behaviour. Of course you *could* cram that into the
function signature, but it probably doesn't belong. Hard to say,
though, without a real example.

Some things are function behaviour. Others are argument defaults. The
ones that are argument defaults should go in the signature; the ones
that aren't should go in the body. At the moment, there's a technical
limitation that means that "new empty list" cannot be spelled as an
argument default, and therefore we need workarounds. That's the only
part that I want to change.

Remember, when the walrus operator was introduced, it wasn't meant to
replace all assignment. When list comprehensions were brought in, they
weren't meant to replace all lists. When match/case was added to the
language, it wasn't intended to supplant all if/elif trees. In each
case, a feature exists for the situations where it is more expressive
than the alternatives, and for other cases, don't use it.

> It may be default BEHAVIOR of the FUNCTION to decide in a
> certain way what to assign to that local variable, but that's not a
> default "of the argument", it's part of the function's defined behavior
> like anything else.  In order for me to consider it an argument default,
> it has to have some independent status as a "thing" that is individually
> associated with the argument, not simply rolled into the bytecode of the
> function as a whole.  For instance, this function has default behavior too:
>
> def f(a=<optional>):
>         if a was omitted:
>                 download_a_file_from_the_internet()
>         else:
>                 dont_download_anything()
>
>         But the behavior doesn't suddenly become "a default" just because the
> code happens to assign a value to a.

Correct. Behaviour doesn't become an argument default. But argument
defaults can be crammed into function behaviour if, for a technical
reason, they can't be spelled as defaults. If it's wrong to cram
behaviour into the signature, isn't it just as wrong to stuff the
default into the body of the function?

>         In my conception you can't specify an argument default by means of
> modifications to the function body, because the function body is
> arbitrary code that can do anything.  The different between "an argument
> default" and "stuff that the function does as part of its behavior" is
> that the argument default is segmented out and has its own independent
> existence.

Precisely. That is exactly the distinction. Of course, when there are
technical limitations, sometimes things have to go into other places,
but ideally, the argument defaults should be segmented out and given
their correct position in the signature.

ChrisA
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@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/YON6L7B5IFDNMH36FMXJ3ND3RPYI7ZM2/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to