¿Puede una línea de código Python conocer su nivel de anidación de sangría?

149

De algo como esto:

print(get_indentation_level())

    print(get_indentation_level())

        print(get_indentation_level())

Me gustaría obtener algo como esto:

1
2
3

¿Puede el código leerse de esta manera?

Todo lo que quiero es que la salida de las partes más anidadas del código esté más anidada. De la misma manera que esto hace que el código sea más fácil de leer, haría que la salida sea más fácil de leer.

Por supuesto, podría implementar esto manualmente, usando, por ejemplo .format(), pero lo que tenía en mente era una función de impresión personalizada que vería print(i*' ' + string)dónde iestá el nivel de sangría. Esta sería una forma rápida de hacer una salida legible en mi terminal.

¿Hay una mejor manera de hacer esto que evite el formato manual minucioso?

Fab von Bellingshausen
fuente
70
Tengo mucha curiosidad de por qué necesitas esto.
Harrison el
12
@Harrison Quería sangrar la salida de mi código de acuerdo a cómo se sangraba en el código.
Fab von Bellingshausen
14
La verdadera pregunta es: ¿por qué necesitarías esto? El nivel de sangría es estático; lo sabes con certeza cuando pones el get_indentation_level()enunciado en tu código. Puedes hacer print(3)lo mismo o lo que sea directamente. Lo que podría ser más interesante es el nivel actual de anidamiento en la pila de llamadas de función.
tobias_k
19
¿Es con el propósito de depurar su código? Esto parece ser una forma súper genial de registrar el flujo de ejecución o como una solución súper sobredimensionada para un problema simple, y no estoy seguro de cuál es ... ¡quizás ambas!
Blackhawk
77
@FabvonBellingshausen: Parece que sería mucho menos legible de lo que esperas. Creo que podría beneficiarse mejor al pasar explícitamente un depthparámetro y agregarle el valor apropiado según sea necesario cuando lo pase a otras funciones. No es probable que la anidación de su código corresponda limpiamente a la sangría que desea de su salida.
user2357112 es compatible con Monica el

Respuestas:

115

Si desea una sangría en términos de nivel de anidación en lugar de espacios y pestañas, las cosas se ponen difíciles. Por ejemplo, en el siguiente código:

if True:
    print(
get_nesting_level())

la llamada a get_nesting_levelestá anidada en un nivel de profundidad, a pesar del hecho de que no hay espacios en blanco iniciales en la línea de la get_nesting_levelllamada. Mientras tanto, en el siguiente código:

print(1,
      2,
      get_nesting_level())

la llamada a get_nesting_levelestá anidada a cero niveles de profundidad, a pesar de la presencia de espacios en blanco iniciales en su línea.

En el siguiente código:

if True:
  if True:
    print(get_nesting_level())

if True:
    print(get_nesting_level())

las dos llamadas a get_nesting_levelestán en diferentes niveles de anidación, a pesar de que el espacio en blanco inicial es idéntico.

En el siguiente código:

if True: print(get_nesting_level())

¿Es eso cero niveles anidados, o uno? En términos de INDENTy DEDENTtokens en la gramática formal, tiene cero niveles de profundidad, pero es posible que no sienta lo mismo.


Si desea hacer esto, tendrá que tokenizar todo el archivo hasta el punto de la llamada y contar INDENTy DEDENTtokens. El tokenizemódulo sería muy útil para tal función:

import inspect
import tokenize

def get_nesting_level():
    caller_frame = inspect.currentframe().f_back
    filename, caller_lineno, _, _, _ = inspect.getframeinfo(caller_frame)
    with open(filename) as f:
        indentation_level = 0
        for token_record in tokenize.generate_tokens(f.readline):
            token_type, _, (token_lineno, _), _, _ = token_record
            if token_lineno > caller_lineno:
                break
            elif token_type == tokenize.INDENT:
                indentation_level += 1
            elif token_type == tokenize.DEDENT:
                indentation_level -= 1
        return indentation_level
