Arithmetic sequences in Python

P

Paul Rubin

Xavier Morel said:
The only thing that bothers me about the initial proposal is that
there would not, in fact, be any "range object", but merely a
syntactic sugar for list/generator creation.

Well, it could create something like an xrange. Maybe that's preferable.
Not that I really mind it, but, well, syntactic sugar for the
purpose of syntactic sugar really doesn't bring much to the table.

I don't think this is a valid objection. Python is already full of
syntactic sugar like indentation-based block structure, infix
operators, statements with keyword-dependent syntax, etc. It's that
very sugar that attracts programmers to Python away from comparatively
sugarless languages like Scheme. Indeed, Python is considered by many
to be a sweet language to program in, and they mean that in a nice
way.

If you want, you can think of it as "flavor" rather than "sugar". We
aren't after syntactic minimalism or we'd be using Scheme. The
criterion for adding something like this to Python should be whether
makes the language taste better or not.
 
G

Gregory Petrosyan

Thanks for your replies. So, some problems and possible solutions:

1) [f(n), f(n)-1 .. 0] can be easily catched by interpreter, and f(n)
can be evaluated only once.

2) if you need right border excluded, I think [0 .. n) is very clear
(and consistent with mathematics). And about brakets highlighting... Do
you _really_ need it in this expression? By the way, Python
editors/ide's can be easily extended to support this syntax, and even
to color brakets in [0 .. n) in different colors (so, it'll be very
clear if border is included or not).
But as for me, [0 .. n-1] is better because it looks more like ordinary
list, and we would have no problems on creating generators with
excluded border.

3) Of course, in some cases 'range' is more readable. As for your
examples:

[0,..9] versus range(10)
[55, ...73] versus range(55, 74)
[1, 3, ..len(mystr)] versus range(1, len(mystr)+1, 2)
[55, 65, 295] versus range(55, 296, 10)

a) you use "magic" 10 here...
[0 .. 10) or [0 .. 9] are IMO at least as nice as range(10). And what
about
[1 .. 10] and range(1, 11)? ;-)

b) [55 .. 73] vs range(55, 74)? IMO first is more readable. And you
actually can see what is the last value, while in "range" you think
something like: "if 74 is written here, then last value is 74-1 = 73"

c) [1, 3 .. len(mystr)] vs range(1, len(mystr)+1, 2). As for me, I
should think for 2-3 seconds before I understand what range(1,
len(mystr)+1, 2) stands for :)
And [1, 3 .. len(mystr)] is _obvious_ for almost everybody.

d) why you wrote '296' in range? Maybe, because you are really
experienced programmer, and you can automatically transform 295 -> 296
(because you _know_ the behaviour of range function). But if your task
is simply to iterate through sequence like n1, n2, n3, ... n(X-1), nX,
then [n1 .. nX] looks more natural, isn't it?

4) Proposed syntax can be easily extended to support chars (or any
other enumeration). (Maybe, without _implied_ :) step parameter):

['a' .. 'd'] -> ['a','b','c','d'] (let it be a list for consistency)
('x' .. 'z') -> generator that yields 'x', 'y' and 'z' :)
('a' ..) -> generator that yields english alphabet

Conclusion:
- I don't want to remove 'range' function from use completely. Feel
free to use it ;-)
- The only idea behind my proposal is obviousness. As for me, it was
sometimes slightly difficult to explain to somebody not familiar with
Python what

for n in range(1,10,2):
bla-bla-bla

stands for. Arithmetic sequence IMHO isn't that kind of thing when you
need to know some
special function to use it (is everything OK with my english here?)
 
G

Gregory Petrosyan

Steven said:
Python indexing deliberately goes to one-past-the-end counting for a
reason: it helps prevent off-by-one signpost errors. This syntax goes
against that decision, and adds one more thing to memorise about Python:
the end index is not included in the list, except for arithmetic
sequences, where it is, sometimes but not necessarily. In [5,6,10] the end
index 10 is included; in [5,7,10] it isn't.

1) both in [5,6,10] and [5,7,10] 10 is included ;-)
And as for [5,6 .. 10] and [5,7 .. 10]... First one should look
like [5 .. 10] I think. But you are right:
it looks like a problem that in one case 10 is included and in
other not. I should think about it.

2) I think there's nothing to memorise: in mathematics [1,2 .. 10]
includes 10, almost everybody knows
it.
The thing that newcomer should memorise is (IMHO) _not_so_obvious_
behaviour of 'range'. You
may laugh at me, if you want. But look at Cormen's pseudocode: "for
j=1 to n" includes 'n'. (And
indexing starts from 1, not from 0, but that's another story).
Also, for non-programmer including the
borders is obvious, not otherwise.
 
