Sangría adecuada para cadenas multilínea de Python


456

¿Cuál es la sangría adecuada para las cadenas multilínea de Python dentro de una función?

    def method():
        string = """line one
line two
line three"""

o

    def method():
        string = """line one
        line two
        line three"""

¿o algo mas?

Parece un poco extraño tener la cadena colgando fuera de la función en el primer ejemplo.


44
Las cadenas de documentos se tratan especialmente : se elimina cualquier sangría de la primera línea; la sangría común más pequeña tomada sobre todas las demás líneas no en blanco se elimina de todas ellas. Aparte de eso, los literales de cadena multilínea en Python desafortunadamente son lo que ves es lo que obtienes en términos de espacio en blanco: todos los caracteres entre los delimitadores de cadena se vuelven parte de la cadena, incluida la sangría que, con los instintos de lectura de Python, parece que debería medirse a partir de la sangría de la línea donde comienza el literal.
Evgeni Sergeev

@EvgeniSergeev La herramienta de procesamiento realiza esta tarea (y eso depende en gran medida de la elección de la herramienta de procesamiento). method.__doc__Python no lo modifica más que cualquier otro strliteral.
cz

Respuestas:


453

Probablemente quieras alinearte con el """

def foo():
    string = """line one
             line two
             line three"""

Dado que las líneas nuevas y los espacios se incluyen en la cadena en sí, tendrá que procesarla posteriormente. Si no desea hacer eso y tiene una gran cantidad de texto, es posible que desee almacenarlo por separado en un archivo de texto. Si un archivo de texto no funciona bien para su aplicación y no desea realizar un postproceso, probablemente iría con

def foo():
    string = ("this is an "
              "implicitly joined "
              "string")

Si desea postprocesar una cadena multilínea para recortar las partes que no necesita, debe considerar el textwrapmódulo o la técnica para las cadenas de documentos de posprocesamiento presentadas en PEP 257 :

def trim(docstring):
    if not docstring:
        return ''
    # Convert tabs to spaces (following the normal Python rules)
    # and split into a list of lines:
    lines = docstring.expandtabs().splitlines()
    # Determine minimum indentation (first line doesn't count):
    indent = sys.maxint
    for line in lines[1:]:
        stripped = line.lstrip()
        if stripped:
            indent = min(indent, len(line) - len(stripped))
    # Remove indentation (first line is special):
    trimmed = [lines[0].strip()]
    if indent < sys.maxint:
        for line in lines[1:]:
            trimmed.append(line[indent:].rstrip())
    # Strip off trailing and leading blank lines:
    while trimmed and not trimmed[-1]:
        trimmed.pop()
    while trimmed and not trimmed[0]:
        trimmed.pop(0)
    # Return a single string:
    return '\n'.join(trimmed)

10
Este es el estilo de 'sangría colgante' de continuación de línea. Se prescribe en PEP8 para fines como definiciones de funciones y sentencias if largas, aunque no se menciona para cadenas multilínea. Personalmente, este es un lugar donde me niego a seguir PEP8 (y uso sangría de 4 espacios en su lugar), ya que no me gusta mucho las sangrías colgantes, lo que para mí oscurece la estructura adecuada del programa.
bobince

2
@buffer, en 3.1.2 del tutorial oficial ("Dos literales de cadena uno al lado del otro se concatenan automáticamente ...") y en la referencia del lenguaje.
Mike Graham

55
La segunda forma con concatenación automática de cadenas no incluye nueva línea. Es una característica.
Mike Graham

19
La trim()función especificada en PEP257 se implementa en la biblioteca estándar como inspect.cleandoc.

2
1 a @bobince 's comentario sobre el rechazo 'sangrías' aquí ... Sobre todo porque si cambia el nombre de la variable a partir stringde texto cualquier cosa de una longitud diferente, entonces ahora tiene que actualizar la sangría de , literalmente, cada línea de la cadena multilínea solo para que coincida con la """correcta. La estrategia de sangría no debería complicar los futuros refactores / mantenimiento, y es uno de los lugares donde la PEP realmente falla
kevlarr es el

