On the Python-ideas list, in yet another thread on a way to embed statements in expressions, I raised the issue that the statement-expression distinction, and not having a way to escape it, is important to why Python is so readable. But I couldn't explain exactly why.

The question ultimately comes down to: why have statements in the first place? After all, there's no reason you can't make (almost) everything an expression, even in an imperative language (Ruby and CoffeeScript do so), and use significant indentation within expressions (again, CoffeeScript does so).

Guido's answer

Hm... Practically every language I knew before I designed Python had this distinction built right into the grammar and other assumptions: Algol-60, Fortran, Pascal, C, ABC. Even Basic. I was aware of the alternative design choice: Algol-68 had statements-as-expression, and Lisp of course -- but I wasn't a big Lisp fan, and in Algol-68 it was largely a curiosity for people who wanted to write extra-terse code (also, IIRC the prevailing custom was to stick to a more conservative coding style which was derived from Algol-60). 
So it's hard to say to what extent this was a conscious choice and to what extent it was just tradition. But there's nothing necessarily wrong with tradition (up to a point). I think it still makes sense that statements are laid out vertically while expressions are laid out horizontally. Come to think of it, mathematics uses a similar convention -- a formula is laid out (primarily) horizontally, while a sequence of formulas (like a proof or a set of axioms) is laid out vertically. 
I think several Zen items apply: readability counts, and flat is better than nested. There is a lot to be said for the readability that is the result of the constraints of the blackboard or the page. (And this reminds me of how infuriating it is to me when this is violated -- e.g. 2up text in a PDF that's too tall to fit on the screen vertically, or when a dumb editor breaks lines but doesn't preserve indentation.)
Guido's analogy with mathematical proofs is compelling. It's a large part of the design philosophy of Python that code should read like English when possible (e.g., the use of the colon is based on the way colons are used in English sentences), or like mathematics when English doesn't make sense (e.g., operator precedence).

And ultimately, "readability counts" is the answer here. But hopefully there's a way to explain how statements help readability, that gets to the actual fact behind Guido's analogy, and behind the intuition behind the tradition.

Ron Adam's followup

Expressions evaluate in unique name spaces, while statements generally do
not. Consider "a + b"; it is evaluated in a private method after the values
a and b are passed to it. 
Statements are used to mutate the current name space, while expressions
generally do not. 
Statements can alter control flow, while expressions generally do not. 
Having a clear distinction between expressions and statements makes reading
and understanding code much easier.

I'm not as sure about Ron's first three points. For example, in Python, "a[0] = 2" is evaluated by calling an a.__setitem__ method with the values a, 0, and 2 passed to it, exactly as "a + b" is evaluated by calling an a.__add__ method with the values a and b passed to it. And C++ takes this farther, where even normal assignment is a method call, and Ruby takes it even farther, where a for loop is evaluated by passing the object being looped over and the proc to call on each element to a method. So, that only requires a handful of things to be statements, like break, continue, and return (the very things that are statements in Ruby and CoffeeScript).

But his last point, that's the whole crux of the matter: I think having a clear distinction does make reading and understanding code easier, and it's a large part of why idiomatic Python code is more readable than Ruby or CoffeeScript code. (Guido raised another point earlier about CoffeeScript: its grammar is basically not defined at all, except in terms of translation rules to JavaScript, which means it's impossible to hold the syntax in your head. And I agree—but not many other languages suffer from that problem, and yet they're less readable than Python.) The question is why it makes reading and understanding code easier.

My thoughts

Actually, I don't think having the distinction does necessarily make reading code easier.  It enables a language to make reading code easier, but depending on how it makes other choices, it may not get that benefit. After all, JavaScript is less readable than CoffeeScript, and I think Java and C++ would both gain from being able to do more in expressions, but Python wouldn't.

What's the difference? Partly the fact that compound statements are indentation-driven rather than brace-driven, but standard style guidelines for other languages (which the vast majority of code follows) already recommend that. And partly the fact that terse forms (like comprehensions) are only for purely declarative code, but those other languages don't have any corresponding forms in the first place.

