31 votes

Str.replace (..). Replace (..) ad nauseam est-il un idiome standard en Python?

Par exemple, disons que je voulais une fonction d'échapper à une chaîne pour une utilisation en HTML (comme dans Django échapper à filtre):

    def escape(string):
        """
        Returns the given string with ampersands, quotes and angle 
        brackets encoded.
        """
        return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("'", '&#39;').replace('"', '&quot;')

Cela fonctionne, mais il devient laid rapidement et semble avoir une mauvaise performance algorithmique (dans cet exemple, la chaîne est à plusieurs reprises traversé 5 fois). Ce qui serait mieux, c'est quelque chose comme ceci:

    def escape(string):
        """
        Returns the given string with ampersands, quotes and angle 
        brackets encoded.
        """
        # Note that ampersands must be escaped first; the rest can be escaped in 
        # any order.
        return replace_multi(string.replace('&', '&amp;'),
                             {'<': '&lt;', '>': '&gt;', 
                              "'": '&#39;', '"': '&quot;'})

Une telle fonction existe pas, ou est le standard de Python langage à utiliser ce que j'ai écrit avant?

23voto

Mike Graham Points 22480

Avez-vous une application qui est en cours d'exécution trop lente et que vous avez fourni à trouver qu'une ligne comme cet extrait est à l'origine pour être lent? Les goulots d'étranglement se produisent à des endroits inattendus.


Le courant extrait traverse la chaîne de 5 fois, de faire une chose chaque fois. Vous suggérez traversent une fois, sans doute de faire cinq choses à chaque fois (ou au moins de faire quelque chose à chaque fois). Il n'est pas clair que cela va automatiquement faire un meilleur travail pour moi. Actuellement, l'algorithme est O(n*m) (en supposant que la longueur de la chaîne est plus longue que les choses dans les règles), où n est la longueur de la chaîne et m est le nombre de règles de substitution. Vous pouvez, je pense, de réduire la complexité algorithmique de quelque chose comme O(n*log(m)) et dans le cas précis, nous sommes dans le cas où la première des choses, ne sont qu'un personnage (mais pas dans le cas de plusieurs appels d' replace en général)-O(n), mais ce n'est pas grave puisque m est de 5 , mais n est illimitée.

Si m est constante, alors, la complexité de ces deux solutions se passe vraiment à O(n). Il n'est pas clair pour moi que cela va être une tâche digne d'essayer à son tour cinq simple passe dans un complexe, le temps réel dont je ne devine pas à l'heure actuelle. Si il y avait quelque chose sur ce qui pourrait rendre compte de l'échelle de mieux, j'aurais pensé que c'était beaucoup plus intéressant tâche.

Tout faire sur une passe plutôt que de passes consécutives exige également des questions en suspens en ce qui concerne quoi faire à propos des conflits de règles et comment elles sont appliquées. La résolution de ces questions est clair avec une chaîne de replace.

16voto

Mike Boers Points 3999

Que diriez-vous de tester différentes façons de le faire et de voir qui vient plus vite (en supposant que nous sommes seulement à se soucier de la façon la plus rapide de le faire).

def escape1(input):
        return input.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("'", '&#39;').replace('"', '&quot;')

translation_table = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    "'": '&#39;',
    '"': '&quot;',
}

def escape2(input):
        return ''.join(translation_table.get(char, char) for char in input)

import re
_escape3_re = re.compile(r'[&<>\'"]')
def _escape3_repl(x):
    s = x.group(0)
    return translation_table.get(s, s)
def escape3(x):
    return _escape3_re.sub(_escape3_repl, x)

def escape4(x):
    return unicode(x).translate(translation_table)

test_strings = (
    'Nothing in there.',
    '<this is="not" a="tag" />',
    'Something & Something else',
    'This one is pretty long. ' * 50
)

import time

for test_i, test_string in enumerate(test_strings):
    print repr(test_string)
    for func in escape1, escape2, escape3, escape4:
        start_time = time.time()
        for i in xrange(1000):
            x = func(test_string)
        print '\t%s done in %.3fms' % (func.__name__, (time.time() - start_time))
    print

L'exécution de cette offre:

'Nothing in there.'
    escape1 done in 0.002ms
    escape2 done in 0.009ms
    escape3 done in 0.001ms
    escape4 done in 0.005ms

'<this is="not" a="tag" />'
    escape1 done in 0.002ms
    escape2 done in 0.012ms
    escape3 done in 0.009ms
    escape4 done in 0.007ms

