4

I need to create a range skipping over every 4th number, starting from 5. For example, if the range a is from 1-20, then the numbers 5,9,13,17 will be excluded.

a = [1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20]

What I tried is creating a regular range, and then a second range consisting the numbers I want to skip, and then remove the second range from the first one.

a = list(range(1,21))
b = list(range(5,21,4))
for x in b:
   if x in a:
      a.remove(x)

This works, but not for a very large range. Is there a more efficient way to do it?

user3483203
  • 50,081
  • 9
  • 65
  • 94
Blue
  • 67
  • 1
  • 6
  • Possible duplicate of [Skip over a value in the range function in python](https://stackoverflow.com/questions/24089924/skip-over-a-value-in-the-range-function-in-python) – Rushabh Mehta Aug 03 '18 at 01:07
  • @RushabhMehta Not much of a duplicate. – miradulo Aug 03 '18 at 01:24
  • 1
    ranges only support start, stop, step, by definition. Which parts of the range API do you need to keep? sized? O(1) membership? O(1) memory? reversed? slicable? – wim Aug 03 '18 at 01:28

7 Answers7

8

Solution:

For efficiency, I would recommend using a generator expression like this:

r = (x for x in range(1,21) if x not in range(5,21,4))

or, equivalently, and without needing to write the upper bound twice:

r = (x for x in range(1,21) if x == 1 or x % 4 != 1)

You can use this generator as you would use a normal sequence (list/tuple)*, and convert the generator to a list with list() if you absolutely need to.

Justification:

The advantage of this approach is that it does not require storing all of the items in memory, so you can make the upper bound arbitrarily large without any performance problems.


*(Sort of-there are caveats, as mentioned by the commenters below. e.g. if you want fast membership checks, you would be better off just using the two ranges separately)

Ollin Boer Bohan
  • 2,296
  • 1
  • 8
  • 12
  • A generator is different from a range object, though. ranges are sized (len) and support O(1) membership test. – wim Aug 03 '18 at 01:23
  • Upvote for the second one, I actually find that more flexible, And understandable, but that may just be my warped mind :-) – paxdiablo Aug 03 '18 at 01:27
4

Use a set and another list comprehension:

a = range(1, 21)
b = set(range(5, 21, 4))

[i for i in a if i not in b]
# [1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20]

You could remove the set from the second range, but I am finding this is slower than set inclusion checking:

Functions

def chris(m):
  a = range(1, m)
  b = set(range(5, m, 4))
  return [i for i in a if i not in b]

def chris2(m):
  a = range(1, m)
  b = range(5, m, 4)
  return [i for i in a if i not in b]


def ollin(m):
  return list(x for x in range(1,m) if x not in range(5,m,4))

def ollin2(m):
  return list(x for x in range(1,m) if x == 1 or x % 4 != 1)

def smac(m):
  return [v for i, v in enumerate(range(1,m)) if i == 0 or i % 4 != 0]

Setup

from timeit import timeit

import pandas as pd
import matplotlib.pyplot as plt

res = pd.DataFrame(
       index=['chris', 'chris2', 'ollin', 'ollin2', 'smac'],
       columns=[10, 50, 100, 500, 1000, 5000, 10000],
       dtype=float
)

for f in res.index: 
    for c in res.columns:
        stmt = '{}(c)'.format(f)
        setp = 'from __main__ import c, {}'.format(f)
        res.at[f, c] = timeit(stmt, setp, number=50)

ax = res.div(res.min()).T.plot(loglog=True) 
ax.set_xlabel("N"); 
ax.set_ylabel("time (relative)");

plt.show()

enter image description here

user3483203
  • 50,081
  • 9
  • 65
  • 94
2

You can use this list comprehension:

>>> print ([v for i, v in enumerate(range(1,21)) if i == 0 or i % 4 != 0])
[1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20]
smac89
  • 39,374
  • 15
  • 132
  • 179
1

Perhaps a set difference?

numItems = 21
r1 = set(range(1,numItems))
r2 = set(range(5,numItems,4))

r3 = list(r1 - r2)
Kevinj22
  • 966
  • 2
  • 7
  • 11
  • I had that same thought of using sets but was didnt think it would preserve the integers in increasing order but was surprised to see that the order was preserved! list(set((5,4,3,2,1))) = [1, 2, 3, 4, 5] ! However list(set(('y', 'c', 'r', 'd'))) = ['d', 'c', 'y', 'r'] weird ... – quizdog Feb 12 '22 at 04:58
1

If we consider numpy

import numpy as np 
a=np.arange(1,21)
a[np.logical_or(a%4!=1,a==1)]
Out[209]: array([ 1,  2,  3,  4,  6,  7,  8, 10, 11, 12, 14, 15, 16, 18, 19, 20])
BENY
  • 317,841
  • 20
  • 164
  • 234
1

Writing your own generator function could be useful:

def weird_range():
   for i in range(1, 21):
      if i < 5 or i % 4 != 1:
         yield i

>>> list(weird_range())
[1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20]

This will let you do all manner of bizarre sequences.


Alternate is to create your own range object:

class WeirdRange:
   def __contains__(self, val):
      return val < 5 or val % 4 != 1

>>> list(_ for _ in range(1,21) if _ in WeirdRange())
[1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20]

Or you can create your own Iterator:

class WeirdIterator:

   def __init__(self):
      self._idx = 0

   def __iter__(self):
      return self

   def __next__(self):
      self._idx += 1
      if self._idx >= 5  and  self._idx % 4 == 1:
         self._idx += 1
      if self._idx > 20:
         raise StopIteration()
      return self._idx

>>> list(WeirdIterator())
[1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20]
AJNeufeld
  • 8,526
  • 1
  • 25
  • 44
0

a = list(range(1,21))

for x in a:
    if x!=1 and x%4==1:
        continue
    print(x)