For any & every

Sigh. It was hidden in a collapsed-by-default “Examples” section (that I didn’t expand, as I wanted to read the spec, not try to determine it from a series of examples).

So yes, my bad. Although my point was about presentation, and hiding the explanation of the semantics is a pretty bad way of presenting an idea :slightly_frowning_face:

1 Like

I’m trying my best:

  • I’m no longer posting suggestions that can be put on PyPi
  • I’m no longer suggesting alternative ways to write the same thing that don’t offer clear benefits
  • I’m no longer proposing typing suggestions
  • And I followed your steps when writing this up.

But the thing is, I have autism, so I don’t always act logically.


I feared you might interpret that incorrectly. I was simply replying to @barneygale, quoting the relevant parts of your comment. Sorry if you thought I was attacking you.


Sorry, the section got a bit long like “Other discussions about comprehensions”. So I wanted to hide it to decrease clutter. But that also works by showing it by default and allowing to collapse it. (I hope you’re fine with that edit).

I don’t use autism as an excuse.

1 Like

That’s not intended as an excuse, it’s intended as an explanation of why I’m struggling with this.

It comes across as one, since you’re explaining why you’re struggling and then not changing your behaviour.

I’m done talking to you. You’ve been told the same things enough times now that it’s a waste of effort replying to you, and a waste of time reading your posts.

1 Like

If it helps we could start with a simplified syntax:

unquantified_comprehension ::= assignment_expression comp_unquantified_for
quantified_comprehension   ::= assignment_expression comp_quantified_for
comp_unquantified_for      ::= ["async"] "for" target_list "in" or_test
                               [comp_iter]
comp_quantified_for        ::= ["async"] "for" quantifier target_list "in"
                               or_test
quantifier                 ::= any | each
comp_iter                  ::= comp_unquantified_for | comp_if
comp_if                    ::= "if" or_test [comp_iter]

Just to clarify, that’s not a loop, correct? It simply returns True or False?

It’s not a regular loop, it returns a boolean. I didn’t use “all” because grammatically it should be followed by a plural: “for all values”. It would only work for single letters: “for all x”.

I appreciate that, and honestly I’m still trying because you do seem willing to learn and I want to give you the benefit of the doubt. But at some point, as others have said, people simply don’t have the time or energy to keep trying to extract the intent from what you’re posting. We’re all doing this in our free time, and we only have so much of that.

Also, I think you’re taking the wrong message at times. No-one’s saying you shouldn’t suggest things that might have a home on PyPI, or that you shouldn’t get involved in typing. But you need to understand what’s involved in proposals in those areas if you want to. (And maybe avoiding those areas is a reasonable choice, for now at least, if you don’t have confidence that you can put together a good proposal in those areas).

No worries, getting the tone right in messages is notoriously hard. We all get things wrong at times so having a reasonably thick skin is useful :wink:

Oh boy, you’re talking to someone who always writes far too long posts. Working out how to get your point across without losing it in pages of text is hard. And it takes a lot of effort - some of the posts I make can take hours to compose. And they are still too long, and people miss the point I was trying to make…

The quote “Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”[1] is spot on here. Unfortunately, it’s incredibly hard to achieve…

Sigh. You’ve had people say “don’t edit posts” enough by now that you should already know the answer to that. No. Just don’t edit posts. I was perfectly happy that @FelixFourcolor pointed out what I’d missed, and future readers had the post to let them know if they made the same mistake as me. Editing your post just makes the whole exchange between us confusing to people reading this thread later.

I don’t see it as an excuse, and I haven’t seen any sign that @Nineteendo is asking for any concessions here. As they said, it’s just an explanation (which I certainly found helpful - I’ve tried to word my comments with it in mind).

But that’s the problem here. @Nineteendo has stated some changes they’ve made in their behaviour, but they aren’t the ones we’re asking for. I’m still giving the benefit of the doubt here and assuming misunderstanding rather than stubbornness, but it is getting harder to assume that.

Specifically, @Nineteendo - just don’t edit posts. If you can’t do that, with all the clear statements that have been made, there’s no hope of any more nuanced advice on other matters getting through to you :slightly_frowning_face:

