2010-08-19 24 views
54

Python ha construido en función de sum, que es efectivamente equivalente a:suma Python, ¿por qué no las cadenas?

def sum2(iterable, start=0): 
    return start + reduce(operator.add, iterable) 

para todo tipo de parámetros, excepto cuerdas. Funciona para números y listas, por ejemplo:

sum([1,2,3], 0) = sum2([1,2,3],0) = 6 #Note: 0 is the default value for start, but I include it for clarity 
sum({888:1}, 0) = sum2({888:1},0) = 888 

¿Por qué las cadenas se omitieron especialmente?

sum(['foo','bar'], '') # TypeError: sum() can't sum strings [use ''.join(seq) instead] 
sum2(['foo','bar'], '') = 'foobar' 

Creo recordar las discusiones en la lista de Python por la razón, por lo que una explicación o un enlace a un tema explicando que estaría bien.

Editar: Soy consciente de que la forma estándar es hacer "".join. Mi pregunta es por qué la opción de usar sum para cadenas fue prohibida, y no hubo prohibición para, por ejemplo, listas.

Editar 2: Aunque creo que esto no es necesario dado todas las buenas respuestas que obtuve, la pregunta es: ¿Por qué funciona suma en una iterable que contenga números o un iterable que contiene las listas, pero no un iterables que contiene cadenas?

+0

Porque no tiene sentido "sumar" cadenas. – NullUserException

+18

@NullUserException: tiene tanto sentido "sumar" cadenas como lo es para "sumar" listas. –

+1

@Muhammad Alkarouri: al sumar una secuencia, se suman los elementos de la secuencia. ¿Cuáles son los elementos de una cadena? Caracteres individuales, que son, todavía, no números. No puedes sumarlos. Podría "concatenarlos", pero, curiosamente, ya están concatenados en la cadena. –

Respuesta

43

Python intenta desanimarlo para que no "agregue" cadenas. Se supone que debes unirte a ellos:

"".join(list_of_strings) 

Es mucho más rápido y usa mucha menos memoria.

Un punto de referencia rápida:

$ python -m timeit -s 'import operator; strings = ["a"]*10000' 'r = reduce(operator.add, strings)' 
100 loops, best of 3: 8.46 msec per loop 
$ python -m timeit -s 'import operator; strings = ["a"]*10000' 'r = "".join(strings)' 
1000 loops, best of 3: 296 usec per loop 

Editar (para responder edición de OP): En cuanto a por qué las cadenas fueron aparentemente "señalados", creo que es simplemente una cuestión de optimización para un caso común, así como de aplicar las mejores prácticas: puede unir cadenas mucho más rápido con '' .unirse, por lo que prohibir explícitamente las cadenas en sum indicará esto a los novatos.

BTW, esta restricción ha sido en el lugar "siempre", es decir, desde la sum se añadió como una función incorporada (rev. 32347)

+0

El punto de referencia es útil. Su respuesta sería más completa si compara las listas 'reducir' vs' suma' para obtener el mismo resultado. Entonces, el argumento para cadenas funcionaría solo para cadenas. –

+1

En realidad, creo que es al revés: 'reduce' y' sum' son similares para las listas, porque hacen más o menos lo mismo. Es por eso que utilicé este punto de referencia: 'reducir', que representa' suma', en lugar de 'unirse', que es una forma optimizada de" agregar "cadenas. – rbp

+1

Me siento viejo ahora. Comprendo "para siempre" en Python como antes 2.0. Las cosas que se hacen después de la introducción de nuevas clases de estilo no son para siempre. –

13

De the docs:

la forma preferida, rápido para concatenar una secuencia de cadenas es llamando al '' .join (secuencia).

Al hacer que sum se nieguen a operar en cadenas, Python lo ha alentado a utilizar el método correcto.

+1

Así que es parte de la filosofía de 'una forma de hacerlo'. (Como podrían haber hecho cadenas de suma y suma especialmente) –

+3

