Uma linha de código Python pode conhecer seu nível de aninhamento de indentação?

149

De algo assim:

print(get_indentation_level())

    print(get_indentation_level())

        print(get_indentation_level())

Eu gostaria de obter algo parecido com isto:

1
2
3

O código pode se ler dessa maneira?

Tudo o que eu quero é que a saída das partes mais aninhadas do código seja mais aninhada. Da mesma forma que isso facilita a leitura do código, a saída é mais fácil de ler.

É claro que eu poderia implementar isso manualmente, usando, por exemplo .format(), mas o que eu tinha em mente era uma função de impressão personalizada que seria print(i*' ' + string)onde iestá o nível de indentação. Essa seria uma maneira rápida de obter uma saída legível no meu terminal.

Existe uma maneira melhor de fazer isso que evite a formatação manual cuidadosa?

Fab von Bellingshausen
fonte
70
Estou realmente curioso para saber por que você precisa disso.
Harrison
12
@ Harrison Eu queria recuar a saída do meu código de acordo com a forma como ele foi recuado no código.
Fab von Bellingshausen
14
A verdadeira questão é: por que você precisaria disso? O nível de indentação é estático; você sabe com certeza quando coloca a get_indentation_level()declaração no seu código. Você também pode fazer o que print(3)quer que seja diretamente. O que pode ser mais interessante é o nível atual de aninhamento na pilha de chamadas de função.
Tobias_k 26/08/16
19
É com o objetivo de depurar seu código? Isso parece ser uma maneira super genial de registrar o fluxo de execução ou como uma solução superengenharia para um problema simples, e não tenho certeza qual é ... talvez os dois!
Blackhawk
7
@FabvonBellingshausen: Parece que seria muito menos legível do que você espera. Eu acho que você pode ser melhor atendido passando explicitamente um depthparâmetro e adicionando o valor apropriado conforme necessário quando você o passa para outras funções. É provável que o aninhamento do seu código não corresponda corretamente ao recuo que você deseja da saída.
User2357112 suporta Monica

Respostas:

115

Se você deseja recuo em termos de nível de aninhamento, em vez de espaços e guias, as coisas ficam complicadas. Por exemplo, no seguinte código:

if True:
    print(
get_nesting_level())

a chamada para get_nesting_levelestá realmente aninhada em um nível, apesar do fato de que não há espaços em branco à esquerda na linha da get_nesting_levelchamada. Enquanto isso, no seguinte código:

print(1,
      2,
      get_nesting_level())

a chamada para get_nesting_levelestá aninhada com zero nível de profundidade, apesar da presença de espaços em branco à esquerda em sua linha.

No código a seguir:

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

if True:
    print(get_nesting_level())

as duas chamadas get_nesting_levelestão em diferentes níveis de aninhamento, apesar do espaço em branco inicial ser idêntico.

No código a seguir:

if True: print(get_nesting_level())

esses níveis zero aninhados ou um? Em termos INDENTe DEDENTtokens na gramática formal, os níveis são zero, mas você pode não se sentir da mesma maneira.


Se você quiser fazer isso, terá que tokenizar o arquivo inteiro até o ponto da chamada e contar INDENTe DEDENTtokens. O tokenizemódulo seria muito útil para essa função:

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 suporta Monica
fonte
2
Isso não funciona da maneira que eu esperaria quando get_nesting_level()é chamado nessa chamada de função - ele retorna o nível de aninhamento nessa função. Poderia ser reescrito para retornar o nível de aninhamento 'global'?
Fab von Bellingshausen
11
@FabvonBellingshausen: Você pode estar obtendo um nível de aninhamento de recuo e um nível de aninhamento de chamada de função misturados. Esta função fornece o nível de aninhamento de recuo. O nível de aninhamento de chamada de função seria bem diferente e forneceria um nível 0 para todos os meus exemplos. Se você deseja uma espécie de híbrido no nível de indentação / aninhamento de chamadas, que seja incrementado tanto para chamadas de função quanto para estruturas de controle de fluxo como whilee with, isso seria possível, mas não é o que você pediu e alterando a pergunta para fazer algo diferente neste momento seria uma má ideia.
User2357112 suporta Monica
38
Aliás, eu concordo plenamente com todas as pessoas dizendo que isso é uma coisa realmente estranha de se fazer. Provavelmente, existe uma maneira muito melhor de resolver qualquer problema que você esteja tentando resolver, e confiar nisso provavelmente o impedirá, forçando-o a usar todos os tipos de hackers desagradáveis ​​para evitar alterar sua estrutura de recuo ou chamada de função quando você precisar alterações no seu código.
User2357112 suporta Monica
4
eu certamente não esperava que alguém tivesse realmente respondido a isso. (considerar o linecachemódulo para coisas como esta embora - ele é usado para imprimir tracebacks, e pode lidar com módulos importados de arquivos zip e outros truques estranho importação)
Eevee
6
@Eevee: Eu certamente não esperava que tantas pessoas votassem nisso! linecachepode ser bom para reduzir a quantidade de E / S de arquivo (e obrigado por me lembrar disso), mas se eu começar a otimizar isso, ficaria incomodado com a redundância de tokenização do mesmo arquivo para repetições do arquivo mesma chamada ou para vários sites de chamada no mesmo arquivo. Existem várias maneiras de otimizar isso também, mas não tenho certeza do quanto eu realmente quero ajustar e proteger esta coisa louca.
User2357112 suporta Monica
22

