Читая документацию Python, вы могли встретить фрагменты БНФ-нотации (форма Бэкуса-Наура, англ. BNF Notation), которые выглядят примерно так:
name ::= lc_letter (lc_letter | "_")* lc_letter ::= "a"..."z"
Что означает весь этот странный код? Как это может помочь вам в понимании концепций Python? Как читать и интерпретировать эту нотацию?
Эта статья познакомит вас с основами БНФ-нотации Python. Вы узнаете, как ее использовать для глубокого понимания синтаксиса и грамматики языка.
Друзья, подписывайтесь на наш телеграм канал Pythonist. Там еще больше туториалов, задач и книг по Python.
Чтобы извлечь максимальную пользу из этого урока, вы должны быть знакомы с синтаксисом Python, включая ключевые слова, операторы и некоторые общие конструкции, такие как выражения, условные операторы и циклы.
Форма Бэкуса-Наура — это метасинтаксическая нотация для контекстно-свободных грамматик. Информатики часто используют эту нотацию для описания синтаксиса языков программирования, поскольку она позволяет изложить подробное описание грамматики языка.
БНФ-нотация состоит из трех основных частей:
Компонент | Описание | Примеры |
---|---|---|
Терминалы | Строки, которые должны точно совпадать с определенными элементами в инпуте | «def», «return», «:» |
Нетерминалы | Символы, которые будут заменены конкретными значениями. Также могут называться синтаксическими переменными. | <letter>, <digit> |
Правила | Соглашения о том, как соотносятся терминалы и нетерминалы | <letter> ::= «a» |
Комбинируя терминалы и нетерминалы, вы можете создавать правила БНФ, которые могут быть настолько подробными, насколько вам нужно. Нетерминалы должны иметь свои собственные определяющие правила. В грамматике у вас будет корневое правило и потенциально много вторичных правил, определяющих необходимые нетерминалы. Таким образом, в итоге может получиться иерархия правил.
Правила БНФ — это основные компоненты грамматики БНФ. Таким образом, грамматика — это набор БНФ-правил, которые также называются производственными правилами.
На практике вы можете построить набор БНФ-правил, чтобы определить грамматику языка. Здесь под языком понимается набор строк, которые допустимы в соответствии с правилами, определенными в соответствующей грамматике. БНФ в основном используется для языков программирования.
Например, синтаксис Python имеет грамматику, которая определяется как набор правил БНФ, и эти правила используются для проверки синтаксиса любого фрагмента кода Python. Если код не соответствует правилам, то вы получите ошибку SyntaxError.
Вы можете найти множество вариаций оригинальной нотации БНФ. Среди наиболее актуальных — расширенная форма Бэкуса-Наура (РБНФ) и дополненная форма Бэкуса-Наура (ДБНФ).
В следующих разделах вы познакомитесь с основами создания правил БНФ. Обратите внимание, что вы будете использовать разновидность БНФ, соответствующую требованиям сайта BNF Playground, где вы сможете протестировать свои правила.
Как вы уже узнали, комбинируя терминалы и нетерминалы, вы можете создавать правила БНФ. Эти правила обычно имеют следующий синтаксис:
<symbol> ::= expression
В синтаксисе правил БНФ есть следующие части:
<символ>
— нетерминальная переменная, которая часто заключена в угловые скобки (<>)::=
означает, что нетерминал слева будет заменен выражением справа.expression
(выражение) состоит из ряда терминалов, нетерминалов и других символов, которые определяют конкретный фрагмент грамматики.При построении правил БНФ вы можете использовать различные символы с определенными значениями. Например, если вы собираетесь составлять и тестировать правила на сайте BNF Playground, то вы будете применять некоторые из следующих символов:
Символ | Значение |
---|---|
"" | Окружает терминальный символ |
<> | Обозначает нетерминальный символ |
() | Указывает на группу допустимых вариантов |
+ | Указывает на один или несколько предыдущих элементов |
* | Указывает на ноль или более предыдущих элементов |
? | Указывает на ноль или одно вхождение предыдущего элемента |
| | Указывает, что вы можете выбрать один из вариантов |
[x-z] | Указывает на диапазон букв или цифр |
Узнав, как написать правило БНФ и какие символы использовать, вы можете приступить к созданию собственных правил. Обратите внимание, что в BNF Playground есть несколько дополнительных символов и синтаксических конструкций, которые вы можете применять в своих правилах. Для получения полной информации щелкните раздел «Grammar Help» в верхней части страницы сайта BNF Playground.
Теперь пора поиграться с парой пользовательских правил БНФ. Начнем с общего примера.
Допустим, вам нужно создать контекстно-свободную грамматику для определения того, как пользователь должен вводить полное имя человека. В данном случае полное имя будет состоять из трех компонентов:
Все компоненты должны отделяться друг от друга пробелами. Второе имя также следует рассматривать как необязательное. Вот как можно определить это правило:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name>
Левая часть вашего правила БНФ — это нетерминальная переменная, которая идентифицирует полное имя человека. Символ ::=
обозначает, что переменная <full_name>
будет заменена правой частью правила.
Правая часть правила состоит из нескольких компонентов. Во-первых, у вас есть имя, которое вы определяете с помощью нетерминала <first_name>
. Далее вам нужен пробел, чтобы отделить имя от следующего компонента. Чтобы задать этот пробел, используется терминал, который состоит из символа пробела между кавычками.
После имени можно указать второе имя, а после него нужен еще один пробел. Чтобы сгруппировать эти два элемента, вы раскрываете круглые скобки. Затем вы создаете <middle_name>
и терминал " "
. Оба элемента необязательны, поэтому после них ставится знак вопроса (?
), чтобы обозначить это условие.
Наконец, вам нужна фамилия. Чтобы определить этот компонент, вы используете еще один нетерминал, <family_name>
. Вот и все! Вы построили свое первое правило БНФ. Однако у вас все еще нет рабочей грамматики. У вас есть только корневое правило.
Чтобы завершить грамматику, вам нужно определить правила для <first_name>
, <middle_name>
и <family_name>
. При этом должны соблюдаться некоторые требования:
В этом случае можно начать с определения двух правил: одного для заглавных и одного для строчных букв:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name> <uppercase_letter> ::= [A-Z] <lowercase_letter> ::= [a-z]
В этом фрагменте грамматики вы создаете два довольно похожих правила. Первое правило принимает все заглавные буквы ASCII от A до Z. Второе правило принимает все строчные буквы. В этом примере не поддерживаются буквы с дополнительными значками и буквы, не входящие в ASCII.
Установив эти правила, вы можете создать остальные. Для начала добавьте правило <first_name>
:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name> <uppercase_letter> ::= [A-Z] <lowercase_letter> ::= [a-z] <first_name> ::= <uppercase_letter> <lowercase_letter>*
Чтобы определить правило <first_name>
, начните с нетерминала <uppercase_letter>
, чтобы выразить, что первая буква имени должна быть заглавной. Далее следует нетерминал <lowercase_letter>
, за которым следует звездочка (*
). Эта звездочка означает, что в имени может быть ноль или более строчных букв после начальной заглавной.
По такому же принципу можно построить правила <middle_name>
и <family_name>
. Хотите попробовать? Закончив, откройте секцию ниже, чтобы получить полную грамматику и сравнить ее со своей.
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name> <uppercase_letter> ::= [A-Z] <lowercase_letter> ::= [a-z] <first_name> ::= <uppercase_letter> <lowercase_letter>* <middle_name> ::= <uppercase_letter> <lowercase_letter>* <family_name> ::= <uppercase_letter> <lowercase_letter>*
Вы можете проверить, работает ли грамматика вашего полного имени, с помощью сайта BNF Playground.
Перейдя на сайт BNF Playground, вы можете вставить свои грамматические правила в область ввода текста. Затем нажмите кнопку COMPILE BNF. Если с правилами БНФ все в порядке, можно ввести полное имя в поле ввода Test a string here!. После того как вы введете полное имя человека, поле станет зеленым, если введенная строка соответствует правилам.
В предыдущем разделе вы познакомились с созданием БНФ-грамматики, определяющей, как пользователи должны вводить имя человека. Это общий пример, который может иметь или не иметь отношения к программированию. В этом разделе вы перейдете к более техническим вопросам. Вы напишете короткий набор БНФ-правил для проверки идентификатора в гипотетическом языке программирования.
Идентификатором может быть переменная, функция, класс или имя объекта. В вашем примере вы напишете набор правил для проверки соответствия заданной строки следующим требованиям:
Вот корневое правило для вашего идентификатора:
<identifier> ::= <char> (<char> | <digit>)*
В этом правиле есть нетерминальная переменная <identifier>
, которая определяет корень. В правой части сначала находится нетерминал <char>
. Остальные элементы идентификатора сгруппированы внутри круглых скобок. Звездочка после группы говорит о том, что элементы из этой группы могут встречаться ноль или более раз. Каждый такой элемент — это буквенный или цифровой символ или символ подчеркивания.
Теперь вам нужно определить нетерминалы <char>
и <digit>
с их собственными правилами. Они будут выглядеть так, как показано в коде ниже:
<identifier> ::= <char> (<char> | <digit>)* <char> ::= [A-Z] | [a-z] | "_" <digit> ::= [0-9]
Правило <char>
принимает одну букву ASCII в нижнем или верхнем регистре. Кроме того, оно может принимать знак подчеркивания. Наконец, правило <digit>
принимает цифру от 0 до 9. Теперь ваш набор правил готов. Испытайте его на сайте BNF Playground.
Для программиста чтение правил БНФ может быть довольно полезным навыком. Например, часто можно обнаружить, что официальная документация многих языков программирования полностью или частично включает в себя БНФ- грамматику этих языков. Таким образом, умение читать БНФ позволит вам лучше понять синтаксис и тонкости языка.
А теперь давайте перейдем к БНФ-варианту Python, который вы найдете в нескольких частях документации по языку.
Python использует собственную разновидность БНФ-нотации для определения грамматики языка. Фрагменты БНФ-грамматики вы найдете во многих разделах документации по Python. Эти фрагменты могут помочь вам лучше понять любую синтаксическую конструкцию, которую вы изучаете.
Python-разновидность БНФ использует следующий стиль:
Символ | Значение |
---|---|
name | Содержит имя правила или нетерминала |
::= | Означает «расширить в» |
| | Разделяет альтернативы |
* | Принимает ноль или более повторов предыдущего элемента |
+ | Принимает одно или несколько повторов предыдущего элемента |
[] | Принимает ноль или одно вхождение, что означает, что вложенный элемент является необязательным |
() | Группирует варианты |
"" | Определяет литеральные строки |
пробел | Имеет смысл только для разделения токенов |
Эти символы определяют Python-разновидность БНФ. Одно из заметных отличий от того, как выглядят обычные правила БНФ, заключается в том, что Python не использует угловые скобки (<>
) для заключения нетерминальных символов. Он использует только идентификатор или имя нетерминала. Возможно, это делает правила более чистыми и читабельными.
Также обратите внимание, что квадратные скобки ([]
) имеют в Python другое значение. До сих пор вы использовали их для заключения наборов символов, таких как [a-z]
. В Python эти скобки означают, что заключенный в них элемент является необязательным. Чтобы определить что-то вроде [a-z]
в БНФ-варианте Python, вы будете использовать "a"... "z"
.
В документации по Python вы найдете множество фрагментов БНФ. Научиться ориентироваться и читать их — довольно полезный навык для разработчика Python. Поэтому в следующих разделах мы рассмотрим несколько примеров БНФ-правил из документации Python и научимся их читать.
Теперь, когда вы знаете основы чтения БНФ- нотации и познакомились с особенностями БНФ-варианта Python, пришло время начать читать БНФ-грамматику из документации Python. Таким образом вы приобретете необходимые навыки, чтобы использовать преимущества этой нотации для изучения Python и его синтаксиса.
Начнем с оператора pass
. Это простой оператор, который позволяет вам ничего не делать. Нотация БНФ для этого оператора выглядит следующим образом:
pass_stmt ::= "pass"
Здесь у вас есть имя правила, pass_stmt
. Затем имеется символ ::=
, указывающий на то, что правило расширяется до «pass», который является терминальным символом. Это означает, что данное утверждение состоит из одного только ключевого слова pass. Никаких дополнительных синтаксических компонентов нет. Итак, вы знаете синтаксис оператора pass
:
pass
Правило БНФ для оператора pass
— одно из самых простых правил, которые вы найдете в документации. Оно содержит только терминал, который прямо определяет синтаксис.
Другой распространенный оператор, который вы часто используете при написании кода, — это return
. Этот оператор немного сложнее, чем pass
. Вот правило БНФ для return
из документации:
return_stmt ::= "return" [expression_list]
В этом случае у вас есть имя правила, return_stmt
, и ::=
, как обычно. Затем имеется терминальный символ, состоящий из слова return
. Второй компонент этого правила — необязательный список выражений, expression_list
. Вы знаете, что этот второй компонент является необязательным, потому что он заключен в квадратные скобки.
Наличие необязательного списка выражений после слова return
согласуется с тем, что Python допускает операторы return
без явного возвращаемого значения. В этом случае язык автоматически возвращает None — нулевое значение Python:
>>> def func(): ... return ... >>> print(func()) None
Эта функция использует «голый» return
без указания явного возвращаемого значения. В этом случае Python автоматически возвращает None.
Если вы нажмете на переменную expression_list
в документации, вы попадете на следующее правило:
expression_list ::= expression ("," expression)* [","]
Опять же, у вас есть имя правила и символ ::=
. Затем у вас есть обязательная нетерминальная переменная expression
. У этого нетерминального символа есть собственное правило определения, к которому можно перейти, щелкнув на самом символе.
До этого момента у вас был синтаксис оператора return
с одним возвращаемым значением:
>>> def func(): ... return "Hello!" ... >>> func() 'Hello!'
В этом примере в качестве возвращаемого значения функции используется строка «Hello!». Обратите внимание, что возвращаемым значением может быть любой объект или выражение Python.
Правило продолжается раскрытием круглых скобок. Помните, что в БНФ круглые скобки используются для группировки объектов. В данном случае у вас есть терминал, состоящий из запятой («,»), а затем снова символ выражения. Звездочка после закрывающих круглых скобок указывает на то, что эта конструкция может встречаться ноль или более раз.
В этой части правила описываются операторы return
с несколькими возвращаемыми значениями:
>>> def func(): ... return "Hello!", "Pythonista!" ... >>> func() ('Hello!', 'Pythonista!')
Теперь ваша функция возвращает два значения. Для этого вы предоставляете ряд значений, разделенных запятыми. Вызвав функцию, вы получаете кортеж значений.
Последняя часть правила — [","]
. Она говорит о том, что список выражений может включать необязательную запятую в конце. Эта запятая может привести к неясным результатам:
>>> def func(): ... return "Hello!", ... >>> func() ('Hello!',)
В этом примере вы используете запятую после единственного возвращаемого значения. В результате ваша функция возвращает кортеж с одним элементом. Но обратите внимание, что запятая не оказывает никакого влияния, если у вас уже есть несколько значений, разделенных запятыми:
>>> def func(): ... return "Hello!", "Pythonista!", ... >>> func() ('Hello!', 'Pythonista!')
В этом примере вы добавляете завершающую запятую в конце оператора return с несколькими возвращаемыми значениями. И снова при вызове функции вы получаете кортеж значений.
Еще один интересный фрагмент БНФ, который можно найти в документации Python, определяет синтаксис выражений присваивания, которые строятся с помощью моржового оператора (англ. walrus).
От редакции Pythonist: о моржовом операторе можно почитать в статье «7 фишек Python максимально улучшающие твой код».
Вот корневое БНФ-правило для выражений такого типа:
assignment_expression ::= [identifier ":="] expression
Правая часть правила начинается с необязательного компонента, который включает нетерминал, называемый identifier
, и терминал, состоящий из символа ":="
. Этот символ — сам моржовый оператор. Затем стоит обязательное выражение.
Примечание. На первый взгляд может показаться странным, что часть присваивания необязательна, ведь суть выражения присваивания заключается в самом присваивании. Однако, сделав эту часть необязательной, вы значительно упрощаете многие правила грамматики, поскольку выражение присваивания допускается почти везде, где допускается обычное выражение. Пример такого упрощения вы увидите в следующем разделе.
Это соответствует синтаксису выражения присваивания с моржовым оператором:
identifier := expression
Обратите внимание, что в выражении присваивания часть присваивания необязательна. Вы получите то же значение при оценке выражения независимо от того, выполняете вы присваивание или нет.
Вот рабочий пример выражения присваивания:
>>> (length := len([1, 2, 3])) 3 >>> length 3
В этом примере создается выражение, которое присваивает переменной length
количество элементов в списке.
Обратите внимание, что выражение заключено в круглые скобки. В противном случае возникло бы исключение SyntaxError. Ознакомьтесь с разделом «Walrus Operator Syntax» статьи «Walrus Operator: Python 3.8 Assignment Expressions», чтобы понять, зачем нужны круглые скобки.
Теперь, когда вы научились читать правила БНФ для простых выражений, можно перейти к сложным. Условные операторы довольно часто встречаются в любом фрагменте кода Python. В документации Python приведено БНФ-правило для этого типа утверждений:
if_stmt ::= "if" assignment_expression ":" suite ("elif" assignment_expression ":" suite)* ["else" ":" suite]
Когда вы начинаете читать это правило, вы сразу же находите терминальный символ "if"
, который должен использоваться для начала любого условного оператора. Затем вы найдете нетерминал assignment_expression
, который вы уже изучали в предыдущем разделе.
Примечание. Правило if_stmt
использует нетерминал assignment_expression
для определения условия. Это позволяет использовать в условии как выражение присваивания, так и обычное выражение. Помните, что часть присваивания в выражении assignment_expression
необязательна.
Далее идет терминал ":"
. Его нужно использовать в конце заголовка составного оператора. Это двоеточие означает, что заголовок оператора завершен. Наконец, у вас есть обязательный нетерминал, называемый suite
, который представляет собой набор утверждений, выделенный отступом.
Следуя этой первой части правила, вы получите следующий синтаксис языка Python:
if assignment_expression: suite
Это — стандартный оператор if
. Он начинается с ключевого слова if
. Затем идет выражение, которое Python оценивает на предмет истинности. И наконец, двоеточие, которое открывает возможность иметь блок, выделенный отступом.
Вторая строка правила БНФ определяет синтаксис оборота elif
. В этой строке ключевое слово elif
является терминальным символом. Затем выражение, двоеточие и снова блок кода с отступами:
if assignment_expression: suite elif assignment_expression: suite
В условном операторе может быть ноль или более пунктов elif
, на что указывает звездочка после закрывающих круглых скобок. Все они будут следовать одному и тому же синтаксису.
Последней частью БНФ-правила касательно условия является предложение else, которое состоит из ключевого слова else
, за которым следует двоеточие и кода, выделенного отсутпом. Вот как это переводится в синтаксис Python:
if assignment_expression: suite elif assignment_expression: suite else: suite
Предложение else
также является необязательным в Python. В БНФ-правиле на это указывают квадратные скобки, окружающие заключительную строку правила.
Вот игрушечный пример рабочего условного оператора:
>>> def read_temperature(): ... return 25 ... >>> if (temperature := read_temperature()) < 10: ... print("The weather is cold!") ... elif 10 <= temperature <= 25: ... print("The weather is nice!") ... else: ... print("The weather is hot!") ... The weather is nice!
В обороте if вы используете выражение присваивания, чтобы получить текущее значение температуры. Затем вы сравниваете текущее значение с 10. Затем вы используете значение температуры для создания выражения в предложении elif
. И наконец, для тех случаев, когда температура высокая, есть пункт else
.
Циклы — это еще один часто используемый составной оператор в Python. В Python есть два оператора цикла: for
и while
.
Грамматика БНФ для цикла for
в Python выглядит следующим образом:
for_stmt ::= "for" target_list "in" starred_list ":" suite ["else" ":" suite]
Первая строка определяет заголовок цикла, который начинается с терминала "for"
. Затем идет нетерминал target_list
. Он представляет переменную или переменные цикла.
Далее идет терминал "in"
, который представляет ключевое слово in
. Символ нетерминала starred_list
представляет объект итерируемого цикла. И наконец, двоеточие, которое дает пропуск к блоку кода с отступами, suite
.
Примечание. Грамматика Python находится в постоянном развитии. Например, в Python 3.10 правило цикла for
было записано так:
for_stmt ::= "for" target_list "in" expression_list ":" suite ["else" ":" suite]
Здесь вместо starred_list
используется expression_list
. А в Python 3.11 в циклах for
стали использоваться starred_list
. Таким образом, грамматика изменилась.
Опять же, вы можете щелкнуть любой нетерминальный символ, чтобы перейти к определяющему его БНФ-правилу и углубиться в его определение и синтаксис. Например, если вы щелкните символ target_list
, то перед вами откроются следующие правила БНФ:
target_list ::= target ("," target)* [","] target ::= identifier | "(" [target_list] ")" | "[" [target_list] "]" | attributeref | subscription | slicing | "*" target
В первой строке видно, что target_list
состоит из одного или нескольких целевых объектов, разделенных запятыми. Этот список может включать необязательную запятую в конце, что не меняет результат. На практике целевые объекты могут быть идентификатором (переменной), кортежем, списком или любым другим из предложенных вариантов. Символы |
дают понять, что все эти значения являются отдельными альтернативами.
Вторая строка правила БНФ для цикла for
определяет синтаксис предложения else. Этот пункт является необязательным, о чем свидетельствуют квадратные скобки. Строка состоит из терминала «else», за которым следует двоеточие и набор кода с отступом.
Вы можете перевести приведенное выше правило БНФ в следующий синтаксис Python:
for target_list in starred_list: suite else: suite
Цикл имеет ряд переменных цикла, разделенных запятыми, в target_list
и итерируемый объект данных, представленный starred_list
.
Вот быстрый пример цикла for
:
>>> high = 5 >>> for number in range(high): ... if number > 5: ... break ... print(number) ... else: ... print("range covered") ... 0 1 2 3 4 range covered
В этом цикле выполняется итерация по диапазону чисел от 0 до high
. В этом примере значение high
равно 5, поэтому оператор break
не выполняется, а в конце цикла выполняется предложение else
. Если вы измените значение high
на 10, то оператор break
будет выполняться, а else
— нет.
Примечание. Не имеет смысла использовать цикл с предложением else
, если в основном наборе цикла нет оператора break
. Если вы оказались в такой ситуации, удалите заголовок else
: и разгруппируйте его suite
.
Что касается циклов while
, то их БНФ-правило выглядит следующим образом:
while_stmt ::= "while" assignment_expression ":" suite ["else" ":" suite]
Циклы while
в Python начинаются с ключевого слова while
, которое является первым компонентом в правой части правила. Затем вам понадобится assignment_expression
, двоеточие и код, выделенный отступом:
while assignment_expression: suite else: suite
Обратите внимание, что цикл while
также имеет необязательное предложение else
, которое работает так же, как и в цикле for
. Можете ли вы придумать работающий пример цикла while
?
При чтении БНФ-правил в документации Python вы можете практиковать некоторые приемы, чтобы лучше понимать прочитанное. Вот несколько рекомендаций:
Если вы примените эти рекомендации при чтении БНФ, то почувствуете себя гораздо комфортнее. Вы сможете лучше понять правила и улучшить свои навыки работы с Python.
Теперь вы знаете, что такое нотация БНФ и как она используется в официальной документации Python. Вы познакомились с основами Python-версии нотации БНФ и научились ее читать. Это довольно сложный навык, который поможет вам лучше понять синтаксис и грамматику языка.
Знание того, как читать БНФ-нотацию в документации Python, позволит вам лучше и глубже понять синтаксис и грамматику Python. Дерзайте!
Перевод статьи «BNF Notation: Dive Deeper Into Python’s Grammar».
Управление памятью - важный, но часто упускаемый из виду аспект программирования. При неправильном подходе оно…
Как возникает круговой импорт? Эта ошибка импорта обычно возникает, когда два или более модуля, зависящих…
Вы когда-нибудь оказывались в ситуации, когда скрипт на Python выполняется очень долго и вы задаетесь…
В этом руководстве мы разберем все, что нужно знать о символах перехода на новую строку…
Блок if __name__ == "__main__" в Python позволяет определить код, который будет выполняться только при…
Давайте разберем, как настроить модульные тесты для экземпляров классов. Мы напишем тесты для проверки функциональности…