Home > Software engineering >  How to turn elements of a list into their negative counterpart if their index mod 4 is greater than
How to turn elements of a list into their negative counterpart if their index mod 4 is greater than

Time:08-12

I'm trying to convert a list, say, L = [1, 2, 3, 4, 5, 6, 7, 8, ... , n] into another list L' = [1, 2, -3, -4, 5, 6, -7, -8, ...., ±n] in Python. My question is if there is a shorter/more efficient way of doing that than using a for-loop:

for i in range(len(L)):
    if i%4 > 1:
        L[i] *= -1

e.g. by slicing.

CodePudding user response:

The for loop is fairly efficient, since it doesn't waste space for a new L', unless you need to preserve the original list of course, in which case your code is wrong.

If you just care about speed instead of efficiency, you could start out with a numpy array instead of a list, since you'd be able to come up with an operation that may execute faster than the list operation.

If you care about shorter, the comprehension offered by @wkl is the way to go.

l_prime = [-x if i%4 > 1 else x for i, x in enumerate(l)]

Here's the implementations, timed with timeit (standard library):

from timeit import timeit
import numpy as np
import itertools, operator


def samuel(l):
    for i in range(len(l)):
        if i % 4 > 1:
            l[i] *= -1
    return l


def chai(l):
    return list(map(operator.mul, l, itertools.cycle([1, 1, -1, -1])))


def wkl(l):
    return [-x if i % 4 > 1 else x for i, x in enumerate(l)]


def vladimir(l):
    ar = np.array(l)
    ar[2::4] *= -1
    ar[3::4] *= -1
    return ar.tolist()


# ensure all outcomes are the same
assert samuel(list(range(1000))) == chai(list(range(1000))) == wkl(list(range(1000))) == vladimir(list(range(1000)))

print('samuel: ', timeit(lambda: samuel(list(range(1000))), number=100000))
print('chai: ', timeit(lambda: chai(list(range(1000))), number=100000))
print('wkl: ', timeit(lambda: wkl(list(range(1000))), number=100000))
print('vladimir: ', timeit(lambda: vladimir(list(range(100000))), number=1000))

Result:

samuel:  6.736065300000519
chai:  3.7625152999999045
wkl:  7.069251500000064
vladimir:  6.424349999997503

The numpy solution would be made to be faster, without the list conversions, as stated:

def vladimir_a(ar):
    ar[2::4] *= -1
    ar[3::4] *= -1
    return ar.tolist()


ar = np.array(list(range(1000)))
print('vladimir array: ', timeit(lambda: vladimir_a(ar), number=100000))

Result:

vladimir array:  1.269356699999662

(I'm aware ar will be modified 100,000 times, but it doesn't affect the performance)

Edit: actually, that's unfair - the ranges were all in the timed section, this would be fair (and not so great)

def vladimir_a(ar):
    ar[2::4] *= -1
    ar[3::4] *= -1
    return ar.tolist()


print('vladimir array: ', timeit(lambda: vladimir_a(np.array(range(1000))), number=100000))

Result:

vladimir array:  6.5144264999998995

So you may need to do some timing in your actual use case to find what's fastest there. Constructing the same array 100000 times (or the same list) clearly isn't what you're doing, and one would expect you are operating on a large dataset for speed to even be a consideration.

CodePudding user response:

Here's a comparison of various methods:

import time
import numpy as np

n = int(1e7)

s1 = time.time()
L1 = list(range(1, n))
for i in range(len(L1)):
    if i % 4 > 1:
        L1[i] *= -1
e1 = time.time()

s2 = time.time()
L2 = list(range(1, n))
L2 = np.array(L2)
L2[np.arange(n - 1) % 4 > 1] *= -1
L2 = L2.tolist()
e2 = time.time()

s3 = time.time()
L3 = list(range(1, n))
L3 = np.array(L3)
k = (n - 1) // 4
pat = [False, False, True, True]
idx = np.concatenate((np.tile(pat, k), pat[:n - k * 4 - 1])).astype(bool)
L3[idx] *= -1
L3 = L3.tolist()
e3 = time.time()

s4 = time.time()
L4 = list(range(1, n))
L4 = np.array(L4)
L4[2::4] *= -1
L4[3::4] *= -1
L4 = L4.tolist()
e4 = time.time()

assert all(np.array(L1) == np.array(L2))
assert all(np.array(L1) == np.array(L3))
assert all(np.array(L1) == np.array(L4))

print(f'time1: {e1 - s1:.2f}s, time2: {e2 - s2:.2f}s, time3: {e3 - s3:.2f}s, time4: {e4 - s4:.2f}s')

prints

time1: 3.76s, time2: 1.72s, time3: 1.58s, time4: 1.52s

CodePudding user response:

With Python 3, you can use itertools's cycle to repeat an iterable over and over again, and then use map to combine two iterables with some function:

>>> import itertools, operator
>>> list(map(operator.mul, range(10), itertools.cycle([1, 1, -1, -1])))
[0, 1, -2, -3, 4, 5, -6, -7, 8, 9]
>>> list(map(operator.mul, [2, 3, 5, 7, 11, 13, 17, 19], itertools.cycle([1, 1, -1, -1])))
[2, 3, -5, -7, 11, 13, -17, -19]

CodePudding user response:

To add to @Grismar’s answer, numpy arrays can be dramatically faster than iteration because they do operations on arrays in parallel (even on CPUs).

Here is how to solve your problem using numpy arrays (with slicing):

import numpy as np

# If you already have a list `L`:
ar = np.array(L)

# But it’s faster to use numpy to 
# generate the array of numbers 
# (here: integers from 1 to n):
n = 15
ar = np.arange(1, n   1)

ar[2::4] *= -1
ar[3::4] *= -1

# You can convert a numpy array into a list. 
# Note that this step may not be necessary 
# at all, because numpy arrays can be
# used where you would use lists. 
# But for completeness, here is how to do it:
L_prime = ar.tolist()

A function you can use:

def make_array(n):
    ar = np.arange(1, n   1)
    ar[2::4] *= -1
    ar[3::4] *= -1
    return ar
  • Related