3

What is the fastest way to make a list-like object containing integers/floats (very simple datatypes) in python?

What do I mean by "list-like"?

This means I want to have an object that supports the two (very) basic operations of a list: getting an object in a certain index (1) and changing its value (2).

What posts did I come across before posting this and why didn't they solve my problem?

I came across these two: [1] [2]

They didn't solve my problem because all the solutions to them were simply too slow: in my PC array.array('i',(0,)*10 ** 8) resulted in an error (lol); [0 for _ in range(10**8)] took about 15 seconds (wow!); [0] * 10 ** 8 took 2.3 seconds; [None] * 10 ** 8 took 1.8 seconds; (1.8sec could be faster...)

What did I try to do?

I tried using the ctypes module

from ctypes import c_int
array = (c_int * 10 ** 8)()

The code above took only 0.7 seconds ... but is there a way to make it faster? Besides being fast it has some disadvantages:

  1. As it uses the c/c++ variables' skeleton, the integers in it will be in a "not as unlimited as python" integer value range
  2. You can't have more than one datatype in the list
  3. You have to import a module to use it

Is it really possible to do what I'm asking? Is there a faster way rather than using the ctypes module? If so make sure that you are using a 'built-in' / 'pre-installed' module.

Edit:

Why can't I simply install some module, like numpy?

I'm using python for competitive programming and most interpreters/judges just won't allow external libraries.

Can we have custom objects stored with array.array?