P

Paul Rubin

Gregory Petrosyan said:
1) both in [5,6,10] and [5,7,10] 10 is included ;-)
And as for [5,6 .. 10] and [5,7 .. 10]... First one should look
like [5 .. 10] I think. But you are right:
it looks like a problem that in one case 10 is included and in
other not. I should think about it.

[5,7 .. 10] means [5,7,9,11,13,15, ... ] limited to x<=10, so that
means [5,7,9]. 10 is not included.
 
P

Paul Rubin

Gregory Petrosyan said:
1) [f(n), f(n)-1 .. 0] can be easily catched by interpreter, and f(n)
can be evaluated only once.

I think it would be counterintuitive for the interpreter to do that.
If I type f(n) twice I expect it to be evaluated twice.
2) if you need right border excluded, I think [0 .. n) is very clear
(and consistent with mathematics).

Oh man, that's really ugly. I'm not crazy about Ruby's "..." either
though I guess it's ok. Using colon for non-inclusion might be more
Python-like since it resembles both the syntax and behavior of an
existing python range:

[0.. : n]

(hmm, it does look kind of ugly).
3) Of course, in some cases 'range' is more readable. As for your
examples:

[0,..9] versus range(10)
[55, ...73] versus range(55, 74)
[1, 3, ..len(mystr)] versus range(1, len(mystr)+1, 2)
[55, 65, 295] versus range(55, 296, 10)

Those examples should be written:

[0 .. 9] versus range(10)
[55 .. 73] versus range(55,74)
[1, 3, .. len(mystr)] versus range(1, len(mystr)+1, 2)
[55, 65, .. 295] versus range(55, 296, 10)

I find the ".." version more readable than the "range" version for
all four cases. YMMV.
4) Proposed syntax can be easily extended to support chars (or any
other enumeration). (Maybe, without _implied_ :) step parameter):

['a' .. 'd'] -> ['a','b','c','d'] (let it be a list for consistency)

Hmm:

Hugs.Base> ['a'..'d']
"abcd"
Hugs.Base>

Note that "abcd" in Haskell is actually a list of chars.
('a' ..) -> generator that yields english alphabet

I think this has to be an infinite generator or one that yields all
the ascii chars starting with 'a'.
 
T

Tom Anderson

Please visit http://www.python.org/peps/pep-0204.html first.

As you can see, PEP 204 was rejected, mostly because of not-so-obvious
syntax. But IMO the idea behind this pep is very nice.

Agreed. Although i have to say, i like the syntax there - it seems like a
really natural extension of existing syntax.
So, maybe there's a reason to adopt slightly modified Haskell's syntax?

Well, i do like the .. - 1..3 seems like a natural way to write a range.
I'd find 1...3 more natural, since an ellipsis has three dots, but it is
slightly more tedious.

The natural way to implement this would be to make .. a normal operator,
rather than magic, and add a __range__ special method to handle it. "a ..
b" would translate to "a.__range__(b)". I note that Roman Suzi proposed
this back in 2001, after PEP 204 was rejected. It's a pretty obvious
implementation, after all.
Something like

[1,3..10] --> [1,3,5,7,9]
(1,3..10) --> same values as above, but return generator instead of
list
[1..10] --> [1,2,3,4,5,6,7,8,9,10]
(1 ..) --> 'infinite' generator that yield 1,2,3 and so on
(-3,-5 ..) --> 'infinite' generator that yield -3,-5,-7 and so on

-1. Personally, i find the approach of specifying the first two elements
*absolutely* *revolting*, and it would consistently be more awkward to use
than a start/step/stop style syntax. Come on, when do you know the first
two terms but not the step size?
1) "[]" means list, "()" means generator

Yuck. Yes, i know it's consistent with list comps and genexps, but yuck to
those too!

Instead, i'd like to see lazy lists used here - these look like lists, and
can be used exactly like a list, but if all you want to do is iterate over
them, they don't need to instantiate themselves in memory, so they're as
efficient as an iterator. The best of both worlds! I've written a sketch
of a generic lazy list:

http://urchin.earth.li/~twic/lazy.py

