On Sun, Oct 24, 2021 at 1:00 PM Steven D'Aprano
On Sun, Oct 24, 2021 at 06:54:36AM +1100, Chris Angelico wrote:
[...]
Teaching moment. Currently, the equivalent second function would be this:
def f2(l=None): if l is None: l = [] l.append(4) return l
And the whole "early bind or late bind" question is there just the same; the only difference is that the late binding happens somewhere inside the function body, instead of being visible as part of the function's header. (In this toy example, it's the very next line, which isn't a major problem; but in real-world examples, it's often buried deeper in the function, and it's not obvious that passing None really is the same as passing the array's length, or using a system random number generator, or constructing a new list, or whatever it is.)
I challenge that assertion. I've never knowingly seen a function where the late binding is "buried deeper in the function", certainly not deep enough that it is not obvious. It is a very strong convention that such late binding operations occur early in the function body.
You know, before you use the parameter, not afterwards *wink*
But then I mostly look at well-written functions that are usually less than two, maybe three, dozen lines long, with a managable number of parameters.
If you are regularly reading badly-written functions that are four pages long, with fifty parameters, your experience may differ :-)
The bisect function you gave earlier is a real-world example of a non-toy function. You will notice that the body of bisect_right:
- does the late binding early in the body, immediately after checking for an error condition;
- and is a manageable size (19 LOC).
What I'm more often seeing is cases that are less obviously a late-binding, but where the sentinel is replaced with the "real" value at the point where it's used, rather than up the top of the function.
https://github.com/python/cpython/blob/3.10/Lib/bisect.py
The bisect module is also good evidence that this proposal may not be as useful as we hope. We have:
def insort_right(a, x, lo=0, hi=None, *, key=None):
which just passes the None on to bisect_right. So if we introduced optional late-binding, the bisect module has two choices:
- keep the status quo (don't use the new functionality);
- or violate DRY (Don't Repeat Yourself) by having both functions duplicate the same late-binding.
It's only a minor DRY violation, but still, if the bisect module was mine, I wouldn't use the new late-binding proposal.
So I think that your case is undermined a little by your own example.
The truth is, though, that the default for hi is not None - it's really "length of the given list". Python allows us to have real defaults for parameters, rather than simply leaving trailing parameters undefined as JavaScript does; this means that you can read off the function header and see what the meaning of parameter omission is. Late-binding semantics allow this to apply even if the default isn't a constant. If this proposal is accepted, I would adopt the DRY violation, since it would literally look like this: def insort_right(a, x, lo=0, hi=>len(a), *, key=None): def bisect_right(a, x, lo=0, hi=>len(a), *, key=None): def insort_left(a, x, lo=0, hi=>len(a), *, key=None): def bisect_left(a, x, lo=0, hi=>len(a), *, key=None): That's not really a lot of repetition, and now everyone can see that the default lo is 0 and the default hi is the length of a. Four "hi=>len(a)" isn't really different from four "hi=None" when it comes down to it. ChrisA