I can see many of the answers use the array function of the array module. They all use 'i' to specify we want to store integers. Is it possible to make a class and create an `array.array' containing it? For example:

class Point:
 def __init__(self, x, y):
  self.x = x
  self.y = y

# make array.array object with all indexes containing a Point with atributes x and y with value 0
# an example with a list of what I want to do is this:
# l = [Point(0, 0) for _ in range(10**3)]
ArthurQ
  • 118
  • 1
  • 8
  • 3
    Have you considered `numpy`? If you're talking about performance in large numeric computations, you should not be worried about importing modules. See [Why NumPy instead of Python lists?](https://stackoverflow.com/questions/993984/why-numpy-instead-of-python-lists) – jpp Apr 21 '18 at 14:17
  • you've got `bytearray(10**8)` too. Values are limited in range (0-255) but it's pretty fast – Jean-François Fabre Apr 21 '18 at 14:27

5 Answers5

4

array.array('i',(0,) * 10**8) resulted in an error (lol)

You didn't specify what error you got - that works for me, although it's not very fast because it builds an intermediate tuple and immediately discards it. Using Python's built-in types, array.array will probably result in best performance, provided you avoid the tuple:

a = array.array('i', (0,)) * 10**8

The code above took only 0.7 seconds ... but is there a way to make it faster?

It will be hard to beat array.array if you're not allowed to create or import C extensions. On my several years old machine the above takes 0.6 seconds. You can further optimize it by increasing the size of the initial array. For example, this produces the same result, but is almost 3x faster (!):

# 0.22 s
a = array.array('i', (0,) * 10) * 10**7

On my machine the following version works best:

# 0.19 s
a = array.array('i', (0,) * 100) * 10**6

Further increasing the initial array size doesn't help and soon starts degrading performance.

To get better efficiency, consider alternative approaches, such as a lazy list or an altogether different data structure tailored for your use case. Given the context of a competition, that might be what is actually being sought.

Be aware, however, that each solution will have different tradeoffs. For example, a lazy array such as one provided by @KonstantinNikitin will be extremely efficient to construct, but its __getitem__ and __setitem__, implemented in pure Python, will be several orders of magnitude slower than those of list or array.array. Which is better for you boils down to what operations are more frequent in your program, and that is up to you to find out.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 1
    I didn't specify the eror because when I ran it on my machine it crashed and I had to restart it lol ... but your solution is the best one so far! – ArthurQ Apr 21 '18 at 14:51
  • 1
    I really like the idea of your `array.array`! Also may be useful to do some wrapping above the bytearray, where you read, for example, to access item `i` values from [4 * i .. 4 * i + 4). >>> timeit.timeit('x = bytearray(10 ** 8)', '', number=1) 0.0671752679918427 >>> timeit.timeit('x = array.array("i", (0,) * 100) * 10 ** 6', 'import array', number=1) 0.30561141700309236 – Konstantin Nikitin Apr 21 '18 at 14:55
  • @ArthurQ Ouch. While a 100 million element tuple is rather large by Python standards, it's not huge enough that it should be causing a crash, especially as it only contains references to the same `0` object. – user4815162342 Apr 21 '18 at 14:55
  • @KonstantinNikitin `array.array` is probably the most underrated Python standard library module, and was so even before it was made somewhat obsolete by `numpy`. Interestingly, in PyPy `array.array('i', (0,)) * 10**8` is instantaneous because PyPy recognizes and special-cases construction of an array consisting of all zeros. – user4815162342 Apr 21 '18 at 14:57
  • By the way ... can you also use this module to make an array of custom objects? For example: making an array of points (consider you coded the 'Point' class yourself and it has the 'x' and 'y' attributes) – ArthurQ Apr 21 '18 at 15:06
  • @ArthurQ Not directly, but you could create two arrays, one for all x's and one for all y's. If done right, this could be encapsulated in a `PointArray` class that hides the gory details. But please ask a separate question about details of that, because adding this new requirement renders all existing answers obsolete. – user4815162342 Apr 21 '18 at 15:40
  • @user4815162342 yeah but what if I need to sort them based on the distance from the origin (example) using only the 'sort' function (keeping x and y coords)? – ArthurQ Apr 21 '18 at 15:45
  • @ArthurQ You can sort them by sorting a list of indices and then rearranging both arrays. I'm not sure if performance will be acceptable, but sorting a list of `Point`s would not be much faster either. Again, this sounds like good material for a new question. – user4815162342 Apr 21 '18 at 16:03
  • @KonstantinNikitin The trouble with wrapping the `bytearray` is that then you pay the price of `__getitem__` and `__setitem__` implemented in Python, in which case you might as well use your `LazyArray` and have instantaneous construction. – user4815162342 Apr 21 '18 at 16:09
3

I would just use the numpy module, which supports fast array operations.

For example making an array with numbers 0 to 10**8:

import numpy as np
import time

b = time.time()
a = np.linspace(0, 10**8, 10**8)
c = time.time()
print(c-b)
>>>0.5000154972076416

Or making an array of 0s that is 10**8 long:

b = time.time()
a = np.zeros(shape=(10**8,))
c = time.time()
print(c-b)
>>>0.0

The main reason why numpy is this fast is because it is implemented in C.

EDIT: If you want to do it with only pre-installed packages, you can try using the array package:

import array
import time
r = time.time()
a = array.array('i', [0]) * (10**8)
print(time.time()-r)
>>>0.15627217292785645
Primusa
  • 13,136
  • 3
  • 33
  • 53
2

I'd say that you can try different approaches:

1) numpy. It's really a standard for arrays. It comes with a cost of crossing Python <-> C boundary for each operation, but it really depends on your task.

x = numpy.array(10 ** 8)

timeit.timeit('x = numpy.array(10 ** 8)', 'import numpy', number=1)
4.195800283923745e-05

2) lazy initialization (like JavaScript arrays).

class LazyArray:
    def __init__(self, size):
        self.storage = {}
        self.size = size

    def check(self, i):
        if i < 0 or i >= self.size:
            raise RuntimeError() 

    def __getitem__(self, i):
        self.check(i)
        return self.storage.get(i, 0)

    def __setitem__(self, i, value):
        self.check(i)
        self.storage[i] = value

x = LazyArray(10 ** 8)
x[10]
>> 0
x[10] = 5
x[10]
>> 0
Konstantin Nikitin
  • 2,256
  • 1
  • 17
  • 12
  • Using dictionaries can be slow ... insertion and getting index operations may take O(1) but they can also take O(n) (worst case) – ArthurQ Apr 21 '18 at 14:38
  • Yeah, lazy initialisation has it's own price. You can try to mix then. Like manually splitting the interval to list of [0 .. sqrt(size)] dicts. – Konstantin Nikitin Apr 21 '18 at 14:44
2

If you really only want these two properties:

getting an object in a certain index (1) and changing its value (2)

then you can just use a collections.defaultdict:

import collections
my_list = collections.defaultdict(lambda: 0)

which is rather fast (~0.4 μs):

$ python3 -m timeit -s 'import collections' 'collections.defaultdict(lambda: 0)' 
1000000 loops, best of 3: 0.417 usec per loop

however, actually using it will probably be quite a bit slower than any of the types suggested in other answers.

ash
  • 5,139
  • 2
  • 27
  • 39
0

For cases where you only need integers from 0 to 255, bytearray objects are quite fast to create:

>>> timeit.timeit('bytearray(100000)', number=1000)
0.005567271093696036
>>> timeit.timeit('array.array("B", [0])*100000', 'import array', number=1000)
0.36631167401839093
>>> timeit.timeit('array.array("i", [0])*100000', 'import array', number=1000)
0.56494557472422

Unlike array.array, this zeros the allocation directly instead of copying from an object initialized with zeros.

user2357112
  • 260,549
  • 28
  • 431
  • 505