'Something & Something else'
    escape1 done in 0.002ms
    escape2 done in 0.012ms
    escape3 done in 0.003ms
    escape4 done in 0.007ms

'This one is pretty long. <snip>'
    escape1 done in 0.008ms
    escape2 done in 0.386ms
    escape3 done in 0.011ms
    escape4 done in 0.310ms

Ressemble juste de remplacer l'un après l'autre va le plus vite.

Edit: exécuter les tests de nouveau avec 1000000 itérations donne les informations suivantes pour les trois premières cordes (la quatrième prendrait trop de temps sur ma machine pour moi d'attendre =P):

'Nothing in there.'
    escape1 done in 0.001ms
    escape2 done in 0.008ms
    escape3 done in 0.002ms
    escape4 done in 0.005ms

'<this is="not" a="tag" />'
    escape1 done in 0.002ms
    escape2 done in 0.011ms
    escape3 done in 0.009ms
    escape4 done in 0.007ms

'Something & Something else'
    escape1 done in 0.002ms
    escape2 done in 0.011ms
    escape3 done in 0.003ms
    escape4 done in 0.007ms

Les chiffres sont à peu près la même. Dans le premier cas, elles sont même plus cohérent que le direct de la chaîne de remplacement est plus rapide maintenant.

13voto

mkotechno Points 1480

Je préfère quelque chose de propre comme:

 substitutions = [
    ('<', '&lt;'),
    ('>', '&gt;'),
    ...]

for search, replacement in substitutions:
    string = string.replace(search, replacement)
 

7voto

Ken Points 1743

C'est ce que fait Django :

 def escape(html):
    """Returns the given HTML with ampersands, quotes and carets encoded."""
    return mark_safe(force_unicode(html).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
 

5voto

Michael Points 1943

Conformément à la suggestion de bebraw, voici ce que j'ai fini par utiliser (dans un module séparé, bien sûr):

 import re

class Subs(object):
    """
    A container holding strings to be searched for and replaced in
    replace_multi().

    Holds little relation to the sandwich.
    """
    def __init__(self, needles_and_replacements):
        """
        Returns a new instance of the Subs class, given a dictionary holding 
        the keys to be searched for and the values to be used as replacements.
        """
        self.lookup = needles_and_replacements
        self.regex = re.compile('|'.join(map(re.escape,
                                             needles_and_replacements)))

def replace_multi(string, subs):
    """
    Replaces given items in string efficiently in a single-pass.

    "string" should be the string to be searched.
    "subs" can be either:
        A.) a dictionary containing as its keys the items to be
            searched for and as its values the items to be replaced.
        or B.) a pre-compiled instance of the Subs class from this module
               (which may have slightly better performance if this is
                called often).
    """
    if not isinstance(subs, Subs): # Assume dictionary if not our class.
        subs = Subs(subs)
    lookup = subs.lookup
    return subs.regex.sub(lambda match: lookup[match.group(0)], string)
 

Exemple d'utilisation:

 def escape(string):
    """
    Returns the given string with ampersands, quotes and angle 
    brackets encoded.
    """
    # Note that ampersands must be escaped first; the rest can be escaped in 
    # any order.
    escape.subs = Subs({'<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'})
    return replace_multi(string.replace('&', '&amp;'), escape.subs)
 

Beaucoup mieux :). Merci pour l'aide.

Éditer

Peu importe, Mike Graham avait raison. Je l'ai comparé et le remplacement finit par être beaucoup plus lent.

Code:

 from urllib2 import urlopen
import timeit

def escape1(string):
    """
    Returns the given string with ampersands, quotes and angle
    brackets encoded.
    """
    return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("'", '&#39;').replace('"', '&quot;')

def escape2(string):
    """
    Returns the given string with ampersands, quotes and angle
    brackets encoded.
    """
    # Note that ampersands must be escaped first; the rest can be escaped in
    # any order.
    escape2.subs = Subs({'<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'})
    return replace_multi(string.replace('&', '&amp;'), escape2.subs)

# An example test on the stackoverflow homepage.
request = urlopen('http://stackoverflow.com')
test_string = request.read()
request.close()

test1 = timeit.Timer('escape1(test_string)',
                     setup='from __main__ import escape1, test_string')
test2 = timeit.Timer('escape2(test_string)',
                     setup='from __main__ import escape2, test_string')
print 'multi-pass:', test1.timeit(2000)
print 'single-pass:', test2.timeit(2000)
 

Production:

 multi-pass: 15.9897229671
single-pass: 66.5422530174
 

Tellement pour ça.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X