[Python-ideas] With expressions

Abe Dillon abedillon at gmail.com
Fri Aug 3 17:36:47 EDT 2018


I like this idea in theory, but I'm not sold yet.

I think there's a lot of draw to the concept of "expressionizing"
statements because many statements require an unnatural ordering in-which
the most important code, the logic, comes after some necessary but
ultimately noisy (from the readers perspective) preamble. So I expect
people to keep asking for expressionized statements and slowly, but,
surely, they'll make their way into the language. They just need to be very
carefully thought out.

Expressionization may break the "one and only on obvious way" guideline,
but it can offer concise, readable code in a lot of instances where a
statement-based version would be clumsy and noisy, and there's already some
precedent for it:

function declaration => lambda
for-loops => generator expressions and comprehensions
if-else => ternary statements

With the exception of lambda, expressionized statements usually allow one
to put the "meat before the vegitables" so to speak. That is; the highest
value part of the expression comes first and all the book-keeping follows. To
illustrate this, I'll write a part of an expression and gradually reveal
the complete expression, you can see how progressively easier it is to
predict the next reveal:

def initials(people):
    return {"".join(name[0] ...

# The identifier "name" isn't in scope so it must be assigned in the for
clause of a comprehension.

def initials(people):
    return {"".join(name[0] for name in ...

# This is a nested comprehension so it's not much of a surprise that the
iterator might be related to another
# yet-to-be assigned identifier.

def initials(people):
    return {"".join(name[0] for name in person.names ...

# Blindly accessing the first element of an empty sequence could cause
problems

def initials(people):
    return {"".join(name[0] for name in person.names if name) ...

# The inner generator is closed but we still need a binding for "person"

def initials(people):
    return {"".join(name[0] for name in person.names if name) for person in
...

# There's not much left to iterate over and decent variable names point to
one obvious choice

def initials(people):
    return {"".join(name[0] for name in person.names if name) for person in
people}


The same could be said for lambdas if they were defined logic-first because
they're usually used in a context where the call signature is obvious:

hand = sorted(cards, key=(card.suit if card is not wild else max_value <==
card))[-5:]

Of course, no such thing exists so the '<==' syntax is made up (in-fact a
possibly better alternative is, "with"), but it doesn't really matter
because a reverse lambda isn't going to happen. You can see, however; that
the function's signature is pretty obvious from context, so it's more for
the computer's sake than the reader's sake and would be better placed out
of the way.

I like the current proposal because it follows that design idea, however; I
haven't taken the time to think about all the edge cases yet.
For instance, what would the following do?

initial = person.name[0] with suppress(AttributeError)  # Hangover from PEP
505 discussion...

Now that I think of it, this seems to inherently make assignment part of
the expression:

data = file.read() with open(...) as file

is supposed to be equivalent to:

with open(...) as file:
    data = file.read()

Right?

So maybe it only makes sense to use expression assignment (PEP 572):

data = (d := file.read() with open(...) as file)

To which I say, "Eww..."

Also:

initial = (i := person.name[0] with suppress(AttributeError))

Is still ambiguous (and still eww).

One tactic that other expressionizations have taken is to limit the scope.
For instance, the ternary operation only covers expressionization of
"if-else" not "just if" or "if-elif-..." or "if-elif-...-else", and
generator expressions don't allow the 'else' clause
<http://book.pythontips.com/en/latest/for_-_else.html> of normal for-loops.
So maybe you can obviate some of the edge cases by requiring an as clause
or something. I don't know how that would help with the
suppress(AttributeError) case thought...

On Fri, Aug 3, 2018 at 12:56 PM, Todd <toddrjen at gmail.com> wrote:

> On Thu, Aug 2, 2018 at 5:35 AM, Ken Hilton <kenlhilton at gmail.com> wrote:
>
>> Hi, I don't know if someone has already suggested this before, but here
>> goes:
>>
>> With expressions allow using the enter/exit semantics of the with
>> statement inside an expression context. Examples:
>>
>>     contents = f.read() with open('file') as f #the most obvious one
>>     multiplecontents = [f.read() with open(name) as f for name in names]
>> #reading multiple files
>>
>> I don't know if it's worth making the "as NAME" part of the with
>> mandatory in an expression - is this a valid use case?
>>
>>     data = database.selectrows() with threadlock
>>
>> Where this would benefit: I think the major use case is `f.read() with
>> open('file') as f`. Previous documentation has suggested
>> `open('file').read()` and rely on garbage collection; as the disadvantages
>> of that became obvious, it transitioned to a method that couldn't be done
>> in an expression:
>>
>>     with open('file') as f:
>>         contents = f.read()
>>
>> Therefore `f.read() with open('file') as f`, I think, would be much
>> welcomed as the best way to read a file in an expression.
>>
>> For those wondering about the scope semantics of the "as NAME", I think
>> they would be identical to the scope semantics of the "for" expression -
>> i.e. these are legal:
>>
>>     contents = f.read() with open('file') as f
>>     grid = [[i] * 4 for i in range(4)]
>>
>> But these are not:
>>
>>     contents = f.read() with open('file') as f
>>     f.seek(0)
>>     grid = [[i] * 4 for i in range(4)]
>>     grid[i][i] = 4
>>
>> Is this a good idea? Are there some subtleties I've failed to explain?
>> Please let me know.
>>
>> Sharing,
>> Ken Hilton
>>
>>
> If this is a common enough operation for you, it would be trivially easy
> to just write a function that does this.  There is already a module on pypi
> that has this function: read_and_close.
>
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20180803/0d844caf/attachment.html>


More information about the Python-ideas mailing list