255

La textwrap.dedentfunción permite comenzar con una sangría correcta en la fuente y luego quitarla del texto antes de usarla.

La compensación, como han señalado algunos otros, es que esta es una llamada de función adicional en el literal; tenga esto en cuenta al decidir dónde colocar estos literales en su código.

import textwrap

def frobnicate(param):
    """ Frobnicate the scrognate param.

        The Weebly-Ruckford algorithm is employed to frobnicate
        the scrognate to within an inch of its life.

        """
    prepare_the_comfy_chair(param)
    log_message = textwrap.dedent("""\
            Prepare to frobnicate:
            Here it comes...
                Any moment now.
            And: Frobnicate!""")
    weebly(param, log_message)
    ruckford(param)

Lo que \sigue en el mensaje de registro literal es asegurar que el salto de línea no esté en el literal; de esa manera, el literal no comienza con una línea en blanco, sino que comienza con la siguiente línea completa.

El valor de retorno de textwrap.dedentes la cadena de entrada con todas las sangrías de espacios en blanco iniciales comunes eliminadas en cada línea de la cadena. Entonces el log_messagevalor anterior será:

Prepare to frobnicate:
Here it comes...
    Any moment now.
And: Frobnicate!

2
Si bien esta es una solución razonable y agradable de saber, hacer algo como esto dentro de una función llamada con frecuencia podría ser un desastre.
haridsv

@haridsv ¿Por qué sería un desastre?
jtmoulia

10
@jtmoulia: Una descripción mejor que el desastre sería "ineficiente" porque el resultado de la textwrap.dedent()llamada es un valor constante, al igual que su argumento de entrada.
Martineau

2
@haridsv el origen de ese desastre / ineficiencia es definir una cadena constante dentro de una función frecuentemente llamada. Posible cambiar la definición constante por llamada para una búsqueda por llamada. De esa forma, el preprocesamiento dentado solo se ejecutaría una vez . Una pregunta relevante puede ser stackoverflow.com/q/15495376/611007. Enumera ideas para evitar definir la constante por cada llamada. Aunque las alternativas parecen requerir una búsqueda. Aún así, se intentan varias formas de encontrar el lugar favorable para almacenarlo. Por ejemplo: def foo: return foo.xluego la siguiente línea foo.x = textwrap.dedent("bar").
n611x007

1
Supongo que sería ineficiente si la cadena está destinada para el registro que solo está habilitado en modo de depuración, y de lo contrario no se usa. Pero entonces, ¿por qué registrar un literal de cadena multilínea de todos modos? Por lo tanto, es difícil encontrar un ejemplo de la vida real donde lo anterior sería ineficiente (es decir, donde ralentiza considerablemente el programa), porque lo que sea que consuma estas cadenas será más lento.
Evgeni Sergeev

53

Use inspect.cleandocasí:

def method():
    string = inspect.cleandoc("""
        line one
        line two
        line three""")

La sangría relativa se mantendrá como se esperaba. Como se comenta a continuación, si desea mantener las líneas vacías anteriores, use textwrap.dedent. Sin embargo, eso también mantiene el salto de primera línea.

Nota: Es una buena práctica sangrar bloques lógicos de código en su contexto relacionado para aclarar la estructura. Por ejemplo, la cadena de varias líneas que pertenece a la variable string.


55
¿Tan confundido por qué esta respuesta no existía hasta ahora, inspect.cleandocexiste desde Python 2.6 , que fue 2008 ...? Absolutamente la respuesta más limpia, especialmente porque no usa el estilo de sangría colgante, que solo desperdicia una cantidad innecesaria de espacio
kevlarr

1
Esta solución elimina las primeras líneas de texto en blanco (si las hay). Si no desea ese comportamiento, use textwrap.dedent docs.python.org/2/library/textwrap.html#textwrap.dedent
joshuakcockrell

