python concatenate string
Iterar sobre las lĂneas de una cuerda (5)
Tengo una cadena de varias líneas definida así:
foo = """
this is
a multi-line string.
"""
Esta cadena usamos como entrada de prueba para un analizador que estoy escribiendo. La función del analizador recibe un file
-objeto como entrada y lo itera. También llama al método next()
directamente para omitir líneas, así que realmente necesito un iterador como entrada, no como iterable. Necesito un iterador que itere sobre las líneas individuales de esa cadena como un file
-objeto sobre las líneas de un archivo de texto. Por supuesto, podría hacerlo así:
lineiterator = iter(foo.splitlines())
¿Hay una manera más directa de hacer esto? En este escenario, la cadena debe atravesar una vez para la división, y luego otra vez por el analizador. No importa en mi caso de prueba, ya que la cuerda es muy corta, solo estoy preguntando por curiosidad. Python tiene tantos complementos útiles y eficientes para esas cosas, pero no pude encontrar nada que satisfaga esta necesidad.
Aquí hay tres posibilidades:
foo = """
this is
a multi-line string.
"""
def f1(foo=foo): return iter(foo.splitlines())
def f2(foo=foo):
retval = ''''
for char in foo:
retval += char if not char == ''/n'' else ''''
if char == ''/n'':
yield retval
retval = ''''
if retval:
yield retval
def f3(foo=foo):
prevnl = -1
while True:
nextnl = foo.find(''/n'', prevnl + 1)
if nextnl < 0: break
yield foo[prevnl + 1:nextnl]
prevnl = nextnl
if __name__ == ''__main__'':
for f in f1, f2, f3:
print list(f())
Ejecutar esto como la secuencia de comandos principal confirma que las tres funciones son equivalentes. Con timeit
(y a * 100
para foo
para obtener cadenas sustanciales para una medición más precisa):
$ python -mtimeit -s''import asp'' ''list(asp.f3())''
1000 loops, best of 3: 370 usec per loop
$ python -mtimeit -s''import asp'' ''list(asp.f2())''
1000 loops, best of 3: 1.36 msec per loop
$ python -mtimeit -s''import asp'' ''list(asp.f1())''
10000 loops, best of 3: 61.5 usec per loop
Tenga en cuenta que necesitamos la llamada a la list()
para garantizar que los iteradores se recorren, no solo se crean.
IOW, la implementación ingenua es mucho más rápida, ni siquiera divertida: 6 veces más rápida que mi intento de find
llamadas, que a su vez es 4 veces más rápido que un enfoque de nivel inferior.
Lecciones a retener: la medición siempre es algo bueno (pero debe ser preciso); los métodos de cadena como las splitlines
se implementan de manera muy rápida; poner cadenas juntas programando a un nivel muy bajo (especialmente por bucles de +=
de piezas muy pequeñas) puede ser bastante lento.
Editar : se agregó la propuesta de @ Jacob, ligeramente modificada para dar los mismos resultados que los demás (se guardan los espacios en blanco al final de una línea), es decir:
from cStringIO import StringIO
def f4(foo=foo):
stri = StringIO(foo)
while True:
nl = stri.readline()
if nl != '''':
yield nl.strip(''/n'')
else:
raise StopIteration
La medición da:
$ python -mtimeit -s''import asp'' ''list(asp.f4())''
1000 loops, best of 3: 406 usec per loop
no tan bueno como el .find
basado en .find
, aún así, vale la pena tenerlo en cuenta, ya que podría ser menos propenso a pequeños errores uno por uno (cualquier bucle donde veas ocurrencias de +1 y -1, como mi f3
anterior , debería desencadenar automáticamente las sospechas "por orden", y también deberían hacerlo muchos bucles que carecen de dichos ajustes y deberían tenerlos, aunque creo que mi código también es correcto ya que pude verificar su salida con otras funciones '').
Pero el enfoque basado en división todavía gobierna.
Un lado: posiblemente un mejor estilo para f4
sería:
from cStringIO import StringIO
def f4(foo=foo):
stri = StringIO(foo)
while True:
nl = stri.readline()
if nl == '''': break
yield nl.strip(''/n'')
al menos, es un poco menos detallado. La necesidad de pelar /n
return iter(stri)
desafortunadamente prohíbe el reemplazo más claro y más rápido del ciclo while con return iter(stri)
(la parte iter
es redundante en las versiones modernas de Python, creo que desde 2.3 o 2.4, pero también es inocua) . Tal vez vale la pena intentarlo, también:
return itertools.imap(lambda s: s.strip(''/n''), stri)
o variaciones de los mismos, pero me estoy deteniendo aquí ya que es más o menos un ejercicio teórico basado en la strip
, el más simple y el más rápido.
La búsqueda basada en Regex es a veces más rápida que el enfoque del generador:
RRR = re.compile(r''(.*)/n'')
def f4(arg):
return (i.group(1) for i in RRR.finditer(arg))
No estoy seguro de a qué te refieres con "luego otra vez por el analizador sintáctico". Después de que se haya dividido, no hay más recorrido de la cadena , solo un recorrido de la lista de cadenas divididas. Probablemente esta sea la forma más rápida de lograr esto, siempre que el tamaño de la cuerda no sea absolutamente grande. El hecho de que python use cadenas inmutables significa que siempre debe crear una nueva cadena, por lo que esto debe hacerse en algún momento.
Si su cadena es muy grande, la desventaja está en el uso de la memoria: tendrá la cadena original y una lista de cadenas divididas en la memoria al mismo tiempo, duplicando la memoria requerida. Un enfoque de iterador puede ahorrarle esto, creando una cadena según sea necesario, aunque todavía paga la penalización de "división". Sin embargo, si su cadena es tan grande, generalmente quiere evitar incluso que la cadena no dividida esté en la memoria. Sería mejor simplemente leer la cadena desde un archivo, que ya le permite iterar a través de ella como líneas.
Sin embargo, si ya tiene una cadena enorme en la memoria, un enfoque sería usar StringIO, que presenta una interfaz similar a un archivo para una cadena, lo que incluye permitir iterar por línea (internamente usando .find para encontrar la próxima línea nueva). Luego obtienes:
import StringIO
s = StringIO.StringIO(myString)
for line in s:
do_something_with(line)
Si leo Modules/cStringIO.c
correctamente, esto debería ser bastante eficiente (aunque algo prolijo):
from cStringIO import StringIO
def iterbuf(buf):
stri = StringIO(buf)
while True:
nl = stri.readline()
if nl != '''':
yield nl.strip()
else:
raise StopIteration
Supongo que podrías enrollar el tuyo:
def parse(string):
retval = ''''
for char in string:
retval += char if not char == ''/n'' else ''''
if char == ''/n'':
yield retval
retval = ''''
if retval:
yield retval
No estoy seguro de cuán eficiente es esta implementación, pero eso solo se repetirá una vez sobre su cadena.
Mmm, generadores.
Editar:
Por supuesto, también querrá agregar cualquier tipo de acciones de análisis que quiera realizar, pero eso es bastante simple.