Может ли строка кода Python знать уровень вложенности вложения?

От чего-то вроде этого:

print(get_indentation_level())

    print(get_indentation_level())

        print(get_indentation_level())

Я хотел бы получить что-то вроде этого:

1
2
3

Может ли код считывать себя таким образом?

Все, что я хочу, это вывод из более вложенных частей кода, чтобы он был более вложенным. Точно так же, как это упрощает чтение кода, это облегчит чтение.

Конечно, я мог бы реализовать это вручную, используя, например, .format(), но я имел в виду специальную функцию печати, которая print(i*' ' + string), где i является уровнем отступа. Это будет быстрый способ сделать читаемый вывод на моем терминале.

Есть ли лучший способ сделать это, чтобы избежать кропотливого ручного форматирования?

Ответ 1

Если вы хотите отступы в плане уровня вложенности, а не пробелов и вкладок, все становится сложным. Например, в следующем коде:

if True:
    print(
get_nesting_level())

вызов get_nesting_level на самом деле вложен на один уровень, несмотря на то, что в строке вызова get_nesting_level нет ведущего пробела. Между тем, в следующем коде:

print(1,
      2,
      get_nesting_level())

вызов get_nesting_level имеет глубину вложенных нулей, несмотря на наличие на ее строке пробелов.

В следующем коде:

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

if True:
    print(get_nesting_level())

два вызова get_nesting_level находятся на разных уровнях вложенности, несмотря на то, что ведущие пробелы идентичны.

В следующем коде:

if True: print(get_nesting_level())

- это вложенные нулевые уровни или один? В терминах INDENT и DEDENT в формальной грамматике он имеет нулевые уровни в глубину, но вы можете не чувствовать то же самое.


Если вы хотите это сделать, вам нужно будет сделать токенизацию всего файла до точки вызова и подсчета INDENT и DEDENT tokens. Модуль tokenize был бы очень полезен для такой функции:

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

Ответ 2

Да, это определенно возможно, вот рабочий пример:

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

Ответ 3

Вы можете использовать sys.current_frame.f_lineno, чтобы получить номер строки. Затем, чтобы найти количество уровней отступов, вам нужно найти предыдущую строку с нулевым отступом, а затем вычесть текущий номер строки из этого номера строки, вы получите количество отступов:

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

Демо:

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

Если вы хотите, чтобы число уровней отступа было основано на линиях previouse с помощью :, вы можете просто сделать это с небольшим изменением:

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

Демо:

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

И как альтернативный ответ здесь - это функция для получения количества отступов (пробелов):

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]))

Ответ 4

>>> 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.

Ответ 5

Чтобы решить "настоящую" проблему, которая приведет к вашему вопросу, вы можете реализовать контекст-менеджер, который отслеживает уровень отступов и делает блок-структуру with в коде соответствующим уровням отступов вывода. Таким образом, отступ кода по-прежнему отражает выходной отступ, не связывая слишком много. По-прежнему можно реорганизовать код в разные функции и иметь другие отступы, основанные на структуре кода, которые не вступают в вывод с выделением вывода.

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

Вывод:

0
  1
    Hallo 2
      3
    and back one level 2