Personally, I would like very much to see a new PEP regarding range literals and I (like probably everyone else in the community) do have some opinionated views on what range literals could be so I wish for maybe some dialogue before a PEP is written. Am I right?
In any case, here is my take on what range literals would be if we reuse slice syntax for consistency, but lifting the several limitations from the original document:
Notice how the square brackets aren’t part of the slice, so there is no requirement the range literals have them even if we are going to follow slice syntax.
mylist[start:stop:step]
mylist[slice(start, stop, step)]
Also multiple slices are already accepted separated by commas, so even with slice syntax, they may not be surrounded by square brackets directly.
mylist[1:2, 3:4, 5:6]
Going crazy for a minute:
class GimmeSlice:
def __getitem__(self, thing):
return thing
>>> GimmeSlice()[1:lambda x: 3: lambda y: 5, [x for x in range(10)]:[]:f'{1+1}']
(
slice(1, <function <lambda> at 0x116cb2ca0>, <function <lambda> at 0x116cb19e0>),
slice([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], '2')
)
We can already write arbitrary code separated by colons with some set precedence (see the unparenthesized lambdas). This shows there isn’t much separating us from using slices anywhere, same to what happened to the Ellipsis syntax that was reserved for getitem only.
That way it would be ok to make a range literal something like:
foo = 1:10
bar = :len(foo):2
for x in 1:2:
for y in 1:2::
print('The last colon is the one requested by the for block.')
my_dict = {'a': (1:5)} # Parenthesis could be required here
my_range = 1 : 10 : 2 # spaces allowed same as getitem syntax.
With a simple class like this we can cover all of the use cases of the range
functions and raising an error on invalid use cases, such non integers and missing stop:
class Range:
def __getitem__(self, a_slice):
if not isinstance(a_slice, slice):
return range(a_slice)
return range(
a_slice.start if a_slice.start is not None else 0,
a_slice.stop,
a_slice.step if a_slice.step is not None else 1
)
Range()[1:10]
Range()[3:15:2]
Range()[:10] # (or also Range()[10])
Range()[1:] # Error it could also be an infinite generator, such as itertools.count
But now here is an important question. Why slices are just a 3 item struct with no functionality other than a method called indices which the docs say “You probably do not want to use this function.”. That’s right, no one does.
Can’t the slices class be extended to generate useful things? For example, why can’t we reverse the roles and instead of a class object handle a slice, the slice handle the class object, like itertools.islice
.
For example, the iter requests the class to return an object and they could all be objects, in fact there are some low level classes for each of the data types defined in C:
>>> iter([])
<list_iterator at 0x116f34760>
>>> iter('')
<str_ascii_iterator at 0x116e69420>
>>> iter(b'')
<bytes_iterator at 0x1181abf10>
>>> iter(())
<dict_keyiterator at 0x116c49300>
>>> iter(range(1))
<range_iterator at 0x1181abea0>
Among other types like tuples, sets, dict values, dict items and so on.
But what if we want more, what if we could pack everything range, slice, islice, iterators into a single powerful entity and then define a special syntax for it.
class int:
def __range__(cls, start, stop, step):
if start is None:
start = 0
if step is None:
step = 1
if stop is None:
return itertools.count(start, step)
return range(start, stop, step)
class str:
def __range__(cls, start, stop, step):
if not start or not stop:
raise ValueError('start and stop required')
start = ord(start)
stop = ord(stop) + 1 # closed range
if step is None:
step = 1
return map(chr, range(start, stop, step))
class float:
def __range__(cls, start, stop, step):
return np.arange(start, stop, step) # replace numpy with a similar implementation
class slice:
def range(self)
dtype = None
if self.start is not None:
dtype = type(s.start)
if self.stop is not None:
if dtype and dtype is not type(self.stop):
raise ValueError('start and stop must be of same type') # Maybe we could check if they share the same __slice__ method as in both subclass a same class
dtype = type(self.stop)
if dtype is None:
dtype = int # arbitrary for integer range compatibility
return dtype.__range__(self.start, self.stop, self.step)
Then list(1:5)
would execute as:
list(slice(1, 5, None).range())
list(int.__range__(1, 5, None))
list(range(1, 5))
[1, 2, 3, 4]
And this 'a':'f'
would execute as :
slice('a', 'f', None).range()
str.__range__('a', 'f')
<generator at 0x123456>
Which would become 'abcdef'
with the help of ''.join()
I realize there could be some clash with the annotation syntax, but might be possible to overcome with parenthesis whenever they can appear together.