web-dev-qa-db-fra.com

Python somme, pourquoi pas des chaînes?

Python a une fonction intégrée sum, qui est en fait équivalente à:

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

pour tous les types de paramètres sauf les chaînes. Cela fonctionne pour les nombres et les listes, par exemple:

 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

Pourquoi les cordes ont-elles été spécialement laissées de côté?

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

Je semble me souvenir des discussions dans la liste Python pour la raison, donc une explication ou un lien vers un fil expliquant ce serait bien.

Edit: Je suis conscient que la méthode standard est de faire "".join. Ma question est de savoir pourquoi l'option d'utiliser la somme pour les chaînes a été interdite, et aucune interdiction n'était là pour, disons, les listes.

Edit 2: Bien que je pense que ce n'est pas nécessaire compte tenu de toutes les bonnes réponses que j'ai obtenues, la question est: Pourquoi la somme fonctionne-t-elle sur un itérable contenant des nombres ou un itérable contenant des listes mais pas un itérable contenant des chaînes?

61
Muhammad Alkarouri

Python essaie de vous décourager de "sommer" les chaînes. Vous êtes censé les rejoindre:

"".join(list_of_strings)

C'est beaucoup plus rapide et utilise beaucoup moins de mémoire.

Une référence rapide:

$ 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

Modifier (pour répondre à la modification d'OP): Quant à savoir pourquoi les chaînes ont apparemment été "singularisées", je pense qu'il s'agit simplement d'optimiser pour un cas commun, ainsi que d'appliquer les meilleures pratiques: vous pouvez joindre des chaînes beaucoup plus rapidement avec ''. join, l'interdiction explicite des chaînes sur sum le signalera aux débutants.

BTW, cette restriction est en place "pour toujours", c'est-à-dire depuis que le sum a été ajouté en tant que fonction intégrée ( rev. 32347 )

49
rbp

Vous pouvez en effet utiliser sum(..) pour concaténer des chaînes, si vous utilisez l'objet de départ approprié! Bien sûr, si vous allez aussi loin, vous avez déjà suffisamment compris pour utiliser "".join(..) de toute façon ..

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

Voici la source: http://svn.python.org/view/python/trunk/Python/bltinmodule.c?revision=81029&view=markup

Dans la fonction builtin_sum, nous avons ce morceau de code:

     /* 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);
    }

Alors ... c'est votre réponse.

Il est explicitement vérifié dans le code et rejeté.

17
HS.

De les docs :

Le moyen rapide et préféré de concaténer une séquence de chaînes est d'appeler '' .join (séquence).

En faisant sum refuser d'opérer sur les chaînes, Python vous a encouragé à utiliser la bonne méthode.

14
unutbu

Réponse courte: efficacité.

Réponse longue: La fonction sum doit créer un objet pour chaque somme partielle.

Supposons que le temps nécessaire pour créer un objet est directement proportionnel à la taille de ses données. Soit N le nombre d'éléments de la séquence à additionner.

doubles ont toujours la même taille, ce qui fait que le temps d'exécution de sum O (1) × N = O (N).

int (anciennement long) est de longueur arbitraire. Soit M la valeur absolue du plus grand élément de séquence. Alors le temps d'exécution le plus défavorable de sum est lg (M) + lg (2M) + lg (3M) + ... + lg (NM) = N × lg (M) + lg (N!) = O (N log N).

Pour str (où M = la longueur de la chaîne la plus longue), le temps d'exécution le plus défavorable est M + 2M + 3M + ... + NM = M × ( 1 + 2 + ... + N) = O (N²).

Ainsi, les chaînes summing seraient beaucoup plus lentes que les nombres summing.

str.join n'alloue aucun objet intermédiaire. Il préalloue un tampon suffisamment grand pour contenir les chaînes jointes et copie les données de chaîne. Il s'exécute en O (N) temps, beaucoup plus rapide que sum.

11
dan04

La raison pour laquelle

@ dan04 a une excellente explication des coûts d'utilisation de sum sur de grandes listes de chaînes.

L'élément manquant expliquant pourquoi str n'est pas autorisé pour sum est que beaucoup, beaucoup de gens essayaient d'utiliser sum pour les chaînes, et pas beaucoup utilisent sum pour les listes et les tuples et autres structures de données O (n ** 2). Le piège est que sum fonctionne très bien pour les courtes listes de chaînes, mais est ensuite mis en production où les listes peuvent être énormes et les performances ralentissent jusqu'à une analyse. C'était un piège si courant que la décision a été prise d'ignorer le typage de canard dans ce cas et de ne pas autoriser l'utilisation de chaînes avec sum.

10
Ethan Furman

Edit: Déplacé les parties sur l'immuabilité à l'histoire.

Fondamentalement, c'est une question de préallocation. Lorsque vous utilisez une instruction telle que

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

et attendez-vous à ce qu'il fonctionne de manière similaire à une instruction reduce, le code généré ressemble à quelque chose comme

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("$")

Dans chacune de ces étapes, une nouvelle chaîne est créée, ce qui pourrait donner une surcharge de copie à mesure que les chaînes s'allongent de plus en plus. Mais ce n'est peut-être pas le sujet ici. Ce qui est plus important, c'est que chaque nouvelle chaîne sur chaque ligne doit être allouée à sa taille spécifique (qui. Je ne le sais pas, elle doit allouer dans à chaque itération de l'instruction reduce, il peut y avoir des heuristiques évidentes à utiliser et Python peut allouer un peu plus ici et là pour la réutilisation - mais à plusieurs points, la nouvelle chaîne être assez grand pour que cela ne vous aide plus et Python doit allouer à nouveau, ce qui est assez cher.

Une méthode dédiée comme join, a cependant le travail de déterminer la taille réelle de la chaîne avant qu'elle ne commence et ne devrait donc en théorie allouer qu'une seule fois, au début, puis remplir simplement cette nouvelle chaîne, ce qui est beaucoup moins cher que l'autre solution.

4
Debilski

Je ne sais pas pourquoi, mais ça marche!

import operator
def sum_of_strings(list_of_strings):
    return reduce(operator.add, list_of_strings)
3
Dinesh Panchananam