Une ligne de code Python peut-elle connaître son niveau d'imbrication d'indentation?

149

De quelque chose comme ça:

print(get_indentation_level())

    print(get_indentation_level())

        print(get_indentation_level())

J'aimerais obtenir quelque chose comme ceci:

1
2
3

Le code peut-il se lire de cette manière?

Tout ce que je veux, c'est que la sortie des parties les plus imbriquées du code soit plus imbriquée. De la même manière que cela rend le code plus facile à lire, cela rendrait la sortie plus facile à lire.

Bien sûr, je pourrais l'implémenter manuellement, en utilisant par exemple .format(), mais ce que j'avais à l'esprit était une fonction d'impression personnalisée qui serait print(i*' ' + string)où se itrouve le niveau d'indentation. Ce serait un moyen rapide de rendre une sortie lisible sur mon terminal.

Existe-t-il une meilleure façon de faire cela qui évite un formatage manuel fastidieux?

Fab von Bellingshausen
la source
71
Je suis vraiment curieux de savoir pourquoi vous en avez besoin.
Harrison
13
@Harrison Je voulais indenter la sortie de mon code en fonction de la façon dont il était mis en retrait dans le code.
Fab von Bellingshausen
14
La vraie question est: pourquoi en auriez-vous besoin? Le niveau d'indentation est statique; vous le savez avec certitude lorsque vous insérez la get_indentation_level()déclaration dans votre code. Vous pouvez tout aussi bien faire print(3)ou n'importe quoi directement. Ce qui pourrait être plus intéressant, c'est le niveau actuel d'imbrication sur la pile d'appels de fonction.
tobias_k
19
Est-ce dans le but de déboguer votre code? Cela semble soit une manière super géniale de consigner le flux d'exécution, soit une solution super sur-conçue pour un problème simple, et je ne suis pas sûr de savoir de quoi il s'agit ... peut-être les deux!
Blackhawk
7
@FabvonBellingshausen: Cela semble être beaucoup moins lisible que vous ne l'espérez. Je pense que vous pourriez être mieux servi en passant explicitement un depthparamètre et en lui ajoutant la valeur appropriée si nécessaire lorsque vous le transmettez à d'autres fonctions. L'imbrication de votre code n'est pas susceptible de correspondre proprement à l'indentation que vous souhaitez sortir de votre sortie.
user2357112 prend en charge Monica

Réponses:

115

Si vous voulez une indentation en termes de niveau d'imbrication plutôt que d'espaces et de tabulations, les choses se compliquent. Par exemple, dans le code suivant:

if True:
    print(
get_nesting_level())

l'appel à get_nesting_levelest en fait imbriqué d'un niveau de profondeur, malgré le fait qu'il n'y ait pas d'espace blanc au début sur la ligne de l' get_nesting_levelappel. En attendant, dans le code suivant:

print(1,
      2,
      get_nesting_level())

l'appel à get_nesting_levelest imbriqué à zéro niveaux de profondeur, malgré la présence d'espaces blancs de premier plan sur sa ligne.

Dans le code suivant:

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

if True:
    print(get_nesting_level())

les deux appels à get_nesting_levelsont à des niveaux d'imbrication différents, malgré le fait que le premier espace blanc soit identique.

Dans le code suivant:

if True: print(get_nesting_level())

est-ce que ce sont des niveaux zéro imbriqués ou un? En termes de INDENTet de DEDENTjetons dans la grammaire formelle, il n'y a aucun niveau, mais vous pourriez ne pas ressentir la même chose.


Si vous voulez faire cela, vous allez devoir tokeniser l'ensemble du fichier jusqu'au point de l'appel et compter INDENTet DEDENTtokens. Le tokenizemodule serait très utile pour une telle fonction:

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 prend en charge Monica
la source
2
Cela ne fonctionne pas comme je l'attendrais quand il get_nesting_level()est appelé dans cet appel de fonction - il retourne le niveau d'imbrication dans cette fonction. Pourrait-il être réécrit pour revenir au niveau d'imbrication «global»?
Fab von Bellingshausen
11
@FabvonBellingshausen: Vous avez peut-être confondu le niveau d'imbrication de l'indentation et le niveau d'imbrication des appels de fonction. Cette fonction donne le niveau d'imbrication de l'indentation. Le niveau d'imbrication des appels de fonction serait assez différent et donnerait un niveau de 0 pour tous mes exemples. Si vous voulez une sorte d'hybride de niveau d'indentation / d'imbrication d'appels qui incrémente à la fois pour les appels de fonction et les structures de flux de contrôle comme whileet with, ce serait faisable, mais ce n'est pas ce que vous avez demandé, et changer la question pour demander quelque chose de différent à ce stade serait une mauvaise idée.
user2357112 prend en charge Monica
38
Soit dit en passant, je suis entièrement d'accord avec tous les gens qui disent que c'est une chose vraiment étrange à faire. Il existe probablement un bien meilleur moyen de résoudre le problème que vous essayez de résoudre, et en vous appuyant sur cela, vous risquez de vous gêner en vous obligeant à utiliser toutes sortes de hacks désagréables pour éviter de modifier votre indentation ou la structure de vos appels de fonction lorsque vous devez faire modifications de votre code.
user2357112 soutient Monica
4
Je ne m'attendais certainement pas à ce que quelqu'un réponde à cela. (considérez le linecachemodule pour des trucs comme celui-ci cependant - il est utilisé pour imprimer des traces, et peut gérer les modules importés à partir de fichiers zip et d'autres astuces d'importation étranges)
Eevee
6
@Eevee: Je ne m'attendais certainement pas à ce que tant de gens votent pour ça! linecachepeut être bon pour réduire la quantité d'E / S de fichier (et merci de me le rappeler), mais si je commençais à optimiser cela, je serais dérangé par la façon dont nous re-tokenisons de manière redondante le même fichier pour les répétitions du même appel, ou pour plusieurs sites d'appel dans le même fichier. Il y a un certain nombre de façons dont nous pourrions optimiser cela aussi, mais je ne sais pas à quel point je veux vraiment régler et protéger ce truc fou.
user2357112 prend en charge Monica
22