user2357112 es compatible con Monica
fuente
2
Esto no funciona de la manera que esperaría cuando get_nesting_level()se llama dentro de esa llamada de función: devuelve el nivel de anidación dentro de esa función. ¿Podría reescribirse para devolver el nivel de anidación 'global'?
Fab von Bellingshausen
11
@FabvonBellingshausen: es posible que el nivel de anidación de sangría y el nivel de anidación de llamadas de función se mezclen. Esta función proporciona un nivel de anidación de sangría. El nivel de anidamiento de llamadas de función sería bastante diferente y daría un nivel de 0 para todos mis ejemplos. Si desea una especie de sangría / nivel de anidamiento de llamadas híbrido que se incremente tanto para las llamadas de función como para las estructuras de flujo de control como whiley with, eso sería factible, pero no es lo que solicitó, y cambiar la pregunta para hacer algo diferente en este momento Sería una mala idea.
user2357112 es compatible con Monica el
38
Por cierto, estoy totalmente de acuerdo con todas las personas que dicen que esto es algo realmente extraño. Probablemente haya una forma mucho mejor de resolver cualquier problema que intente resolver, y confiar en esto probablemente lo estorbará al obligarlo a usar todo tipo de hacks desagradables para evitar cambiar su sangría o estructura de llamada de función cuando necesite realizar cambios a su código.
user2357112 es compatible con Monica el
44
Ciertamente no esperaba que alguien hubiera respondido esto realmente. (Considere el linecachemódulo para cosas como esta, sin embargo, se utiliza para imprimir trazas y puede manejar módulos importados de archivos zip y otros trucos de importación extraños)
Eevee
66
@Eevee: ¡Ciertamente no esperaba que tanta gente votara esto! linecachepodría ser bueno para reducir la cantidad de E / S de archivo (y gracias por recordármelo), pero si comenzara a optimizar eso, me molestaría cómo estamos volviendo a tokenizar el mismo archivo de forma redundante para repeticiones de misma llamada o para múltiples sitios de llamadas dentro del mismo archivo. Hay varias maneras en que podríamos optimizar eso también, pero no estoy seguro de cuánto realmente quiero sintonizar y a prueba de balas esta locura.
user2357112 es compatible con Monica el
22

Sí, eso es definitivamente posible, aquí hay un ejemplo de trabajo:

import inspect

def get_indentation_level():
    callerframerecord = inspect.stack()[1]
    frame = callerframerecord[0]
    info = inspect.getframeinfo(frame)
    cc = info.code_context[0]
    return len(cc) - len(cc.lstrip())

if 1:
    print get_indentation_level()
    if 1:
        print get_indentation_level()
        if 1:
            print get_indentation_level()