The big difference is that mutating functions (idiomatically) don't return the mutated object. This means that fluent style code is impossible to write in Python, which comes at a cost. More generally, Python is not a great language for writing some kinds of complex expressions that other languages can handle easily. But it also provides a benefit. When combined with the two preceding features, and the statement-expression divide (and a well-designed language and set of idioms) you get two things that are nearly unique to Python:
  • Flow control is immediately visible by scanning the code, because it's represented by indentation levels and very little else is.
  • State mutation is almost immediately visible by scanning the code, because each line of code usually represents exactly one mutation, and usually to the thing on the left.
That's a lot of important information to be able to pick up without thoroughly reading the code and thinking through it. It often lets you quickly find the part that you do need to read thoroughly and think about, just by skimming the rest.

This implies that there might be different ways to make languages readable than the way Guido came up with. And to some extent, I think Ruby and CoffeeScript are both examples of steps in one possible direction. But, even though they've broken with tradition more than Python has, they haven't gotten as far yet. The real question is whether you can find a way to make functional-style code as nice as it is in Ruby (or even ML or Haskell) while making imperative/OO-style code as nice as it is in Python. I think that would require some big clever new idea nobody's come up with yet, not just tweaking the basic designs of Ruby and Python, but maybe I'm wrong.

Further thoughts

Earlier in the thread, Franklin Lee pointed to a great blog post by Guido, Language Design is Not Just Solving Puzzles. In that post, Guido's main point is that attempting to "solve the puzzle" of how to embed statements into expressions in Python is wrong-headed. And he's got an interesting argument.

I had suggested that the inability to escape the division was important for reasons beyond just keeping the parser simpler, but Guido suggests that keeping the parser simpler is already enough reason. Anything that's complicated for the compiler to parse is also likely to be too complicated for a human to parse instinctively; if you have to stop and think about the syntax, it gets in the way of you reading the code.

C and its derivatives provide a perfect example of what he means. The declaration syntax is complicated enough that there are interview questions and online puzzles asking you to parse your way through this and explain what the type is:*
    const char **(*)(const char *(*)(const char *), const char **)
Or, in C++, to explain why this function doesn't construct a list named lines:**
    list<string> lines(istream_iterator<string>(cin), istream_iterator<string>());
Anyway, Guido suggests that parsing Python is essentially modal: there's an indent-sensitive statement-reading mode, and a non-indent-sensitive expression-reading mode. You can embed expressions in statements, but not the other way around, so you basically just need a mode flag rather than a stack of modes. And it's not that the stack would be too complicated to code into a parser, it's that it would be too complicated to fit into a reader's intuition. Anything extra you have to keep in mind while reading is something that gets in the way of your reading. (He puts it better than me, and not just because he devotes a whole post to it rather than just a couple of paragraphs, so go read it.)

Elsewhere in the thread, Guido also raised the point that being able to hold the syntax in your head is important, pointing out that part of the problem with understanding CoffeeScript is the fact that you can't possibly understand the syntax because it isn't even defined anywhere except as a set of translation rules to JavaScript.

I think he's right about both these points. And if you combine them with the fact that statements can be (and, in Python, are) used to make code more skimmable, that explains why being able to embed a statement in an expression is probably a losing proposition.


* It's a pointer to a string-specific version of the map function. C represents strings as const char *, so const char ** is an array of strings. The parentheses around the next asterisk are necessary to make it part of the whole declaration rather than part of the return type, so we're declaring a pointer to a function that returns an array of strings. The first parameter type is similarly a pointer to a function returning a string, and taking a string, and the second parameter type is an array of strings. It may be easier to read in SML syntax (or, really, almost any other syntax…): (string -> string) * (string list) -> string list.

** In C, some constructions can be parsed as either an expression or a declaration, both of which are valid statements; C resolves this by treating any such ambiguous constructions as declarations. That doesn't come up too often in C, but in C++, it does all the time. Here, we're trying to construct a list<string> by calling the list constructor with a pair of begin and end iterators. For the begin, we're passing istream_iterator<string>(cin), which iterates lines off standard input. For the end, we're passing istream_iterator<string>(), which is the default end iterator of the same type. But istream_iterator<string>() can also be read as either a type—a function that takes nothing and returns an iterator—or an expression—a call of the constructor. So now we have to see whether the rest of the statement can be read as a declaration to know which one it is. istream_iterator<string>(cin) can be read as a type followed by an identifier (in unnecessary, but legal, parentheses). Which means the whole thing can be read as a function declaration: lines takes an iterator named cin, and a function that returns an iterator with no parameter name, and returns a list.
3