Note that this is what xrange does already (as i've just discovered).

tom
 
T

Tom Anderson

Agreed. *IF* we truly needed an occasional "up to X *INCLUDED*"
sequence, it should be in a syntax that can't FAIL to be noticed, such
as range(X, endincluded=True).

How about first,,last? Harder to do by mistake, but pretty horrible in its
own way.

tom
 
P

Paul Rubin

Tom Anderson said:
The natural way to implement this would be to make .. a normal
operator, rather than magic, and add a __range__ special method to
handle it. "a .. b" would translate to "a.__range__(b)". I note that
Roman Suzi proposed this back in 2001, after PEP 204 was
rejected. It's a pretty obvious implementation, after all.

Interesting, but what do you do about the "unary postfix" (1 ..)
infinite generator?
-1. Personally, i find the approach of specifying the first two
elements *absolutely* *revolting*, and it would consistently be more
awkward to use than a start/step/stop style syntax. Come on, when do
you know the first two terms but not the step size?

Usually you know both, but showing the first two elements makes
sequence more visible. I certainly like (1,3..9) better than (1,9;2)
or whatever.
1) "[]" means list, "()" means generator
Yuck. Yes, i know it's consistent with list comps and genexps, but
yuck to those too!

I'd be ok with getting rid of [] and just having generators or
xrange-like class instances. If you want to coerce one of those to a
list, you'd say list((1..5)) instead of [1..5].
 
D

Dennis Lee Bieber

[1,3..10] --> [1,3,5,7,9]

On first exposure to the left hand notation, I see something that
represents [1, 3, 4, 5, 6, 7, 8, 9] (or ...,9,10 based upon Ada's usage
of "..")

That is, I see two separate elements in there. One being a range
"3..10", and the other being a scalar "1". I do not see the elements
being "1,3" as an argument to ".."

What would be expected from

[1, 3, 6 .. 20]

???

Again, I see [1, 3, 6, 7, 8, 9, 10, ... , 18, 19] yet, if your "1,3"
is the left part of a "..", I could just as easily see the above
resulting in [1, 3, 6, 10, 15] (as the next step would be 21). IE, the
left part (the pattern, as it were) would be "1, 3, 6", not just "3, 6".
--
 
P

Paul Rubin

Dennis Lee Bieber said:
What would be expected from
[1, 3, 6 .. 20]
???

In Haskell:

