Cortar una lista en Python sin generar una copia
Tengo el siguiente problema.
Dada una lista de números enteros
L
, necesito generar todas las sublistasL[k:]
for k in [0, len(L) - 1]
, sin generar copias .
¿Cómo logro esto en Python? ¿Con un objeto buffer de alguna manera?
La respuesta corta
La división de listas no genera copias de los objetos de la lista; simplemente copia las referencias a ellos. Esa es la respuesta a la pregunta formulada.
la respuesta larga
Pruebas de valores mutables e inmutables.
Primero, probemos la afirmación básica. Podemos demostrar que incluso en el caso de objetos inmutables como los números enteros, solo se copia la referencia. Aquí hay tres objetos enteros diferentes, cada uno con el mismo valor:
>>> a = [1000 + 1, 1000 + 1, 1000 + 1]
Tienen el mismo valor, pero puedes ver que son tres objetos distintos porque tienen id
s diferentes:
>>> map(id, a)
[140502922988976, 140502922988952, 140502922988928]
Cuando los cortas, las referencias siguen siendo las mismas. No se han creado nuevos objetos:
>>> b = a[1:3]
>>> map(id, b)
[140502922988952, 140502922988928]
El uso de diferentes objetos con el mismo valor muestra que el proceso de copia no se molesta en realizar prácticas : simplemente copia directamente las referencias.
Las pruebas con valores mutables dan el mismo resultado:
>>> a = [{0: 'zero', 1: 'one'}, ['foo', 'bar']]
>>> map(id, a)
[4380777000, 4380712040]
>>> map(id, a[1:]
... )
[4380712040]
Examinar la sobrecarga de memoria restante
Por supuesto, las propias referencias están copiadas. Cada uno cuesta 8 bytes en una máquina de 64 bits. Y cada lista tiene su propia sobrecarga de memoria de 72 bytes:
>>> for i in range(len(a)):
... x = a[:i]
... print('len: {}'.format(len(x)))
... print('size: {}'.format(sys.getsizeof(x)))
...
len: 0
size: 72
len: 1
size: 80
len: 2
size: 88
Como nos recuerda Joe Pinsonault , esos gastos generales se acumulan. Y los objetos enteros en sí mismos no son muy grandes: son tres veces más grandes que las referencias. Entonces, esto le ahorra algo de memoria en un sentido absoluto, pero asintóticamente, podría ser bueno poder tener múltiples listas que sean "vistas" en la misma memoria.
Ahorrar memoria mediante el uso de vistas
Desafortunadamente, Python no proporciona una manera fácil de producir objetos que sean "vistas" en listas. ¡O tal vez debería decir "afortunadamente"! Significa que no tienes que preocuparte por el origen de una porción; Los cambios en el original no afectarán el corte. En general, eso hace que el razonamiento sobre el comportamiento de un programa sea mucho más fácil.
Si realmente desea ahorrar memoria trabajando con vistas, considere usar numpy
matrices. Cuando se divide una numpy
matriz, la memoria se comparte entre el segmento y el original:
>>> a = numpy.arange(3)
>>> a
array([0, 1, 2])
>>> b = a[1:3]
>>> b
array([1, 2])
¿Qué sucede cuando modificamos a
y volvemos a mirar b
?
>>> a[2] = 1001
>>> b
array([ 1, 1001])
Pero esto significa que debe asegurarse de que cuando modifica un objeto, no esté modificando otro sin darse cuenta. Ésa es la contrapartida del uso numpy
: ¡menos trabajo para la computadora y más trabajo para el programador!
Dependiendo de lo que estés haciendo, es posible que puedas utilizar islice
.
Dado que opera mediante iteración, no creará nuevas listas, sino que simplemente creará iteradores con yield
elementos de la lista original según lo solicitado para sus rangos.
Una alternativa simple a islice
eso no itera a través de elementos de la lista que no necesita:
def listslice(xs, *args):
for i in range(len(xs))[slice(*args)]:
yield xs[i]
Uso:
>>> xs = [0, 2, 4, 6, 8, 10]
>>> for x in listslice(xs, 2, 4):
... print(x)
4
6
Generalmente, la división de listas es la mejor opción.
Aquí hay una comparación rápida de rendimiento:
from timeit import timeit
from itertools import islice
for size in (10**4, 10**5, 10**6):
L = list(range(size))
S = size // 2
def sum_slice(): return sum(L[S:])
def sum_islice(): return sum(islice(L, S, None))
def sum_for(): return sum(L[i] for i in range(S, len(L)))
assert sum_slice() == sum_islice()
assert sum_slice() == sum_for()
for method in (sum_slice, sum_islice, sum_for):
print(f'Size={size}, method={method.__name__}, time={timeit(method, number=1000)} ms')
Resultados:
Size=10000, method=sum_slice, time=0.0298 ms
Size=10000, method=sum_islice, time=0.0449 ms
Size=10000, method=sum_for, time=0.2500 ms
Size=100000, method=sum_slice, time=0.3262 ms
Size=100000, method=sum_islice, time=0.4492 ms
Size=100000, method=sum_for, time=2.4849 ms
Size=1000000, method=sum_slice, time=5.4092 ms
Size=1000000, method=sum_islice, time=5.1139 ms
Size=1000000, method=sum_for, time=26.198 ms