1
¡Esto es perfecto!
zzzz zzzz

23

Una opción que parece faltar en las otras respuestas (solo mencionada en un comentario de naxa) es la siguiente:

def foo():
    string = ("line one\n"          # Add \n in the string
              "line two"  "\n"      # Add "\n" after the string
              "line three\n")

Esto permitirá una alineación adecuada, unirá las líneas implícitamente y aún mantendrá el desplazamiento de línea, lo cual, para mí, es una de las razones por las que me gustaría usar cadenas multilínea de todos modos.

No requiere ningún procesamiento posterior, pero debe agregar manualmente \nen cualquier lugar donde desea que finalice la línea. Ya sea en línea o como una cadena separada después. Este último es más fácil de copiar y pegar.


Tenga en cuenta que este es un ejemplo de una cadena unida implícitamente, no una cadena multilínea.
trk

@trk, es multilínea en el sentido de que la cadena contiene líneas nuevas (también conocidas como líneas múltiples), pero sí, se usa la unión para eludir los problemas de formato que tenía el OP.
holroy

17

Algunas opciones mas. En Ipython con pylab habilitado, dedent ya está en el espacio de nombres. Lo comprobé y es de matplotlib. O se puede importar con:

from matplotlib.cbook import dedent

En la documentación dice que es más rápido que el equivalente de textwrap y en mis pruebas en ipython es de hecho 3 veces más rápido en promedio con mis pruebas rápidas. También tiene la ventaja de que descarta las líneas en blanco iniciales, esto le permite ser flexible en la forma de construir la cadena:

"""
line 1 of string
line 2 of string
"""

"""\
line 1 of string
line 2 of string
"""

"""line 1 of string
line 2 of string
"""

Usar el dedent matplotlib en estos tres ejemplos dará el mismo resultado sensible. La función de sangría textwrap tendrá una línea en blanco con el primer ejemplo.

La desventaja obvia es que textwrap está en la biblioteca estándar mientras que matplotlib es un módulo externo.

Algunas compensaciones aquí ... las funciones dedent hacen que su código sea más legible donde se definen las cadenas, pero requieren un procesamiento posterior para obtener la cadena en formato utilizable. En las cadenas de documentos es obvio que debe usar la sangría correcta ya que la mayoría de los usos de la cadena de documentos harán el procesamiento requerido.

Cuando necesito una cadena no larga en mi código, encuentro el siguiente código ciertamente feo donde dejo que la cadena larga salga de la sangría adjunta. Definitivamente falla en "Hermoso es mejor que feo", pero se podría argumentar que es más simple y más explícito que la alternativa dedent.

def example():
    long_string = '''\
Lorem ipsum dolor sit amet, consectetur adipisicing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip.\
'''
    return long_string

print example()

6

Si desea una solución rápida y fácil y se ahorra escribir nuevas líneas, puede optar por una lista, por ejemplo:

def func(*args, **kwargs):
    string = '\n'.join([
        'first line of very long string and',
        'second line of the same long thing and',
        'third line of ...',
        'and so on...',
        ])
    print(string)
    return

Si bien este no es el mejor enfoque, lo he usado de vez en cuando. Si lo usa, debe usar una tupla en lugar de una lista, ya que no se va a modificar antes de unirse.
Lyndsy Simon

4

yo prefiero

    def method():
        string = \
"""\
line one
line two
line three\
"""

o

    def method():
        string = """\
line one
line two
line three\
"""

1
Esto no responde a la pregunta, porque la pregunta establece explícitamente que la sangría (dentro de la función) es importante.
bignose

@bignose La pregunta decía "Parece un poco extraño", no se permite su uso.
lk_vc

¿Cómo podría lograr esto sin la sangría fea?
lfender6445

@ lfender6445 bueno, tal vez puedas colocar todas estas cadenas en un archivo separado de otros códigos ...
lk_vc

3

Mis dos centavos, escapar del final de la línea para obtener las sangrías:

