python

Отступы в Python — вариант решения

  • среда, 13 мая 2020 г. в 00:29:07
https://habr.com/ru/post/501538/
  • Python
  • Программирование


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

Казалось бы в случае с Python такая операция не возможна.

По крайней мере я ни где решения такого полноценного автоформатирования для Python не смог найти. Пришлось решить эту проблему самому.

Вступление


Дальше буду использовать сокращения: ПГ — программа, ПГ-ст — программист, ППГ — подпрограмма (функция/метод) и т.п.

Основные недостатки Python:

1) Python «не минимизирован» по использованию ресурсов и своих данных и т.о. не подходит для написания ПГ, требовательных к использованию ресурсов, таких как мобильные приложения, ПГ низкого уровня (драйвера, резидентные ПГ и т.п.) и т.д.

2) Python медленный и однопоточный ( GIL — Global Interpreter Lock ).

3) В Python программные блоки основаны ТОЛЬКО(!) на отступах. Из-за этого:

  • уменьшается «читабильность» ПГ (см.ниже),
  • невозможно полноценное автоформатирование исходного текста,
  • появляется вероятность иногда очень трудно исправляемых ошибок при случайном и незамеченном ошибочном смещении отступов.

Первый недостаток Python сгладить сложно и тут он просто имеет ограничение в сфере своего использования. Но это естественный момент, т.к. невозможно придумать язык, который был бы максимально эффективен во всех сферах выполняемых задач.

Второй недостаток Python решается тем, что у него прекрасная двусторонняя
интероперабельность с C/C++.

Часто успешный проект развивается довольно стремительно. И сначала сильных требований к быстродействию нет и в Python при необходимости вставляют крошечные вставки на С/С++.
Но по мере расширения использования проекта требования к быстродействию быстро растут и Python все больше начинает выполнять функции вызываемого языка из С/С++. При этом значимость Python в проекте не падает, т.к. он остается более удобным инструментом чем С/С++ при ПГ-нии участков, не требующих большой скорости, которых в большом проекте все-равно много.

На третьем недостатке Python хочу остановиться подробнее.

Все вы знаете, для подавляющего большинства языков очень часто используется автоформат исходного текста.

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

Любой ПГ-ст, написавший проекты в несколько тысяч строк языка, знает, что процесс разработки — это часто не просто придумывание очередного куска ПГ и его набивка, а постоянный «перенос» кусков ПГ-мы то в ППГ, то в общие внешние модули, то между блоками кода.

Тем более, что на практике юзера/заказчики уже на стадии проб готового «скелета» проекта, когда они «потрогают» реальные наброски ПГ-мы, часто начинают довольно значительно «дополнять» постановку задачи, что приводит к сильной корректировке исходного кода.

И тогда в Python в случае сильного редактирования куска чужой(!) (или своей, но которую уже совершенно не помнишь) ПГ, если случайно «зацепил» ( и не заметил этого) при сдвиге/переносе блока лишний участок, который принадлежит другому блоку, то ПГ может остаться полностью работоспособной, но алгоритм ее работы поменяется (читай «ПГ начнет работать не правильно») и найти место такой ошибки в чужой(!) ПГ и потом правильно(!) восстановить отступы иногда бывает очень не просто!

В этом случае можно конечно исходный текст (до изменений) посмотреть, но если уже внес в этом месте много изменений, то придется еще и всю эту цепочку изменений «отматывать».

А вариант, как я для себя убрал третий недостаток я и хочу предложить Вашему вниманию.

Решение проблемы отступов в Python


Отступы в Python формируются следующими командами:

- class
- def

- for
- while

- if

- try

И для удаления зависимости от отступов, для каждой из этих команд я решил взять за правило использовать «закрывающую» команду, которая однозначно будет закрывать блок команд (отступов).

Команды «class» и «def»


Для команд class/def обычно проблемы с завершением блока отступов нет, т.к. блок завершается новой командой class/def.

Единственный случай — это когда есть одна или несколько ППГ, объявленных внутри другой ППГ/метода.

def ppg_1():
    def ppg_2():
        ... команды ppg_2 ...
    def ppg_3():
        ... команды ppg_3 ...
        ... команды ppg_3 ...
        ... команды ppg_3 ...
    ... команды ppg_1 ...

Тогда блок команд последней из этих внутренних ППГ может слиться с командами ППГ/метода, в которой объявлена эта внутренняя ППГ, если случайно ошибочно сместить этот блок команд последней внутренней ППГ.

def ppg_1():
    def ppg_2():
        ... команды ppg_2 ...
    def ppg_3():
        ... команды ppg_3 ...
    ... команды ppg_3 ...
    ... команды ppg_3 ...
    ... команды ppg_1 ...

Но в этом случае в конце этой внутренней ППГ/метода нужно просто поставить «return» и
это и будет точно конец ППГ/метода.

