Чи може рядок коду Python знати рівень вкладеності відступів?


149

З чогось такого:

print(get_indentation_level())

    print(get_indentation_level())

        print(get_indentation_level())

Я хотів би отримати щось подібне:

1
2
3

Чи може код читати себе таким чином?

Все, що я хочу, - це висновок з більш вкладених частин коду, які будуть більш вкладеними. Таким же чином, як це полегшує читання коду, це зробить висновок простішим для читання.

Звичайно, я міг реалізувати це вручну, використовуючи, наприклад .format(), але це, що я мав на увазі, - це спеціальна функція друку, яка б print(i*' ' + string)де iбула рівнем відступу. Це був би швидкий спосіб зробити читабельний вихід на моєму терміналі.

Чи є кращий спосіб зробити це, щоб уникнути кропіткого ручного форматування?


70
Мені дуже цікаво, для чого вам це потрібно.
Гаррісон

12
@Harrison Я хотів відступити вихід мого коду відповідно до того, як він був з відступом у коді.
Фаб фон Беллінсгаузен

14
Справжнє питання: навіщо вам це потрібно? Рівень відступу статичний; ви знаєте це з упевненістю, коли вкладаєте get_indentation_level()статтю у свій код. Ви можете так само добре робити print(3)або що завгодно безпосередньо. Що може бути більш цікавим - це поточний рівень розміщення на стеку викликів функцій.
tobias_k

19
Це з метою налагодження вашого коду? Це здається або надто геніальним способом реєстрації потоку виконання, або як надмірно розроблене рішення для простої проблеми, і я не впевнений, що це таке ... можливо, і те й інше!
Blackhawk

7
@FabvonBellingshausen: Це здається, що це буде набагато менш читабельним, ніж ви сподіваєтесь. Я думаю, що вам може бути краще, якщо явно depthпройдете параметр і додаєте до нього відповідне значення за необхідності, коли ви передасте його іншим функціям. Вкладення вашого коду, швидше за все, не буде відповідати чистому відступі, який ви хочете вивести.
user2357112 підтримує Моніку

Відповіді:


115

Якщо ви хочете відступити в плані розміру, а не пробілах та вкладках, все стає складніше. Наприклад, у наступному коді:

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жетони. 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
Це не працює так, як я б очікував, коли get_nesting_level()викликається в рамках виклику функції ―, він повертає рівень введення в межах цієї функції. Чи можна було б переписати, щоб повернути "глобальний" рівень гніздування?
Фаб фон Беллінсгаузен

11
@FabvonBellingshausen: Можливо, ви отримаєте рівень вкладеності відступів і рівень гніздування викликів функцій. Ця функція забезпечує рівень вкладеності відступів. Рівень вкладеності виклику функцій був би досить різним і давав би рівень 0 для всіх моїх прикладів. Якщо ви хочете свого роду гібрид рівня відступів / викликів, який збільшується як для викликів функцій, так і для управління потоковими структурами на кшталт whileі with, це можна зробити, але це не те, що ви просили, і змінивши питання, щоб задати щось інше в цей момент було б поганою ідеєю.
user2357112 підтримує Моніку

38
Між іншим, я повністю згоден з усіма людьми, які кажуть, що це дійсно дивна річ. Напевно, є набагато кращий спосіб вирішити будь-яку проблему, яку ви намагаєтеся вирішити, і покладатися на це, швидше за все, буде заважати вам, змушуючи вас використовувати всілякі неприємні хаки, щоб уникнути зміни вашого відступу або структури виклику функції, коли вам потрібно зробити зміни вашого коду.
user2357112 підтримує Моніку

4
я точно не очікував, що хтось насправді відповів на це. (врахуйте linecacheмодуль для подібних речей - він використовується для друку зворотних записів і може обробляти модулі, імпортовані з zip-файлів та інші дивні хитрощі імпорту)
Eevee

6
@Eevee: Я, звичайно, не очікував, що так багато людей викличе це! linecacheможе бути корисним для зменшення кількості вводу-виводу файлів (і дякую, що нагадали про це), але якби я почав оптимізувати це, мене би турбувало те, як ми надмірно повторно токенізуємо той самий файл для повторень один і той же дзвінок або для декількох сайтів дзвінків в одному файлі. Є декілька способів, які ми могли б також оптимізувати, але я не впевнений, наскільки я дійсно хочу настроїти та куленепробивати цю божевільну річ.
user2357112 підтримує Моніку

22

Так, це однозначно можливо, ось робочий приклад:

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

4
Що стосується коментаря @Prune, чи можна це зробити, щоб повернути відступ у рівнях замість пробілів? Чи завжди буде добре просто розділити на 4?
Фаб фон Беллінсгаузен

2
Ні, розділити на 4, щоб отримати рівень відступу, цей код не працює. Можна перевірити, збільшивши рівень відступу останньої заяви про друк, остання надрукована вартість просто збільшується.
Крейг Берглер

10
Хороший старт, але насправді не відповідає на питання imo. Кількість пробілів не відповідає рівню відступу.
вім

1
Це не так просто. Заміна 4 пробілів одинарними пробілами може змінити логіку коду.
wim

1
Але цей код ідеально підходить для того, що шукала ОП: (коментар до ОП № 9): "Я хотів відступити вихід мого коду відповідно до того, як він був з відступом у коді". Тож він може зробити щось на кшталтprint('{Space}'*get_indentation_level(), x)
Крейг Берглер

10

Ви можете використовувати 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

Якщо ви хочете, щоб номер рівня відступів був заснований на попередніх лініях, :ви можете зробити це з невеликою зміною:

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

запитували кількість рівнів відступу, а не кількість пробілів. вони не обов'язково пропорційні.
Вім

Для вашого демо-коду вихід повинен бути 1 - 2 - 3 - 3
Крейг Бурлер

@CraigBurgler Для отримання 1 - 2 - 3 - 3 ми можемо порахувати кількість рядків перед поточним рядком, який закінчується а, :поки ми не зустрінемо рядок з нульовим відступом, ознайомтеся з редагуванням!
Касрамвд

2
хммм ... нормально ... тепер спробуйте кілька тестових випадків @ user2357112;)
Крейг Берглер

@CraigBurgler Це рішення справедливе для більшості випадків, і щодо цієї відповіді, це також загальний спосіб, воно також не дає комплексного рішення. Спробуйте{3:4, \n 2:get_ind_num()}
Касрамвд

7

Щоб вирішити "реальну" проблему, яка призводить до вашого запитання, ви можете реалізувати контекст-менеджер, який слідкує за рівнем відступу та змушує 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

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.

12
Це дає відступ у просторах, а не в рівнях. Якщо програміст не використовує послідовну кількість відступів, це може бути некрасивим для перетворення на рівні.
Чорнослив

4
Чи функція недокументована? Я не можу його знайти тут
GingerPlusPlus
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.