Ouais, c'est certainement possible, voici un exemple de travail:

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
la source
4
En ce qui concerne le commentaire de @Prune, peut-il être fait pour renvoyer l'indentation en niveaux au lieu d'espaces? Sera-t-il toujours acceptable de simplement diviser par 4?
Fab von Bellingshausen
2
Non, diviser par 4 pour obtenir le niveau d'indentation ne fonctionnera pas avec ce code. Peut vérifier en augmentant le niveau de retrait de la dernière instruction d'impression, la dernière valeur imprimée est simplement augmentée.
Craig Burgler
10
Un bon début, mais ne répond pas vraiment à la question imo. Le nombre d'espaces n'est pas le même que le niveau d'indentation.
wim
1
Ce n'est pas aussi simple. Le remplacement de 4 espaces par des espaces simples peut changer la logique du code.
wim
1
Mais ce code est parfait pour ce que l'OP recherchait: (Commentaire OP # 9): «Je voulais indenter la sortie de mon code en fonction de la façon dont il était mis en retrait dans le code. Donc, il peut faire quelque chose commeprint('{Space}'*get_indentation_level(), x)
Craig Burgler
10

Vous pouvez utiliser sys.current_frame.f_linenopour obtenir le numéro de ligne. Ensuite, pour trouver le nombre de niveau d'indentation, vous devez trouver la ligne précédente avec une indentation nulle, puis soustraire le numéro de ligne actuel du numéro de cette ligne, vous obtiendrez le numéro d'indentation:

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

Démo:

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 vous voulez le numéro du niveau d'indentation basé sur les lignes précédentes, :vous pouvez le faire avec un peu de changement:

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

Démo:

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

Et comme réponse alternative, voici une fonction pour obtenir le nombre d'indentation (espace blanc):

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
la source
question posée pour le nombre de niveaux d'indentation, pas le nombre d'espaces. ils ne sont pas nécessairement proportionnels.
wim
Pour votre code de démonstration, la sortie doit être 1 - 2 - 3 - 3
Craig Burgler
@CraigBurgler Pour obtenir 1 - 2 - 3 - 3, nous pouvons compter le nombre de lignes avant la ligne actuelle qui se terminent par un :jusqu'à ce que nous rencontrions la ligne sans indentation, vérifiez l'édition!
Kasramvd
2
hmmm ... ok ... maintenant, essayez quelques-uns des cas de test de @ user2357112;)
Craig Burgler
@CraigBurgler Cette solution est juste pour la plupart des cas, et concernant cette réponse, c'est aussi une manière générale, elle ne donne pas non plus une solution complète. Try{3:4, \n 2:get_ind_num()}
Kasramvd
7

Pour résoudre le «vrai» problème qui mène à votre question, vous pouvez implémenter un gestionnaire de contexte qui garde la trace du niveau d'indentation et fait correspondre la withstructure de bloc dans le code aux niveaux d'indentation de la sortie. De cette façon, l'indentation du code reflète toujours l'indentation de sortie sans trop coupler les deux. Il est toujours possible de refactoriser le code en différentes fonctions et d'avoir d'autres indentations basées sur une structure de code qui ne joue pas avec l'indentation de sortie.

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

Production:

0
  1
    Hallo 2
      3
    and back one level 2
BlackJack
la source
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
la source
12
Cela donne l'indentation dans les espaces, pas dans les niveaux. À moins que le programmeur n'utilise des quantités d'indentation cohérentes, cela peut être moche à convertir en niveaux.
Prune
4
La fonction est-elle non documentée? Je ne peux pas le trouver ici
GingerPlusPlus