Let's talk about performance. There is a threshold in the neighbourhood of ~30-100 input size (depending on method and density of odd numbers) before which overhead of Numpy makes it slower, and beyond which pure Python code will never be faster than vectorized code.
If you need to use pure Python as you are now, replace your approach with that of stefan. If you don't need to use pure Python, and especially if you expect long input, use Numpy. To compare,
from timeit import timeit
from typing import Sequence
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from numpy.random import default_rng
def orig(source_array: Sequence[int]) -> list[int]:
z,e = [],[]
o = list(enumerate(source_array))
[z.append(i) if i[1] % 2 == 1 else e.append(i) for i in o]
z.sort(key=lambda z: z[1])
[z.insert(i[0],i) for i in e]
return [l[1] for l in z]
def fmc(nums: Sequence[int]) -> list[int]:
odds = [n for n in nums if n % 2]
odds.sort(reverse=True)
return [
odds.pop() if n % 2 else n
for n in nums
]
def stefan(source_array: Sequence[int]) -> list[int]:
odds = sorted(n for n in source_array if n%2)
iter_odd = iter(odds)
return [next(iter_odd) if n%2 else n for n in source_array]
def with_array_index(source_array: np.ndarray) -> np.ndarray:
output = source_array.copy()
odds_idx, = (source_array % 2).nonzero()
output[odds_idx] = np.sort(output[odds_idx])
return output
METHODS = orig, stefan, fmc, with_array_index
def test() -> None:
slybot_input = np.array([0, 1, 9, 5, 4, 8, 2, 4, 1, 2, 3, 2, 10, 11, 25, 25, 24, 35, 1, 2, 3, 4, 5])
expected = np.array([0, 1, 1, 1, 4, 8, 2, 4, 3, 2, 3, 2, 10, 5, 5, 9, 24, 11, 25, 2, 25, 4, 35])
for method in METHODS:
orig_input = slybot_input.copy()
output = method(slybot_input)
assert np.all(output == expected)
assert np.all(orig_input == slybot_input)
def time() -> None:
rand = default_rng(seed=0)
sizes = (10**np.linspace(start=1, stop=4, num=30)).astype(int)
measurements = []
for size in sizes:
for odd_density in (0.1, 0.5, 0.9):
np_inputs = 2*rand.integers(low=-1000, high=1000, size=size)
np_inputs[rand.random(size) < odd_density] += 1
list_inputs = np_inputs.tolist()
all_inputs = (list_inputs,) * (len(METHODS)-2) + (np_inputs,)*2
reps = 2 * sizes.max() // size
for _ in range(reps):
for method, inputs in zip(METHODS, all_inputs):
def run():
return method(inputs)
dur = timeit(stmt=run, number=1)
measurements.append((size, odd_density, method.__name__, dur))
df = pd.DataFrame(
data=measurements,
columns=('size', 'odd_density', 'method', 'duration'),
)
fig, ax = plt.subplots()
sns.lineplot(ax=ax, data=df, x='size', y='duration', hue='method', style='odd_density')
ax.set_xscale('log')
ax.set_yscale('log')
plt.show()
if __name__ == '__main__':
test()
time()
