[Tutor] Random revisions

Danny Yoo dyoo@hkn.eecs.berkeley.edu
Wed, 13 Feb 2002 14:53:33 -0800 (PST)


On Wed, 13 Feb 2002, Danny Yoo wrote:

> On Thu, 14 Feb 2002, kevin parks wrote:
> 
> > I see in the random module that you can get random integers with
> > randrange and you can get 0-1.0 floats, but how does one get floats in
> > a range (of say 0-100, 50-128, or whatever...), do you just use the
> > regular random (0-1) floats and scale the output?
> 
> I think scaling would be the way to go on this one.  I can't think offhand
> of a function in the random module that will do this, but cooking up such
> a function should be too bad.
> 
> This sounded like a fun problem, so I've cooked up such a function.



By the way, I started playing around with randrange(), and found something
really wacky.  Take a look:


###
>>> random.randrange(0, 10, 2)
2
>>> random.randrange(0, 10, 2)
8
>>> random.randrange(0, 10, 2, 42)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "/opt/Python-2.1.1/lib/python2.1/random.py", line 288, in randrange
    istart = int(start)
TypeError: object of type 'int' is not callable
###


What in the world?!  If you're confused, you're not alone.  According to
the documentation, randrange() is defined to only take, at most, three
parameters.  But it appears that it can accept four... but then it barfs.  
Huh?


There's something weird about the definition of randrange() in the random
module.  Take a look:

###
    def randrange(self, start, stop=None, step=1, int=int, default=None):
        """Choose a random item from range(start, stop[, step]).

        This fixes the problem with randint() which includes the
        endpoint; in Python this is usually not what you want.
        Do not supply the 'int' and 'default' arguments.
        """

        # This code is a bit messy to make it fast for the
        # common case while still doing adequate error checking
        istart = int(start)
        if istart != start:
            raise ValueError, "non-integer arg 1 for randrange()"
        if stop is default:
            if istart > 0:
                return int(self.random() * istart)
            raise ValueError, "empty range for randrange()"
        istop = int(stop)
        if istop != stop:
            raise ValueError, "non-integer stop for randrange()"
        if step == 1:
            if istart < istop:
                return istart + int(self.random() *
                                   (istop - istart))
            raise ValueError, "empty range for randrange()"
        istep = int(step)
        if istep != step:
            raise ValueError, "non-integer step for randrange()"
        if istep > 0:
            n = (istop - istart + istep - 1) / istep
        elif istep < 0:
            n = (istop - istart + istep + 1) / istep
        else:
            raise ValueError, "zero step for randrange()"

        if n <= 0:
            raise ValueError, "empty range for randrange()"
        return istart + istep*int(self.random() * n)
###


I've been staring at this, looking at the comments, and looking at those
two parameters 'int' and 'default', and I still can't figure out why those
two parameters are in there... hmmm...

Ah, it is probably an optimization.  What the author might have intended
is to optimize the time it takes to grab at the int() function.  Local
variables are faster to look up than globals, and since randrange() is
often used in loops, this may be intended to squeeze out some more
performance.

random.shuffle() also appears to use this kind of optimization.  Someone
must have done some time profiling and found this optimization significant
enough to do something tricky like this.  But this feels very ugly to
me... Is it really necessary to do something like this?



Just as we revise essays and letters, I think that it's good to see how
one might revise functions.  (I'd better warn that "revision" doesn't
necessarily mean "improvement".  *grin*)

Here's an attempt to revise randrange() and remove this trickyness:

###
    def randrange(self, start, stop=None, step=1):
        """Choose a random item from range(start, stop[, step]).

        This fixes the problem with randint() which includes the
        endpoint; in Python this is usually not what you want.
        """

        # This code is a bit messy to make it fast for the
        # common case while still doing adequate error checking
        if start != int(start):
            raise ValueError, "non-integer arg 1 for randrange()"
        if stop is None:
            if start > 0:
                return int(self.random() * start)
            raise ValueError, "empty range for randrange()"
        if stop != int(stop):
            raise ValueError, "non-integer stop for randrange()"
        if step == 1:
            if start < stop:
                return start + int(self.random() *
                                   (stop - start))
            raise ValueError, "empty range for randrange()"
        if step != int(step):
            raise ValueError, "non-integer step for randrange()"
        if step > 0:
            n = (stop - start + step - 1) / step
        elif step < 0:
            n = (stop - start + step + 1) / step
        else:
            raise ValueError, "zero step for randrange()"

        if n <= 0:
            raise ValueError, "empty range for randrange()"
        return start + step*int(self.random() * n)
###


Does anyone have time to profile this and see how it compares to
random.randrange()?  It's been like this ever since version 1.16 of
randrange.py:

http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/python/python/dist/src/Lib/random.py