Hugs.Base> [1,3,6..20]
ERROR - Syntax error in expression (unexpected `..')
Hugs.Base>
Again, I see [1, 3, 6, 7, 8, 9, 10, ... , 18, 19]

You'd write that in Haskell as 1:3:[6..19]. In Python I guess
you'd say [1,3]+[6..19].
 
G

Gregory Petrosyan

Some ideas:

1) Let [a,b .. c] be *ordinary list* !
Just like [1,2,3]. Are there any questions why 3 is included in
[1,2,3]? IMO it's more correct to think about [first, next .. last] as
about syntax for list creation, but not as about
"syntax-to-replace-range-function". (And, because it's an ordinary
list, you could iterate through it in usual way: "for each element of
list do...")

2) [5 .. 0] -> [5,4,3,2,1,0]
So, if "next" is omited, let the default step be 1 if "first" < "last"
and -1 otherwise.
 
A

Antoon Pardon

Op 2006-01-16 said:
Paul Rubin said:
Steven D'Aprano said:
For finite sequences, your proposal adds nothing new to existing
solutions like range and xrange.

Oh come on, [5,4,..0] is much easier to read than range(5,-1,-1).

But not easier than reversed(range(6)) [[the 5 in one of the two
expressions in your sentence has to be an offbyone;-)]]

Why don't we give slices more functionality and use them.
These are a number of ideas I had. (These are python3k ideas)

1) Make slices iterables. (No more need for (x)range)

2) Use a bottom and stop variable as default for the start and
stop attribute. top would be a value that is greater than
any other value, bottom would be a value smaller than any
other value.

3) Allow slice notation to be used anywhere a value can be
used.

4) Provide a number of extra operators on slices.
__neg__ (reverses the slice)
__and__ gives the intersection of two slices
__or__ gives the union of two slices

5) Provide sequences with a range (or slice) method.
This would provide an iterator that iterates over
the indexes of the sequences. A slice could be
provided


for i in xrange(6):

would then become

for i in (0:6):

for a reversed sequence

for i in reversed(xrange(6)):

would become

for i in - (0:6):


for i, el in enumerate(sequence):

would become

for i in sequence.range():
el = sequence

But the advantage is that this would still work when
someone subclasses a list so that it start index
is an other number but 0.

If you only wanted every other index one could do
the following

for i in sequence.range:):2):

which would be equivallent to

for i in sequence.range() & :):2):
 
P

Paul Rubin

Gregory Petrosyan said:
2) [5 .. 0] -> [5,4,3,2,1,0]
So, if "next" is omited, let the default step be 1 if "first" < "last"
and -1 otherwise.

So what do you want [a..b] to do? Dynamically decide what direction
to go? Ugh!
 
G

Gregory Petrosyan

Hmm, and why not? Or you realy hate such behaviour?

Note, everything I post here is just some ideas I want to discuss, and
to make these ideas better after discussion. And this particular idea
needs to be discussed, too.
 
X

Xavier Morel

Paul said:
I don't think this is a valid objection. Python is already full of
syntactic sugar like indentation-based block structure, infix
operators, statements with keyword-dependent syntax, etc. It's that
very sugar that attracts programmers to Python away from comparatively
sugarless languages like Scheme. Indeed, Python is considered by many
to be a sweet language to program in, and they mean that in a nice
way.

If you want, you can think of it as "flavor" rather than "sugar". We
aren't after syntactic minimalism or we'd be using Scheme. The
criterion for adding something like this to Python should be whether
makes the language taste better or not.

I don't know, most of the syntactic sugar I see in Python brings
something to the language, or trivialize the generation of structures
and constructs that may be complex or awkward without it, it has a
natural, honey-ish sweetness full of flavor, it does not taste like some
cancer-spawning artificial sweetener ;-)
 
S

Steven Bethard

Antoon said:
Why don't we give slices more functionality and use them.
These are a number of ideas I had. (These are python3k ideas)

1) Make slices iterables. (No more need for (x)range)

2) Use a bottom and stop variable as default for the start and
stop attribute. top would be a value that is greater than
any other value, bottom would be a value smaller than any
other value.

Just checking your intent here. What should these do?

(2:5) # should give me 2, 3, 4
(2:5:-1) # infinite iterator 2, 1, 0, ...?
:)5) # start at -infinity? what does that even mean?
:)5:-1) # start at -infinity and go backwards?!!

I suspect you should be raising some sort of exception if the start
isn't defined.

STeVe
 
T

Tom Anderson

Interesting, but what do you do about the "unary postfix" (1 ..)
infinite generator?
1.__range__(None)


Usually you know both, but showing the first two elements makes sequence
more visible. I certainly like (1,3..9) better than (1,9;2) or
whatever.

I have to confess that i don't have a pretty three-argument syntax to
offer as an alternative to yours. But i'm afraid i still don't like yours.
:)
1) "[]" means list, "()" means generator
Yuck. Yes, i know it's consistent with list comps and genexps, but yuck
to those too!

I'd be ok with getting rid of [] and just having generators or
xrange-like class instances. If you want to coerce one of those to a
list, you'd say list((1..5)) instead of [1..5].

Sounds good. More generally, i'd be more than happy to get rid of list
comprehensions, letting people use list(genexp) instead. That would
obviously be a Py3k thing, though.

tom
 
T

Tom Anderson

Op 2006-01-16 said:
Paul Rubin said:
For finite sequences, your proposal adds nothing new to existing
solutions like range and xrange.

Oh come on, [5,4,..0] is much easier to read than range(5,-1,-1).

But not easier than reversed(range(6)) [[the 5 in one of the two
expressions in your sentence has to be an offbyone;-)]]

Why don't we give slices more functionality and use them.
These are a number of ideas I had. (These are python3k ideas)

1) Make slices iterables. (No more need for (x)range)

2) Use a bottom and stop variable as default for the start and
stop attribute. top would be a value that is greater than
any other value, bottom would be a value smaller than any
other value.

3) Allow slice notation to be used anywhere a value can be
used.

4) Provide a number of extra operators on slices.
__neg__ (reverses the slice)
__and__ gives the intersection of two slices
__or__ gives the union of two slices

5) Provide sequences with a range (or slice) method.
This would provide an iterator that iterates over
the indexes of the sequences. A slice could be
provided
+5

for i, el in enumerate(sequence):

would become

for i in sequence.range():
el = sequence


That one, i'm not so happy with - i quite like enumerate; it communicates
intention very clearly. I believe enumerate is implemented with iterators,
meaning it's potentially more efficient than your approach, too. And since
enumerate works on iterators, which yours doesn't, you have to keep it
anyway. Still, both would be possible, and it's a matter of taste.
But the advantage is that this would still work when someone subclasses
a list so that it start index is an other number but 0.

It would be possible to patch enumerate to do the right thing in those
situations - it could look for a range method on the enumerand, and if it
found one, use it to generate the indices. Like this:

def enumerate(thing):
if (hasattr(thing, "range")):
indices = thing.range()
else:
indices = itertools.count()
return itertools.izip(indices, thing)
If you only wanted every other index one could do the following

for i in sequence.range:):2):

which would be equivallent to

for i in sequence.range() & :):2):

Oh, that is nice. Still, you could also extend enumerate to take a range
as an optional second parameter and do this with it. Six of one, half a
dozen of the other, i suppose.

tom
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,901
Latest member
Noble71S45

Latest Threads

Top