[Python-ideas] Pass a function as the argument "step" of range()

Terry Reedy tjreedy at udel.edu
Thu Jul 2 21:53:40 CEST 2015


On 7/2/2015 2:30 AM, Pierre Quentel wrote:
> In languages such as Javascript, the incrementation of a for loop
> counter can be done by an operation, for instance :
>
> for(i=1; i<N; i*=2)
>
> would iterate on the powers of 2 lesser than N.
>
> To achieve the same thing in Python we currently can't use range()
> because it increments by an integer (the argument "step"). An option is
> to build a generator like :
>
> def gen(N):
>      i = 1
>      while i<=N:
>          yield i
>          i *= 2
>
> then we can iterate on gen(N).
>
> My proposal is that besides an integer, range() would accept a function
> as the "step" argument, taking the current counter as its argument and
> returning the new counter value. Here is a basic pure-Python
> implementation :
>
> import operator
>
> class Range:
>
>      def __init__(self, start, stop, incrementor):
>          self.start, self.stop = start, stop
>          self.incrementor = incrementor
>          # Function to compare current counter and stop value : <= or >=
>          self.comp = operator.ge <http://operator.ge> if
> self.stop>self.start else operator.le
>          self.counter = None
>
>      def __iter__(self):
>          return self
>
>      def __next__(self):
>          if self.counter is None:
>              self.counter = self.start
>          else:
>              self.counter = self.incrementor(self.counter)
>          if self.comp(self.counter, self.stop):
>              raise StopIteration
>          return self.counter

The idea of iterating by non-constant steps is valid.  Others have given 
multiple options for doing so.

The idea of stuffing this into range is not valid.  It does not fit into 
what 3.x range actually is.  The Range class above is a one-use 
iterator. This post and your counter-responses seem to miss what others 
have alluded to.  3.x range class is something quite different -- a 
reusable collections.abc.Sequence class, with a separate range_iterator 
class.

The range class has the following sequence methods with efficient O(1) 
implementations: __contains__, __getitem__, __iter__, __len__, 
__reversed__, count, and index methods.  Having such efficient methods 
is part of the design goal for 3.x range.  Your proposal would breaks 
all of these except __iter__ (and make that slower too) in the sense 
that the replacements would require the O(n) calculation of list(self), 
whereas avoiding this is part of the purpose of range.

While some of these methods are rarely used, __reversed__ is certainly 
not rare.  People depend on the fact that the often easy to write 
reversed(up-range(...)) is equivalent in output *and speed* to the often 
harder to write iter(down-range(...).

A trivial case is counting down from n to 0
   for i in reversed(range(n+1):
versus
   for i in range(n, -1, -1):
People do not always get the latter correct.

Now onsider a more general case, such as
   r = range(11, 44000000000, 1335)
   r1 = reversed(r)
versus the equivalent
   r2 = iter(range(43999999346, 10, -1335))

43999999346 is r[-1], which uses __getitem__.
Using this is much easier than figuring out the following (which 
__reversed__ has built in).

def reversed_start(start, stop, step):
     rem = (stop - start) % step
     return stop - (rem if rem else step)

-- 
Terry Jan Reedy



More information about the Python-ideas mailing list