I’m still not clear if this is a disagreement about something more than terminology, but as I understand it, other languages that have non-constant defaults use late binding, and call them “defaults”. 

It seems to be a well accepted term.

-CHB




On Thu, Dec 9, 2021 at 12:45 AM Brendan Barnwell <brenbarn@brenbarn.net> wrote:
On 2021-12-08 23:22, Chris Angelico wrote:
> On Thu, Dec 9, 2021 at 5:54 PM Brendan Barnwell <brenbarn@brenbarn.net> wrote:
>>
>> On 2021-12-08 20:36, Chris Angelico wrote:
>> > Remember, though: The comparison should be to a function that looks like this:
>> >
>> > def f(a=[], b=_SENTINEL1, c=_SENTINEL2, d=_SENTINEL3):
>> >      if b is _SENTINEL1: b = {}
>> >      if c is _SENTINEL2: c = some_function(a, b)
>> >      if d is _SENTINEL3: d = other_function(a, b, c)
>> >
>> > If you find the long-hand form more readable, use the long-hand form!
>> > It's not going away. But the introspectability is no better or worse
>> > for these two. The late-bound defaults "{}", "some_function(a, b)",
>> > and "other_function(a, b, c)" do not exist as objects here. Using PEP
>> > 671's syntax, they would at least exist as string constants, allowing
>> > you to visually see what would happen (and, for instance, see that in
>> > help() and inspect.signature).
>>
>>         I don't want to get bogged down in terminology but I am becoming
>> increasingly frustrated by you using the term "default" both for things
>> that are values and things that are not, as if there is no difference
>> between them.
>
> That's absolutely correct: I am using the term "default" for anything
> that provides a default for an optional argument that was omitted. In
> some cases, they are default values. In other cases, they are default
> expressions. If your docstring says "omitting d will use the length of
> a", then the default for d is len(a).

        Your definition is somewhat circular, because you say that a default is
"anything that provides a default".  But that says "default" again.  So
what is a default?

        By your definition, any arbitrary code inside a function body that
eventually assigns something to an argument name is a default.  (It is
not clear to me whether you would consider some code a default if it may
or may not assign a value to an argument, depending on some conditions.)
  So I don't agree with that definition.  That can be default BEHAVIOR,
but it is function behavior; it is not an argument default.

>>  There are no late-bound defaults here, in the sense that
>> I mean, which as I said before has to do with default VALUES.  There is
>> just code in the function body that does stuff.  I am fine with code in
>> a function body doing stuff, but that is the purview of the function and
>> not the argument.  An individual ARGUMENT having a default VALUE is not
>> the same as the FUNCTION defining BEHAVIOR to deal with a missing value
>> for an argument.
>
> In a technical sense, the default value for b is _SENTINEL1, but would
> you describe that in the docstring, or would you say that omitting b
> would use a new empty dictionary? You're getting bogged down, not in
> terminology, but in mechanics. At an abstract level, the default for
> that argument is whatever would be used if the argument is omitted.

        I don't agree.  At an abstract level, there is no clear dividing line
between what you call an argument default and just "arbitrary behavior
of the function".  What if "what would be used" depends on random
numbers or data from some external source?

        Or, again, what you are describing is not an argument default (in my
conception).  It may be the BEHAVIOR of the function to do a certain
thing (like use a certain value in place of an omitted argument) but
unless that behavior is segmented and associated with the argument
itself (not merely part of the function's code flow) I don't consider it
an argument default.

        As for the docstring, yes, I might well mention _SENTINEL1 in the
docstring.  I certainly wouldn't see anything wrong with that.  That's
what the default is.  Again, the function may USE that value in some
way, but that doesn't mean that's not what the default is; it just means
the function conditions its behavior on its argument value, as any
function may do on any argument value, omitted or not.  I get the
impression you think that in a case like that the default "really is"
something else defined in the body of the function, but again I
disagree.  The default really is _SENTINEL1.  Conceptually we may
understand that the function will use that value in a special way, but
that is not really any different than understanding that passing "r" to
open() will open the file for reading while passing "w" will open it for
writing.  It's just that to know how to use a function you need to know
more than the default values of the arguments; you need to know what
they MEAN, and (at least with current technology :-) we have no way of
deriving that from the source code.

        You're quite right that "at an abstract level" it may be the case that
the default behavior is to do a certain thing, but I guess one way to
state my position would be that I think that is TOO abstract of a level
to worry about representing in code.  At an abstract level I may say
"this function computes the number of paths of length N between the
given nodes in the given graph", but I don't expect that to be mentioned
in the signature or automatically provided in the docstring.  I would
certainly WRITE it in the docstring, but I don't expect Python to deduce
that "abstract" level of meaning from code annotations and write that
docstring for me.

        In other words, I think mechanics is the right level to be at here.  We
cannot hope to capture the abstract level that you're describing, and I
think doing so will just muddle matters.  At an abstract level we say
""this function computes the number of paths of length N between the
given nodes in the given graph" but what we write is `def n_paths(graph,
node1, node2)`.  I don't see any reason we need to be able to write
`len(x)` in the function signature just because at that abstract level
we think of it as something that may be computed later.  This is
especially so because, as I mentioned above, there is no clear line
separating "code that we can write in a function to assign a default
value to an argument" and "code we can write in a function for other
purposes" --- and thus there is no way to distinguish behavior that is
"tied" to a particular argument from just code that uses any old
combination of values it wants.

        We write code in terms of instrumental units which necessarily are at a
slightly more concrete level than the purely abstract or conceptual
realm of "what this function does".  For instance, objects (which, until
now, every function argument, default or not, is).  I don't see any
reason why late-bound defaults should be represented in code in a way
that attempts to capture this abstract level when other aspects of
functions are not and cannot be.

> To justify this, please explain WHY it is so important for defaults to
> all be objects. Not just "that's how they are now", but why that is an
> important feature.

        Because I'm used to reasoning about Python code in terms of operations
on objects, and so are a lot of other people.  Everything I or anyone
else currently needs to know about how functions and their arguments
work in Python can be thought of in terms of objects.  Why add a new
complication?  I mean, okay, maybe that is really just saying "that's
how they are now", although it's more like "right now defaults are part
of the big set of things that are objects and this change would peel
them off and create a new type of thing".

        But apart from that, I think part of what makes Python a nice language
is the way that many language functions are represented in terms of
objects, for instance the iterator and descriptor protocols.  The idea
of the object as a locus of functionality --- that the way you "do
something" (like loop or access an attribute) is represented as "get an
object representing the functionality and call certain methods on it"
--- gives unity to many Python features.  It's true that's a pretty
abstract reason, but I think it's a legit one.

        Also, let's remember that burden of evidence is really the other way
around here.  Can you really explain WHY it is so important for
late-bound defaults to be represented with special syntax of the type
you propose?  Not only can you not rely on "that's how they are now"
(because they're not), but in fact you must overcome the reverse
argument, namely that people have been doing pretty well with just
early-bound defaults for decades now.  In other words, even if it is not
particularly important for defaults to be objects, it may still be more
important than being able to write a "late-bound default" (aka "behavior
in the function body") in the signature.

--
Brendan Barnwell
"Do not follow where the path may lead.  Go, instead, where there is no
path, and leave a trail."
    --author unknown
_______________________________________________
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/EXDGAZCHULMFAIP4ARLILRQF76GIILWF/
Code of Conduct: http://python.org/psf/codeofconduct/
--
Christopher Barker, PhD (Chris)

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