It doesn’t help. Your syntax is clear. I’ve been pointed at the semantics now. It’s taken 28 messages, and way too much time, but your proposal is explained.

The problem now is that (IMO) your proposal sucks (sorry to be blunt, but I want to avoid getting stuck in further misunderstandings).

  • It looks like a loop (because that’s what the for keyword means in every other context in Python) but it’s not - it’s a scalar expression.
  • It’s new syntax whose only purpose is to provide another way of spelling an optimisation that can already be achieved in Python with an explicit for loop.
  • In spite of the fact that you quote the “there should be one way” Zen, what you are actually doing is adding an extra way to do something, and your way isn’t even particularly obvious. So you’re actually violating the Zen you claim to be following.

I don’t intend to get into further debate on this proposal. It’s taken this long to understand what you were proposing, and IMO there’s little or no chance of it getting accepted. And I simply don’t have the energy to spend any more time on this proposal. The above is my opinion, so you’re free to agree or disagree with it, but you should assume that as a core developer I do have a certain level of understanding of what makes a good addition to Python[2].


  1. Antoine de Saint-Exupéry, Airman’s Odyssey ↩︎

  2. Even if you disagree with me, you should find some core developer whose advice you’re willing to take, and @barneygale and I are the only ones who’ve shown any interest in this thread so far. Neither of us like it. ↩︎

10 Likes

Also, regular readers on the forum who have read the discussion will not
see your edits either, because they have no reason to go back to see
if something’s been changed.

Please just post a new message in the thread with the revised
syntax/whatever.

I only edit my own posts to fix basic typos, which I make quite a few
of. But if I’m talking about a semantic change, that warrants a new
post.

2 Likes

This is an alternative suggestion.

I was reading the somewhat heated thread here:

where @Nineteendo noticed PR #118946
which replaces an any() with an explicit loop for performance reasons.

Their (quite detailed) suggestion was a comprehension/generator-like
syntax which eefectively did an early return. The core reaon for the
syntax was that any() and all() are ordinary functions, and because
of that the Python compiler cannot optimise them.

There were various objections, which mostly seemed to be around:

  • this is very special case
  • the syntax still looks like a loop but returns a scalar

So, I was thinking: when do we discard following values? In unpacking
assignments. This is not uncommon:

 a, b, *_ = some-iterable-expression-here

Now, _ is just a regular variable name, and the tail end of the
iterable gets plunked into it. This consumes (and thus executes) the
entire iterable. And we can’t really change that without breaking
backwards compatibility.

However, what about allowing:

 a, b, * = some-iterable-expression-here

to mean run the iterable for the first 2 values, and do not run it any
further
? If the iterable were a generator expression the compile then
has direct access to implement something performant.

This doesn’t quite do what Nice Zombies targeted, but it might do with a
little more syntax.

The above works for some fixed number of values but the general case was
“run until some condition”. List comprehensions and generator
expressions already have the if-condition suffix to select perticular
values. What if they also accepted a while-condition to continue the
run?

 expr(x) for x in something-iterable while test(x)

This is itertools.takewhile supported at the language level.

But the OP’s suggestion targeted this code:

 return any(key in m for m in self.maps)

where you basicly need to run until you find a true value. Which
suggests a different additional elaboration of generator expressions:

 expr(x) for x in something-iterable until test(x)

That would let you write:

 found_keys = key for key in m until key in m
 return bool(found_keys)

Thoughts?

1 Like

I don’t think that means you can’t get performance improvements with the existing syntax, though. For example, the compiler could take a call to a function named any and check if it is the builtin one. If it is not, then it would call it as normal, but if it is, it could instead execute an inlined version. That would likely give you most of the performance improvement that the new syntax would.

Now, I don’t know if that’s the best approach, because I’d tend to think more general optimizations would be more fruitful than ones specifically for any and all. There are improvements being made to interpreter all the time, and the difference between calling any/all and the for loop will likely diminish with new versions of Python. That makes the performance rationale for the new syntax less clear.