def ppg_1():
    def ppg_2():
        ... команды ppg_2 ...
    def ppg_3():
        ... команды ppg_3 ...
        ... команды ppg_3 ...
        ... команды ppg_3 ...
        return
    ... команды ppg_1 ...

Команды «for» и «while»


В конце оператора «for» и «while» нужно ставить «continue».

Т.е. команда будут выглядеть так:

for  <...> :             # начало блока
    ... команды ....
    continue             # конец блока

и

while  <...> :           # начало блока
    ... команды ....
    continue             # конец блока

Например, возьмем кусок исходного кода:

        f = open(imya_file,'r',encoding='utf-8')

        i = 0
        for line in f:
            #print(line)
            tek_str = line.rstrip('\n')             #Уберем \n на концах строки
            tek_spisok = tek_str.split('\t')        #Разбивка строк в список

            REGP = tek_spisok[0]
            DATA = tek_spisok[1]
            NNMT = tek_spisok[2]

            tb0.setItem( i, 0, QtWidgets.QTableWidgetItem(REGP) )
            tb0.setItem( i, 1, QtWidgets.QTableWidgetItem(DATA) )
            tb0.setItem( i, 2, QtWidgets.QTableWidgetItem(NNMT) )

            i += 1

        f.close()

Случайное удаление отступа в последних командах блока «for» не приводит к ошибке выполнения программы, но приводит к ошибочным результатам! И не просто искать что случилось, если не знаешь точно алгоритм ПГ (например, если это ПГ уволившегося коллеги)!

Вот как здесь:

        f = open(imya_file,'r',encoding='utf-8')

        i = 0   
        for line in f:   
            #print(line)
            tek_str = line.rstrip('\n')             #Уберем \n на концах строки
            tek_spisok = tek_str.split('\t')        #Разбивка строк в список

        REGP = tek_spisok[0]
        DATA = tek_spisok[1]
        NNMT = tek_spisok[2]

        tb0.setItem( i, 0, QtWidgets.QTableWidgetItem(REGP) )
        tb0.setItem( i, 1, QtWidgets.QTableWidgetItem(DATA) )
        tb0.setItem( i, 2, QtWidgets.QTableWidgetItem(NNMT) )

        i += 1

        f.close()

А в нижеуказанном случае случайное удаление отступа сразу выдаст ошибку:

        f = open(imya_file,'r',encoding='utf-8')
        i = 0   
        for line in f:   
            #print(line)
            tek_str = line.rstrip('\n')             #Уберем \n на концах строки
            tek_spisok = tek_str.split('\t')        #Разбивка строк в список

            REGP = tek_spisok[0]
            DATA = tek_spisok[1]
            NNMT = tek_spisok[2]

            tb0.setItem( i, 0, QtWidgets.QTableWidgetItem(REGP) )
            tb0.setItem( i, 1, QtWidgets.QTableWidgetItem(DATA) )
            tb0.setItem( i, 2, QtWidgets.QTableWidgetItem(NNMT) )

            i += 1

            continue

        f.close()

Команда «if»


В конце оператора «if» нужно ставить команду «elif 0: pass», а вместо «else» использовать команду «elif 1:».

Т.е. для «if» будет законченный блок команд:

if <>                      # начало блока
    ... команды ....
elif <>
    ... команды ....
elif 1:                    # вместо "else"
    ... команды ....
elif 0: pass               # конец блока

Например:

        ret = 0
	find_port = seek_port()

        if find_port != 'yes':

            #Выведем ошибку,что сканер не найден
            okno_info( 'Сканер не найден', font_medium, bg_txt)

            ret = 1

            Dialog_scan.close()

        elif 0: pass

        QR_scan_main( 'Ряды', 1 )

Если так, как выше оформить, то в блоке команд «if… elif 0: pass» удаление отступа сформирует ошибку запуска.

Без «elif 0: pass» если потом случайно удалятся отступы в последних строках блока «if», то сначала будешь искать место, из-за которого ПГ неожиданно стала не правильно работать, а потом думать: какие отступы д.б. в блоке, а какие — нет.

        ret = 0
        find_port = seek_port()

        if find_port != 'yes':

            #Выведем ошибку,что сканер не найден
            okno_info( 'Сканер не найден', font_medium, bg_txt)

        ret = 1

        Dialog_scan.close()

        QR_scan_main( 'Ряды', 1 )

Почему еще на мой взгляд желательно закрывать блоки команд, созданные операторами «for»,
«while», «if» b т.д…

Потому что, когда смотришь на конец завершающихся отступов, но при этом не видишь, где они начинаются, то уже часто не помнишь, какой отступ чем создан.

Тогда конструкции с «continue» и «elif 0: pass» еще (кроме защиты от случайного удаления отступов) позволят указать какой блок чем начат и написать комментарий к нему.