def foo():
    return "{}\n"\
           "freq: {}\n"\
           "temp: {}\n".format( time, freq, temp )

1

Vine aquí buscando un simple 1-liner para eliminar / corregir el nivel de ideación de la cadena de documentos para imprimir, sin hacer que se vea desordenado , por ejemplo, haciéndolo "colgar fuera de la función" dentro del script.

Esto es lo que terminé haciendo:

import string
def myfunction():

    """
    line 1 of docstring
    line 2 of docstring
    line 3 of docstring"""

print str(string.replace(myfunction.__doc__,'\n\t','\n'))[1:] 

Obviamente, si está sangrando con espacios (por ejemplo, 4) en lugar de la tecla de tabulación, use algo como esto en su lugar:

print str(string.replace(myfunction.__doc__,'\n    ','\n'))[1:]

Y no es necesario que elimine el primer carácter si desea que sus cadenas de documentos se vean así:

    """line 1 of docstring
    line 2 of docstring
    line 3 of docstring"""

print string.replace(myfunction.__doc__,'\n\t','\n') 

Esto falla en los métodos de clase y las clases anidadas.
tacaswell

1

La primera opción es la buena, con sangría incluida. Está en estilo python: proporciona legibilidad para el código.

Para mostrarlo correctamente:

print string.lstrip()

Esta parece ser la forma más simple y limpia de formatear cadenas de comillas triples para que no tenga espacios adicionales debido a la sangría
Taylor Liss

44
Esto solo eliminará los espacios iniciales en la primera línea de una cadena multilínea. No ayuda con el formato de las siguientes líneas.
M. Schlenker

0

Depende de cómo desea que se muestre el texto. Si desea que todo esté alineado a la izquierda, formatee como en el primer fragmento o itere a través de las líneas que recortan a la izquierda todo el espacio.


55
La forma en que funcionan las herramientas de procesamiento de docstring es eliminar no todo el espacio de la izquierda, sino tanto como la primera línea con sangría. Esta estrategia es un poco más sofisticada y le permite sangrar y respetarla en la cadena postprocesada.
Mike Graham el

0

Para cadenas, puede justo después de procesar la cadena. Para las cadenas de documentos, debe procesar después la función en su lugar. Aquí hay una solución para ambos que todavía es legible.

class Lstrip(object):
    def __rsub__(self, other):
        import re
        return re.sub('^\n', '', re.sub('\n$', '', re.sub('\n\s+', '\n', other)))

msg = '''
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
      velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
      cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
      est laborum.
      ''' - Lstrip()

print msg

def lstrip_docstring(func):
    func.__doc__ = func.__doc__ - Lstrip()
    return func

@lstrip_docstring
def foo():
    '''
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
    veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
    commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
    velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
    cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
    est laborum.
    '''
    pass


print foo.__doc__

1
El procesamiento de cadenas de documentos ya debe procesar una sangría consistente, como se describe en PEP 257 . Ya hay herramientas, por ejemplo inspect.cleandoc, que hacen esto de la manera correcta.
bignose

0

Tengo un problema similar, el código se volvió realmente ilegible usando multilíneas, salí con algo como

print("""aaaa
"""   """bbb
""")

sí, al principio podría parecer terrible, pero la sintaxis incorporada era bastante compleja y agregar algo al final (como '\ n "') no era una solución


0

Puede usar esta función trim_indent .

import re


def trim_indent(s: str):
    s = re.sub(r'^\n+', '', s)
    s = re.sub(r'\n+$', '', s)
    spaces = re.findall(r'^ +', s, flags=re.MULTILINE)
    if len(spaces) > 0 and len(re.findall(r'^[^\s]', s, flags=re.MULTILINE)) == 0:
        s = re.sub(r'^%s' % (min(spaces)), '', s, flags=re.MULTILINE)
    return s


print(trim_indent("""


        line one
            line two
                line three
            line two
        line one


"""))

Resultado:

"""
line one
    line two
        line three
    line two
line one
"""
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.