BPL
fuente
44
En relación con el comentario hecho por @Prune, ¿se puede hacer que devuelva la sangría en niveles en lugar de espacios? ¿Siempre estará bien simplemente dividir por 4?
Fab von Bellingshausen
2
No, dividir por 4 para obtener el nivel de sangría no funcionará con este código. Puede verificar aumentando el nivel de sangría de la última declaración de impresión, el último valor impreso acaba de aumentar.
Craig Burgler
10
Un buen comienzo, pero realmente no responde la pregunta de la OMI. El número de espacios no es el mismo que el nivel de sangría.
wim
1
No es tan simple. Reemplazar 4 espacios con espacios individuales puede cambiar la lógica del código.
wim
1
Pero este código es perfecto para lo que estaba buscando el OP: (Comentario OP # 9): 'Quería sangrar la salida de mi código de acuerdo con cómo estaba sangrado en el código'. Para que pueda hacer algo comoprint('{Space}'*get_indentation_level(), x)
Craig Burgler
10

Puede usar sys.current_frame.f_linenopara obtener el número de línea. Luego, para encontrar el número de nivel de sangría, necesita encontrar la línea anterior con cero sangría, luego restando el número de línea actual del número de esa línea obtendrá el número de sangría:

import sys
current_frame = sys._getframe(0)

def get_ind_num():
    with open(__file__) as f:
        lines = f.readlines()
    current_line_no = current_frame.f_lineno
    to_current = lines[:current_line_no]
    previous_zoro_ind = len(to_current) - next(i for i, line in enumerate(to_current[::-1]) if not line[0].isspace())
    return current_line_no - previous_zoro_ind

Manifestación:

if True:
    print get_ind_num()
    if True:
        print(get_ind_num())
        if True:
            print(get_ind_num())
            if True: print(get_ind_num())
# Output
1
3
5
6

Si desea el número del nivel de sangría basado en las líneas anteriores con :solo puede hacerlo con un pequeño cambio:

def get_ind_num():
    with open(__file__) as f:
        lines = f.readlines()

    current_line_no = current_frame.f_lineno
    to_current = lines[:current_line_no]
    previous_zoro_ind = len(to_current) - next(i for i, line in enumerate(to_current[::-1]) if not line[0].isspace())
    return sum(1 for line in lines[previous_zoro_ind-1:current_line_no] if line.strip().endswith(':'))

Manifestación:

if True:
    print get_ind_num()
    if True:
        print(get_ind_num())
        if True:
            print(get_ind_num())
            if True: print(get_ind_num())
# Output
1
2
3
3

Y como respuesta alternativa aquí hay una función para obtener el número de sangría (espacio en blanco):

import sys
from itertools import takewhile
current_frame = sys._getframe(0)

def get_ind_num():
    with open(__file__) as f:
        lines = f.readlines()
    return sum(1 for _ in takewhile(str.isspace, lines[current_frame.f_lineno - 1]))
Kasramvd
fuente
pregunta formulada para el número de niveles de sangría, no el número de espacios. No son necesariamente proporcionales.
wim
Para su código de demostración, la salida debe ser 1 - 2 - 3 - 3
Craig Burgler
@CraigBurgler Para obtener 1 - 2 - 3 - 3 podemos contar el número de líneas antes de la línea actual que termina con a :hasta que encontremos la línea con sangría cero, ¡Mira la edición!
Kasramvd
2
hmmm ... ok ... ahora prueba algunos de los casos de prueba de @ user2357112;)
Craig Burgler
@CraigBurgler Esta solución es solo para la mayoría de los casos, y con respecto a esa respuesta, también es una forma general, no ofrece una solución integral también. Prueba{3:4, \n 2:get_ind_num()}
Kasramvd
7

Para resolver el problema "real" que condujo a su pregunta, podría implementar un administrador de contexto que realice un seguimiento del nivel de sangría y haga que la withestructura de bloques en el código corresponda a los niveles de sangría de la salida. De esta forma, la sangría del código aún refleja la sangría de salida sin acoplar demasiado los dos. Todavía es posible refactorizar el código en diferentes funciones y tener otras sangrías basadas en la estructura del código que no interfiera con la sangría de salida.

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function


class IndentedPrinter(object):

    def __init__(self, level=0, indent_with='  '):
        self.level = level
        self.indent_with = indent_with

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, *_args):
        self.level -= 1

    def print(self, arg='', *args, **kwargs):
        print(self.indent_with * self.level + str(arg), *args, **kwargs)


def main():
    indented = IndentedPrinter()
    indented.print(indented.level)
    with indented:
        indented.print(indented.level)
        with indented:
            indented.print('Hallo', indented.level)
            with indented:
                indented.print(indented.level)
            indented.print('and back one level', indented.level)


if __name__ == '__main__':
    main()

Salida:

0
  1
    Hallo 2
      3
    and back one level 2
Veintiuna
fuente
6
>>> import inspect
>>> help(inspect.indentsize)
Help on function indentsize in module inspect:

indentsize(line)
    Return the indent size, in spaces, at the start of a line of text.
Craig Burgler
fuente
12
Esto da la sangría en espacios, no en niveles. A menos que el programador use cantidades de sangría consistentes, esto podría ser feo para convertir a niveles.
Pode el
44
¿La función no está documentada? No puedo encontrarlo aquí
GingerPlusPlus