I'm aware that syntax for ranges and slices has been discussed a good
amount over the years, but I wanted to float an idea out there to see if it
hasn't been considered before. It's not really original. Rather, it's a
combination of a couple parts of Python, and I find it
fascinatingly-consistent with the rest of the language. This will look
similar to PEP 204, but there are some important differences and
clarifications.

(start:stop:step)


Meet a range/slice object. Parentheses are required. (Its syntax in this
regard follows exactly the same rules as a generator expression.) I say
both range and slice because it can be used in either role. On the one
hand, it is iterable and functions exactly like range(start, stop, step) in
those contexts. On the other, it can also be passed into list indexing
exactly like slice(start, stop, step). This is a proposal that range and
slice are really the same thing, just in different contexts.

Why is it useful? I at least find its syntax to be simple, intuitive, and
concise -- more so than the range(...) or slice(...) alternatives. It's
quite obvious for an experienced Python user and just as simple to pick up
as slice notation for a beginner (since it *is* slice notation).

It condenses and clears up sometimes-cumbersome range expressions. A couple
examples:


sum(1:6) # instead of sum(range(1, 6))

list(1:6)


for i in (1:6):

print(i**2)


(i**2 for i in (1:6))


It also makes forming reusable slices clearer and easier:

my_slice = (:6:2) # instead of slice(None, 6, 2)
my_list[my_slice]


It has a couple of siblings that should be obvious (think list or set
comprehension):

[start:stop:step] # gives a list
{start:stop:step} # gives a set


This is similar to passing a range/slice object into the respective
constructor:


[1:6] # list(1:6) or [1, 2, 3, 4, 5]
{1:6} # set(1:6) or {1, 2, 3, 4, 5}


Note that the parentheses aren't needed when it is the only argument of a
function call or is the only element within brackets or braces. It takes on
its respective roles for these bracket and brace cases, just like
comprehensions. This also gives rise to the normal slice syntax:

my_list[1:6:2] # What is inside the brackets is a slice object.
my_list[(1:6:2)] # Equivalent. The parentheses are valid but unnecessary.


So here's the part that requires a little more thought. Any of the values
may be omitted and in the slice context the behavior has no changes from
what it already does: start and stop default to the beginning or end of the
list depending on direction and the step defaults to 1. In the range
context, we simply repeat these semantics, but noting that there is no
longer a beginning or end of a list.

Step defaults to 1 (just like range or slice).
Start defaults to 0 when counting up and -1 when counting down (just like
slice).
If stop is omitted, the object will act like an itertools.count object,
counting indefinitely.

I have found infinite iteration to be a natural and oft-desired extension
to a range object, but I can understand that some may want it to remain
separate and pure within itertools. I also admit that the ability to form
an infinite list with only three characters can be a scary thought (though
we are all adults here, right? ;). Normally you have to take a couple extra
keystrokes:

from itertools import count
list(count())
# rather than just [:]


If that is the case, then raising an error when iter() is called on a
range/slice object with no stop value could be another acceptable course of
action. The syntax will still be left valid.

And that's mainly it. Slice is iterable or range is "indexable" and the
syntax can be used anywhere successive values are desired. If you want to
know what it does or how to use it in some case, just think, "what would a
slice object do?" or "what would a range object do?" or "how would I write
a generator expression/list comprehension here?".

Here are a few more examples:


for i in (:5): # 5 elements 0 to 4, i.e. range(5)

print(i**2)


 for i in (1:): # counts up from one for as long as you want, i.e. count(1)

print(i**2)

if i == 5: break

it = iter(:) # a convenient usage for an infinite counter

next(it)


' '.join(map(str, (:5:2))) # gives '0 2 4'

[(:5), (5:10)] # list of range/slice objects
[[:5], [5:10]] # list of lists
[*(:5), *(5:10)] # uses unpacking to get flat list
[*[:5], *[5:10]] # same unpacking to get flat list


Otherwise you'd have to do:

[list(range(5)), list(range(5, 10))] # list of lists
[*range(5), *range(5, 10)] # flat list


Tuples:

tuple(1:6:2) # (1, 3, 5)
*(1:6:2), # same


I don't actually have experience developing the interpreter and underlying
workings of Python, so I don't know how much of a change this requires. I
thought it might be possible since the constructs already exist in the
language. They just haven't been unified yet. I also realize that there are
a few other use-cases that need to be ironed out. The syntax might also be
too minimal in some cases to be obvious. One of the trickiest things may be
what it will be called, since the current language has the two different
terms.

In the end it's just another range/slice idea, and the idea has probably
already been proposed sometime in the past few decades, but what thoughts
are there?

- Nicholas
_______________________________________________
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to