Sim, isso é definitivamente possível, aqui está um exemplo de trabalho:

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
fonte
4
Em relação ao comentário feito pelo @Prune, isso pode ser feito para retornar o recuo em níveis, em vez de espaços? Será sempre bom simplesmente dividir por 4?
Fab von Bellingshausen
2
Não, divida por 4 para obter o nível de recuo não funcionará com esse código. Pode verificar, aumentando o nível de recuo da última declaração de impressão, o último valor impresso é apenas aumentado.
Craig Burgler
10
Um bom começo, mas realmente não responde à pergunta imo. O número de espaços não é o mesmo que o nível de indentação.
wim
1
Não é tão simples assim. Substituir 4 espaços por espaços únicos pode alterar a lógica do código.
wim
1
Mas esse código é perfeito para o que o OP estava procurando: (Comentário # 9 do OP): 'Eu queria recuar a saída do meu código de acordo com a forma como ele foi recuado no código.' Assim, ele pode fazer algo assimprint('{Space}'*get_indentation_level(), x)
Craig Burgler
10

Você pode usar sys.current_frame.f_linenopara obter o número da linha. Então, para encontrar o número do nível de recuo, você precisa encontrar a linha anterior com recuo zero e, subtraindo o número da linha atual do número dessa linha, você obterá o número de recuo:

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

Demo:

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

Se você quiser o número do nível de indentação com base nas linhas anteriores, :basta fazê-lo com uma pequena alteração:

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(':'))

Demo:

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

E como resposta alternativa, aqui está uma função para obter o número de indentação (espaço em branco):

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
fonte
pergunta feita para o número de níveis de indentação, não o número de espaços. eles não são necessariamente proporcionais.
wim
Para o seu código de demonstração a saída deve ser 1 - 2 - 3 - 3
Craig Burgler
@CraigBurgler Para obter 1 - 2 - 3 - 3, podemos contar o número de linhas antes da linha atual que termina com a :até encontrarmos a linha com recuo zero, confira a edição!
Kasramvd
2
hmmm ... ok ... agora experimentar alguns dos @ casos de teste das user2357112;)
Craig Burgler
@ CraigBurgler Esta solução é apenas para a maioria dos casos. E, em relação a essa resposta, também é uma maneira geral, também não fornece uma solução abrangente. Tente{3:4, \n 2:get_ind_num()}
Kasramvd 26/08/16
7

Para resolver o problema "real" que leva à sua pergunta, você pode implementar um gerenciador de contexto que monitora o nível de withindentação e faz com que a estrutura do bloco no código corresponda aos níveis de indentação da saída. Dessa forma, o recuo do código ainda reflete o recuo da saída sem acoplar muito os dois. Ainda é possível refatorar o código em diferentes funções e ter outros recuos baseados na estrutura do código que não interferem no recuo da saída.

#!/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()

Resultado:

0
  1
    Hallo 2
      3
    and back one level 2
BlackJack
fonte
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
fonte
12
Isso fornece o recuo nos espaços, não nos níveis. A menos que o programador use quantidades de recuo consistentes, isso pode ser feio para converter em níveis.
Prune
4
A função não está documentada? Não consigo encontrá-lo aqui
GingerPlusPlus