Например, видишь конец большого блока:

                            #print("{0}\t{1}".format(key,slovar[key]),end="\n")
                            zn = str("{0}\t{1}".format(key,slovar[key]))
                            new_zn = zn.replace("\\r\\n'","")
                            itog_zn = itog_zn + new_zn[2:] + "\n"

                            if n == 1:
                                itog_str = tek_polya + str(itog_zn)
                                f.write(str(itog_str))
                                itog_zn = ''
                                
                            elif n == 10 or len(slovar) == n:
                                itog_str = str(itog_zn)
                                f.write(str(itog_str))
                                itog_zn = ''
                            n += 1

                        f.close()                           

                        #Копируем файл txt в bak
                        shutil.copyfile(kat+"\\"+imya_f+'.txt', kat+"\\"+imya_f+'.bak')

                        #Выведем информационное сообщение
                        self.okno_info( 'Вы отсканировали '+str(len(slovar))+
                                        ' коробки(ок) в '+str(kych)+' ряд(ах)',
                                        self.font_medium, self.bg_txt)
                        self.Dialog_scan.close()

                      
    elif res == 'Выйти(ESC)':
        self.Menu.close()
        self.Dialog_scan.close()

И сложно вспомнить, что значит каждый уровень отступов.

Но уже гораздо проще, когда видишь вот так:

                            #print("{0}\t{1}".format(key,slovar[key]),end="\n")
                            zn = str("{0}\t{1}".format(key,slovar[key]))
                            new_zn = zn.replace("\\r\\n'","")
                            itog_zn = itog_zn + new_zn[2:] + "\n"

                            if n == 1:
                                itog_str = tek_polya + str(itog_zn)
                                f.write(str(itog_str))
                                itog_zn = ''
                                
                            elif n == 10 or len(slovar) == n:
                                itog_str = str(itog_zn)
                                f.write(str(itog_str))
                                itog_zn = ''

                            elif 0: pass

                            n += 1

                            continue    # убираем символов b'код\\r\\n' по словарю 

                        f.close()                           

                        #Копируем файл txt в bak
                        shutil.copyfile(kat+"\\"+imya_f+'.txt', kat+"\\"+imya_f+'.bak')

                        #Выведем информационное сообщение
                        self.okno_info( 'Вы отсканировали '+str(len(slovar))+
                                        ' коробки(ок) в '+str(kych)+' ряд(ах)',
                                        self.font_medium, self.bg_txt)
                        self.Dialog_scan.close()

                    elif 0: pass     # варианты "закончили ряд" или "весь товар"

                elif 0: pass         # Если был спец.коды - обрабатываем

                continue             # цикл по сканированию кодов

            continue                 # цикл бесконеч., выход - когда "весь товар"
                      
    elif res == 'Выйти(ESC)':
        self.Menu.close()
        self.Dialog_scan.close()

Команда «try»


Тут полная аналогия с «if».

try:                   # начало блока
    ... команды ....
except <...>:
    ... команды ....
except 1:              # вместо "else"
    ... команды ....
except 0: pass         # конец блока

Единственная проблема — команда «finally:». После нее больше нельзя поставить
ни одну их команд текущего try-блока.

Поэтому если есть сильная необходимость в ее использовании, то для сохранения возможности автоформата и защиты от случайного удаления отступов, нужно убрать весь блок команд после «finally:» в ППГ и эту ППГ объявить как ППГ внутри текущей ППГ.

Т.е. текст с «finally:» будет таким:

    def my_ppg():
        ...
        return

    ...

    finally:
        my_ppg()

    ...

В этом случае тоже можно спокойно применить автоформатирование и не бояться
случайного удаления отступов.

Также, теперь я думаю, вы уже поняли, что если на Python написать ПГ с применением ВЕЗДЕ(!) вышеуказанных приемов оформления блоков отступов, то легко написать ПОЛНОЦЕННОЕ АВТОФОРМАТИРОВАНИЕ такой ПГ даже при «перекосе» или полном удалении отступов в ней, т.к. для всех блоков команд есть начало и конец блока, не зависящие от отступов.

Заключение


А теперь с улыбкой поставим вопрос так: «Какие недостатки есть у Python, если правильно оформлять блоки отступов, при необходимости писать для него на С/С++ и не применять в мобильных и ресурсокритичных приложениях ?».

Ответ: «Только несущественные недостатки. Т.е. по большому счету — нет.»

И при такой постановке вопроса нам останется только наслаждаться основными достоинствами Python.

  1. Простота.
  2. Минимальный цикл тестирования участков: написал-запустил-проверил.
  3. Мощность — библиотеки/фреймворки для Python есть «на любой вкус и цвет».

Эти три кита вместе дают, на мой взгляд, практически идеальный вариант языка (не забываем, что при условии вышеуказанной постановки вопроса!).