Besides, if range literals still haven’t been accepted, I don’t think this will be. Personally, if I had to pick one or the other, I’d rather have the range literals.

No, that wouldn’t be productive, but most uses of any/all are probably in functions that aren’t called frequently, so the performance doesn’t matter that much. It would only be worthwhile to replace any/all with for loops in the functions that are called most often.

2 Likes

I’m not sure I understand it. It doesn’t look like there’s any way to know if it went through the whole iterable, or if it stopped early. (doesn’t it just give you some number of values, like generator expressions currently do?) You’d need to know that to implement any or all, I believe, though I might be missing something.

1 Like

This syntax is to replace the any() call, which inherently only needs to run until a true value in encountered.

OK. You mentioned itertools.takewhile, so I thought you meant it was supposed to replace that.

  1. Don’t have comments on implementation as not know much about how things work there.
  2. I like it as it uses while and until keywords which are widely used for such logic.
  3. The syntax is readable
  4. It would be nice if this addressed short-circuited-counting.
    E.g.:
a = iter([0, 0, 1, 1])
take_until_2nd(bool, a) == [0, 0, 1]
# if I had what you suggested:
I could just:
chain(take_until(bool, a), next(a), take_until(bool, a))
# However, the issue is that first `take_until` consumes first "1"

At least a good recipe should come along while working on this.

E.g. I currently have utility which covers 99% of counting cases that I need. It has the logic as follows:

def count_n(iterator, expression, n):
    i = 0
    for el in iterator:
        if expression(el):
            i += 1
            if i > n:
                break
    return return copysign(1, i - n)

any(expression for m in maps): count_n(iterator, expression, 1) > 0
all(expression for m in maps): count_n(iterator, not expression, 1) > 0
1 Like

The opening link is to a PR that micro-optimizes collections.Chainmap.__contains__ and hence Chainmap.get for speed, at the cost of space and readability. The replacement does this by replacing a function call, which is slow in Python, by equivalent code that has been optimized to the particular situation. One of the particulars of the situation is being able to short circuit the loop with return statements.

This example is not a reason to make any general change to python syntax.

  • Speed versus space and readability is a common tradeoff in programming. Similar speedups are available to anyone willing to pay the cost.
  • This replacement comprises the entire code of a special method for a standard library class. Such classes and methods are intended to be blocks in the construction of applications. In general, we try to make such things speedy for the benefit of users. The particular core developer involved here is especially keen to do so. The cost is local while the benefit is general. (But there are cases where we reject faster code because space or maintainability costs outweigh speed benefits.)
  • any is still the obvious way to perform its function. That is why it was the first version. Our general advice is “Make it work; then make it faster, if you have it well tested enough to minimize risk and if the cost otherwise seems worthwhile”. That is what was done here.
  • As shown by OP, the cost of using any is greatest for short loops. I am rather sure that the most common number of maps chained together by a ChainMap is 2, the lowest possible, with 3 being the next most common.
  • The particular proposal is a distortion of comprehension (looping) syntax, which I strongly and consistently oppose. (I won’t further repeat what others said in this regard.)

Nineteendo, please drop the proposal.

FUNNY you should say that! That sounds an awful lot like a job for… a JIT compiler!

It’s even better than you describe, since the JIT compiler can optimize other things too.

There were 2 things:

  • the short circuiting unpacking assignment
  • the extedned generator syntax to expose the tests to the compiler

The generator syntax I have in mind is:

 for expr(x) for x in iterable-expr [while test(x)] [if test2(x)] [until test3(x)]

or possibly:

 for expr(x) for x in iterable-expr [if test2(x)] [while test(x)] [until test3(x)]

or even better:

 for expr(x) for x in iterable-expr [if test2(x)] [{ while test3(x) | until test3(x)}]

i.e. pick while or until, not both.

A while is like takewhile: return the leading contiguous run where
the test is true, then stops.

An until returns the leading run up to and including the first item
for which the test is true, then stops.

On reflection I’m liking the final version above the most: apply
while/until after the if, and probably only use one of while or
until.

Part of the objective here is the same as the OP’s: putting the tests in
the generator syntax exposes them to the compile so Python can make
performant code.