33

I looked over Python Docs (I may have misunderstood), but I didn't see that there was a way to do this (look below) without calling a recursive function.
What I'd like to do is generate a random value which excludes values in the middle.

In other words,
Let's imagine I wanted X to be a random number that's not in
range(a - b, a + b)
Can I do this on the first pass,
or
1. Do I have to constantly generate a number,
2. Check if in range(),
3. Wash rinse ?

As for why I don't wish to write a recursive function,
1. it 'feels like' I should not have to
2. the set of numbers I'm doing this for could actually end up being quite large, and
... I hear stack overflows are bad, and I might just be being overly cautious in doing this.

I'm sure that there's a nice, Pythonic, non-recursive way to do it.

pradyunsg
  • 18,287
  • 11
  • 43
  • 96

7 Answers7

44

Generate one random number and map it onto your desired ranges of numbers.

If you wanted to generate an integer between 1-4 or 7-10, excluding 5 and 6, you might:

  1. Generate a random integer in the range 1-8
  2. If the random number is greater than 4, add 2 to the result.

The mapping becomes:

Random number:    1  2  3  4  5  6  7  8
Result:           1  2  3  4  7  8  9 10

Doing it this way, you never need to "re-roll". The above example is for integers, but it can also be applied to floats.

Li-aung Yip
  • 12,320
  • 5
  • 34
  • 49
  • +1 for good explanation -- this is what I was getting at but I found it unclear to explain with just code. – Andrew Gorcester May 19 '12 at 16:31
  • 3
    @AndrewG. : Thanks. :) It could be explained even better with a few pictures, but the activation energy for me opening Visio is a bit high tonight. ;) – Li-aung Yip May 19 '12 at 16:45
  • Thank you so much for this, it's very straight-forward in its execution and inspires the reader to draw the obvious answer as opposed to simply posting code and saying, "Here, run this." In the end I went with Junuxx's solution, but I appreciate the elegance of this answer as well. –  May 21 '12 at 02:49
31

Use random.choice(). In this example, a is your lower bound, the range between b and c is skipped and d is your upper bound.

import random
numbers = range(a,b) + range(c,d)
r = random.choice(numbers)
Junuxx
  • 14,011
  • 5
  • 41
  • 71
  • 13
    This will work unless the set of possible answers is extremely large, in which case it will use too much memory and crash. – Andrew Gorcester May 19 '12 at 15:57
  • @AndrewG: Agreed, it is not ideal if you have a range size of millions/billions. On the other hand, it's simple and memorable. Your answer, while a very nice and robust solution, might be more error-prone. – Junuxx May 19 '12 at 16:09
  • 1
    Yeah. I would use your answer if a, b, c and d were known from the start and known to be a small set, and mine if they were dependent on input or known to be a large set. – Andrew Gorcester May 19 '12 at 16:11
  • I really appreciate both answers! The obviousness of yours was striking to me; in particular because I'd already been using random.choice for aspects of the program and it just had not occurred to me to use it in this fashion. I've implemented this method for now as it struck me as the most beautiful; the set of numbers I'm dealing with is in the lower hundred thousands (for now) so I'm comfortable with using this method for now. Thanks! –  May 21 '12 at 02:47
  • @Junuxx Sorry, I know it is an old question. Will `b` and `c` be excluded inclusively or just the number between them? – ihavenoidea Oct 21 '18 at 18:53
  • 1
    @ihavenoidea: `range` always includes the first and excludes the second argument. So in my example, `numbers` is [a,b) + [c,d). – Junuxx Oct 22 '18 at 16:23
  • 3
    May need to write it as `numbers = list(range(a,b)) + list(range(c,d))` – oatmilkyway Dec 01 '18 at 02:20
  • @plnnr: In Python 3, yes. – Junuxx Dec 01 '18 at 05:06
9

A possible solution would be to just shift the random numbers out of that range. E.g.

def NormalWORange(a, b, sigma):
    r = random.normalvariate(a,sigma)
    if r < a:
        return r-b
    else:
        return r+b

That would generate a normal distribution with a hole in the range (a-b,a+b).

Edit: If you want integers then you will need a little bit more work. If you want integers that are in the range [c,a-b] or [a+b,d] then the following should do the trick.

def RangeWORange(a, b, c, d):
    r = random.randrange(c,d-2*b) # 2*b because two intervals of length b to exclude
    if r >= a-b:
        return r+2*b
    else:
        return r
Ken
  • 1,778
  • 11
  • 10
7

I may have misunderstood your problem, but you can implement this without recursion

def rand(exclude):
    r = None
    while r in exclude or r is None:
         r = random.randrange(1,10)
    return r

rand([1,3,9])

though, you're still looping over results until you find new ones.

zmo
  • 24,463
  • 4
  • 54
  • 90
4

The fastest solution would be this (with a and b defining the exclusion zone and c and d the set of good answers including the exclusion zone):

offset = b - a
maximum = d - offset
result = random.randrange(c, maximum)
if result >= a:
    result += offset
Andrew Gorcester
  • 19,595
  • 7
  • 57
  • 73
0

You still need some range, i.e., a min-max possible value excluding your middle values.

Why don't you first randomly pick which "half" of the range you want, then pick a random number in that range? E.g.:

def rand_not_in_range(a,b):
    rangechoices = ((0,a-b-1),(a+b+1, 10000000))
    # Pick a half
    fromrange = random.choice(rangechoices)
    # return int from that range
    return random.randint(*fromrange)
Francis Avila
  • 31,233
  • 6
  • 58
  • 96
0

Li-aung Yip's answer makes the recursion issue moot, but I have to point out that it's possible to do any degree of recursion without worrying about the stack. It's called "tail recursion". Python doesn't support tail recursion directly, because GvR thinks it's uncool:

http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html

But you can get around this:

http://paulbutler.org/archives/tail-recursion-in-python/

I find it interesting that stick thinks that recursion "feels wrong". In extremely function-oriented languages, such as Scheme, recursion is unavoidable. It allows you to do iteration without creating state variables, which the functional programming paradigm rigorously avoids.

http://www.pling.org.uk/cs/pop.html

  • As someone who is new to programming, this is totally stuff I need to be reading. Thanks! I'm not necessarily allergic to recursion, and to tell the truth I did not realize that even the BDFL of Python showed disdain for it in general; I just had a gut feeling that I didn't need to use it for this particular project. I wouldn't rule it out for future endeavors. –  May 21 '12 at 03:17
  • Hey, if my answer was useful, please vote it up! I need the reputation. – Isaac Rabinovitch Jun 15 '12 at 18:36
  • I thought I had. My mistake :) –  Jun 18 '12 at 03:58