@ThomasAhle: Existe una razón computacional por la cual 'sum' es la herramienta incorrecta para el trabajo. 'sum' construye el resultado incrementalmente. Hacerlo con cadenas requiere (potencialmente muchas) cadenas temporales inmutables. Si se hace ingenuamente, este proceso requiere mucha memoria temporal y mucha copia.Por el contrario, si se hace con ''' .join()', la cantidad apropiada de memoria se asigna desde el principio, y el resultado se genera inmediatamente. Ver [la exhortación de Martelli] (http://stackoverflow.com/questions/1349311/python-string-join-is-faster-than-but-whats-wrong-here/1350289#1350289). – unutbu

+2

Claro, pero puedes hacer muchas cosas ineficientes en Python, y a menudo no importa. No puedo pensar en ningún otro caso que no sea "suma de cadenas" donde hay pruebas en contra de hacer algo, codificadas en el código Python C. –

3

Edit: Cambié las partes acerca de la inmutabilidad a la historia.

Básicamente, es una cuestión de preasignación.Cuando se utiliza una sentencia como

sum(["a", "b", "c", ..., ]) 

y esperar que funcione similar a una declaración reduce, el código generado se ve algo así como

v1 = "" + "a" # must allocate v1 and set its size to len("") + len("a") 
v2 = v1 + "b" # must allocate v2 and set its size to len("a") + len("b") 
... 
res = v10000 + "$" # must allocate res and set its size to len(v9999) + len("$") 

En cada uno de estos pasos se crea una nueva cadena, que para uno podría dar un poco de sobrecarga de copia ya que las cadenas son cada vez más largas. Pero ese no es el punto aquí. Lo que es más importante, es que cada cadena nueva en cada línea debe ser asignada a su tamaño específico (que. No lo sé debe asignar en cada iteración de la declaración reduce, puede haber heurística obvia para usar y Python podría asignar un poco más aquí y allá para su reutilización, pero en varios puntos la nueva cadena será lo suficientemente grande para que esto no sirva más y Python debe asignar nuevamente, que es bastante caro.

Un método dedicado como join Sin embargo, el trabajo consiste en determinar el tamaño real de la cadena antes de que comience y, por lo tanto, en teoría solo asignar una vez, al principio y luego simplemente llenar esa nueva cadena, que es mucho más económica que la otra solución.

+1

Es cierto, pero esa no es toda la historia. Los enteros son inmutables también. Pero la operación de unión en cadenas es especializada, tiene en cuenta toda la lista y, por lo tanto, es mucho más rápida. – rbp

+0

Sí, vale la inmutabilidad no es el verdadero problema aquí. (Aunque puedo imaginar que los enteros de tamaño de registro sufren menos de inmutabilidad en el lado de Python que las cadenas). Pero creo que la preasignación en realidad * es * la historia completa. – Debilski

+1

@Debiliski: La preasignación fue probablemente el eslabón perdido para mí; explica por qué '" ", join' se comporta mucho mejor que una suma genérica. Tuve que mirar el [código] (http://svn.python.org/projects/python/trunk/Objects/stringobject.c) para entender. Creo que la inmutabilidad es una pista falsa. –

23

¡De hecho puede usar sum(..) para concatenar cadenas, si usa el objeto de inicio apropiado! Por supuesto, si usted va tan lejos que ya han entendido lo suficiente para usar "".join(..) todos modos ..

>>> class ZeroObject(object): 
... def __add__(self, other): 
... return other 
... 
>>> sum(["hi", "there"], ZeroObject()) 
'hithere' 
+4

Esto me parece muy interesante. Por supuesto, es demasiado inteligente a la mitad, pero se suma a la comprensión de esta característica. –

+1

Y es un poco más rápido que la versión reducida. – Debilski

+8

Aún así es extraño que Python busque cadenas, pero no listas o tuplas. – Philipp

10

respuesta corta: Eficiencia.

Respuesta larga: La función sum tiene que crear un objeto para cada suma parcial.

Supongamos que la cantidad de tiempo necesaria para crear un objeto es directamente proporcional al tamaño de sus datos. Deje N denotar el número de elementos en la secuencia para sumar.

double s son siempre del mismo tamaño, lo que hace correr O (1) x tiempo sum 's N = O (N).

int (anteriormente conocido como long) es arbitary-length. Deje que M denote el valor absoluto del elemento de secuencia más grande. Entonces, el peor tiempo de ejecución de sum es lg (M) + lg (2M) + lg (3M) + ... + lg (NM) = N × lg (M) + lg (N!) = O (N registro N).

Para str (donde M = la longitud de la cadena más larga), el peor tiempo de ejecución es M + 2M + 3M + ... + NM = M × (1 + 2 + ... + N) = O (N²).

Por lo tanto, sum cadenas ming sería mucho más lento que sum números de ming.

str.join no asigna ningún objeto intermedio. Preasigna un búfer lo suficientemente grande como para contener las cadenas unidas y copia los datos de cadena. Se ejecuta en O (N) vez, mucho más rápido que sum.

+4

Ese argumento es incorrecto porque lo mismo se aplicaría a las listas y tuplas, que se pueden sumar. Según lo establecido por HS, Python verifica explícitamente las cadenas, y * solo * para las cadenas, lo que simplemente no tiene sentido. – Philipp

+4

@Philipp: La pieza que falta es que mucha, mucha gente estaba tratando de usar 'sum' para cadenas, y no muchos usan' sum' para listas y tuplas. La trampa es que 'sum' funciona bien para listas cortas de cadenas, pero luego se pone en producción donde las listas pueden ser enormes y el rendimiento se ralentiza. Esta fue una trampa tan común que se tomó la decisión de ignorar el tipado de patos en esta instancia y no permitir que las cadenas se usen con 'suma '. –

+1

Excelente respuesta. Excepto que "sería mucho más lento" tiene que ser "es mucho más lento" como se ha demostrado. – steffen

14

Aquí está la fuente: http://svn.python.org/view/python/trunk/Python/bltinmodule.c?revision=81029&view=markup

En la función builtin_sum tenemos este trozo de código:

 /* reject string values for 'start' parameter */ 
     if (PyObject_TypeCheck(result, &PyBaseString_Type)) { 
      PyErr_SetString(PyExc_TypeError, 
       "sum() can't sum strings [use ''.join(seq) instead]"); 
      Py_DECREF(iter); 
      return NULL; 
     } 
     Py_INCREF(result); 
    } 

Así que .. Esa es tu respuesta.

Se ha verificado explícitamente en el código y se ha rechazado.

+4

Es interesante ver el código, pero la pregunta era "* ¿Por qué * no son cadenas sumadas", no "* ¿Cómo * excluyeron cadenas de la suma?" ... – Edmund

+3

Bueno, quise decir que la razón por la que no funciona es porque está explícitamente prohibido en el código. Otros parecían responder al explicar por qué no debías hacerlo. –

9

la razón por la

@ dan04 tiene una excelente explicación de los costos de utilizar sum en grandes listas de cadenas.

La pieza que falta en cuanto a porqué str no está permitido para sum es que muchas, muchas personas estaban tratando de utilizar sum para cuerdas, y no muchos utilizan sum para las listas y tuplas y otros ** O (n 2) estructuras de datos . La trampa es que sum funciona bien para listas cortas de cadenas, pero luego se pone en producción donde las listas pueden ser enormes y el rendimiento se ralentiza. Esta fue una trampa tan común que se tomó la decisión de ignorar el tipado de patos en esta instancia, y no permitir que las cadenas se usen con sum.

+9

¿Hay alguna razón por la que la implementación 'sum()' no solo llame a '' '.join() 'cuando se encuentra una cadena en' sum() 'en lugar de devolver un error que le indica que llame a'' '. join() '? – Slater

+0

@Slater: Sí. Pero no recuerdo de qué se trataba. :( –

2

No sé por qué, pero esto funciona!

import operator 
def sum_of_strings(list_of_strings): 
    return reduce(operator.add, list_of_strings) 
+2

Porque 'reduce' no es' suma', pero aún tendrá un rendimiento horrible para listas grandes. –

Cuestiones relacionadas