View comments

It's been more than a decade since Typical Programmer Greg Jorgensen taught the word about Abject-Oriented Programming.

Much of what he said still applies, but other things have changed. Languages in the Abject-Oriented space have been borrowing ideas from another paradigm entirely—and then everyone realized that languages like Python, Ruby, and JavaScript had been doing it for years and just hadn't noticed (because these languages do not require you to declare what you're doing, or even to know what you're doing). Meanwhile, new hybrid languages borrow freely from both paradigms.

This other paradigm—which is actually older, but was largely constrained to university basements until recent years—is called Functional Addiction.
5

I haven't posted anything new in a couple years (partly because I attempted to move to a different blogging platform where I could write everything in markdown instead of HTML but got frustrated—which I may attempt again), but I've had a few private comments and emails on some of the old posts, so I decided to do some followups.

A couple years ago, I wrote a blog post on greenlets, threads, and processes.
6

Looking before you leap

Python is a duck-typed language, and one where you usually trust EAFP ("Easier to Ask Forgiveness than Permission") over LBYL ("Look Before You Leap"). In Java or C#, you need "interfaces" all over the place; you can't pass something to a function unless it's an instance of a type that implements that interface; in Python, as long as your object has the methods and other attributes that the function needs, no matter what type it is, everything is good.
1

Background

Currently, CPython’s internal bytecode format stores instructions with no args as 1 byte, instructions with small args as 3 bytes, and instructions with large args as 6 bytes (actually, a 3-byte EXTENDED_ARG followed by a 3-byte real instruction). While bytecode is implementation-specific, many other implementations (PyPy, MicroPython, …) use CPython’s bytecode format, or variations on it.

Python exposes as much of this as possible to user code.
6

If you want to skip all the tl;dr and cut to the chase, jump to Concrete Proposal.

Why can’t we write list.len()? Dunder methods C++ Python Locals What raises on failure? Method objects What about set and delete? Data members Namespaces Bytecode details Lookup overrides Introspection C API Concrete proposal CPython Analysis

Why can’t we write list.len()?

Python is an OO language. To reverse a list, you call lst.reverse(); to search a list for an element, you call lst.index().
8

Many people, when they first discover the heapq module, have two questions:

Why does it define a bunch of functions instead of a container type? Why don't those functions take a key or reverse parameter, like all the other sorting-related stuff in Python? Why not a type?

At the abstract level, it's often easier to think of heaps as an algorithm rather than a data structure.
1

Currently, in CPython, if you want to process bytecode, either in C or in Python, it’s pretty complicated.

The built-in peephole optimizer has to do extra work fixing up jump targets and the line-number table, and just punts on many cases because they’re too hard to deal with. PEP 511 proposes a mechanism for registering third-party (or possibly stdlib) optimizers, and they’ll all have to do the same kind of work.
3

One common "advanced question" on places like StackOverflow and python-list is "how do I dynamically create a function/method/class/whatever"? The standard answer is: first, some caveats about why you probably don't want to do that, and then an explanation of the various ways to do it when you really do need to.

But really, creating functions, methods, classes, etc. in Python is always already dynamic.

Some cases of "I need a dynamic function" are just "Yeah? And you've already got one".
1

A few years ago, Cesare di Mauro created a project called WPython, a fork of CPython 2.6.4 that “brings many optimizations and refactorings”. The starting point of the project was replacing the bytecode with “wordcode”. However, there were a number of other changes on top of it.

I believe it’s possible that replacing the bytecode with wordcode would be useful on its own.
1

Many languages have a for-each loop. In some, like Python, it’s the only kind of for loop:

for i in range(10): print(i) In most languages, the loop variable is only in scope within the code controlled by the for loop,[1] except in languages that don’t have granular scopes at all, like Python.[2]

So, is that i a variable that gets updated each time through the loop or is it a new constant that gets defined each time through the loop?

Almost every language treats it as a reused variable.
4
Blog Archive
About Me
About Me
Loading
Dynamic Views theme. Powered by Blogger. Report Abuse.