6.Множественное ветвление
Оператор IF разделяет программу на два набора операторов, выполняемых в зависимости от результата вычисления заданного условия. Бывают ситуации, когда этого недостаточно. Например, пусть мы пишем программу-калькулятор. Она запрашивает у пользователя два числа и код выполняемого над ними действия (1 – сложение, 2 – умножение, 3 – вычитание, 4 — деление). Конечно, можно обойтись и кучей операторов IF, например, так:
и т.д. Но выглядит такая программа запутанно. Здесь лучше применить оператор множественного ветвления CASE. Для его использования должны соблюдаться два правила: во-первых, во всех проверяемых условиях одна и та же переменная должна сравниваться с константами. Так оно и есть: переменная с сравнивается с единицей, двойкой и т.д. Во-вторых, тип переменной с должен быть ограниченным. Это означает, что можно перечислить все значения этого типа. Скажем, тип BYTE – ограниченный, так как переменная типа BYTE принимает значения только от 0 до 255 и все их можно перечислить. А вот переменная типа REAL принимает бесчисленное множество значений (число дробных чисел в любом интервале бесконечно) и ее нельзя использовать в операторе CASE. Его общий вид таков:
Множественное ветвление: if-elif-else. Оператор match в Python
Ранее мы рассмотрели работу условного оператора if . С помощью его расширенной версии if-else можно реализовать две отдельные ветви выполнения. Однако алгоритм программы может предполагать выбор больше, чем из двух путей, например, из трех, четырех или даже пяти. В данном случае следует говорить о необходимости множественного ветвления.
Рассмотрим конкретный пример. Допустим, в зависимости от возраста пользователя, ему рекомендуется определенный видеоконтент. При этом выделяют группы от 3 до 6 лет, от 6 до 12, от 12 до 16, 16+. Итого 4 диапазона. Как бы мы стали реализовывать задачу, имея в наборе инструментов только конструкцию if-else?
Самый простой ответ – последовательно проверять вхождение введенного числа-возраста в определенный диапазон с помощью следующих друг за другом условных операторов:
old = int(input('Ваш возраст: ')) print('Рекомендовано:', end=' ') if 3 old 6: print('"Заяц в лабиринте"') if 6 old 12: print('"Марсианин"') if 12 old 16: print('"Загадочный остров"') if 16 old: print('"Поток сознания"')
Примечание. Названия фильмов выводятся на экран в двойных кавычках. Поэтому в программе для определения строк используются одинарные.
Предложенный код прекрасно работает, но есть одно существенное «но». Он не эффективен, так как каждый if в нем – это отдельно взятый оператор, никак не связанный с другими if . Процессор тратит время и «нервы» на обработку каждого из них, даже если в этом уже нет необходимости. Например, введено число 10. В первом if логическое выражение возвращает ложь, и поток выполнения переходит ко второму if . Логическое выражение в его заголовке возвращает истину, и его тело выполняется. Всё, на этом программа должна была остановиться.
Однако следующий if никак не связан с предыдущим, поэтому далее будет проверяться вхождение значения переменной old в диапазон от 12 до 16, в чем необходимости нет. И далее будет обрабатываться логическое выражение в последнем if , хотя уже понятно, что и там будет False .
Решить проблему избыточности проверок можно, вкладывая условные операторы друг в друга:
old = int(input('Ваш возраст: ')) print('Рекомендовано:', end=' ') if 3 old 6: print('"Заяц в лабиринте"') else: if 6 old 12: print('"Марсианин"') else: if 12 old 16: print('"Загадочный остров"') else: if 16 old: print('"Поток сознания"')
Рассмотрим поток выполнения этого варианта кода. Сначала проверяется условие в первом if (он же самый внешний). Если здесь было получено True , то тело этого if выполняется, а в ветку else мы даже не заходим, так как она срабатывает только тогда, когда в условии if возникает ложь.
Если внешний if вернул False , поток выполнения программы заходит в соответствующий ему внешний else . В его теле находится другой if со своим else . Если введенное число попадает в диапазон от 6 до 12, то выполнится тело вложенного if , после чего программа завершается. Если же число не попадает в диапазон от 6 до 12, то произойдет переход к ветке else . В ее теле находится свой условный оператор, имеющий уже третий уровень вложенности.
Теперь зададимся следующим вопросом. Можно ли как-то оптимизировать код множественного ветвления и не строить лестницу из вложенных друг в друга условных операторов? Во многих языках программирования, где отступы используются только для удобства чтения программистом, но не имеют никакого синтаксического значения, часто используется подобный стиль:
if логическое_выражение < … ; >else if логическое_выражение < … ; >else if логическое_выражение < … ; >else
Может показаться, что имеется только один уровень вложенности, и появляется новое расширение для if , выглядящее как else if . Но это только кажется. На самом деле if , стоящее сразу после else , является вложенным в это else . Выше приведенная схема – то же самое, что
if логическое_выражение < … ; >else if логическое_выражение < … ; >else if логическое_выражение < … ; >else
Именно так ее «понимает» интерпретатор или компилятор. Однако считается, что человеку проще воспринимать первый вариант.
В Питоне такое поднятие вложенного if к внешнему else невозможно, потому что здесь отступы и переходы на новую строку имеют синтаксическое значение. Поэтому в язык Python встроена возможность настоящего множественного ветвления на одном уровне вложенности, которое реализуется с помощью веток elif.
Слово «elif» образовано от двух первых букв слова «else», к которым присоединено слово «if». Это можно перевести как «иначе если».
В отличие от else , в заголовке elif обязательно должно быть логическое выражение также, как в заголовке if . Перепишем нашу программу, используя конструкцию множественного ветвления:
old = int(input('Ваш возраст: ')) print('Рекомендовано:', end=' ') if 3 old 6: print('"Заяц в лабиринте"') elif 6 old 12: print('"Марсианин"') elif 12 old 16: print('"Загадочный остров"') elif 16 old: print('"Поток сознания"')
Обратите внимание, в конце, после всех elif , может использоваться одна ветка else для обработки случаев, не попавших в условия ветки if и всех elif . Блок-схему полной конструкции if-elif-…-elif-else можно изобразить так:
Как только тело if или какого-нибудь elif выполняется, программа сразу же возвращается в основную ветку (нижний ярко-голубой прямоугольник), а все нижеследующие elif , а также else пропускаются.
Оператор match-case в Python
Начиная с версии 3.10 в Питоне появился оператор match , который можно использовать как аналог оператора switch , который есть в других языках. На самом деле возможности match немного шире.
В match множественное ветвление организуется с помощью веток case :
match имя_переменной: case значение_1: действия case значение_2: действия …
Слова match-case можно перевести как «соответствовать случаю». То есть, если значение переменной или выражения при match соответствует значению при каком-либо case , то выполнятся действия, вложенные в этот case .
В отличие от if-elif здесь нельзя использовать логические выражения. После case должен находится литерал, конкретное значение, выражение, возвращающее однозначный результат.
Рассмотрим программу, в которой реализовать множественное ветвление с помощью match-case удобнее, чем через if-elif-else:
sign = input('Знак операции: ') a = int(input('Число 1: ')) b = int(input('Число 2: ')) match sign: case '+': print(a + b) case '-': print(a - b) case '/': if b != 0: print(round(a / b, 2)) case '*': print(a * b) case _: print('Неверный знак операции')
Здесь значение переменной sign проверяется не на вхождение в какой-либо диапазон, а на точное соответствие заданным строковым литералам. При этом в ветках case уже не надо писать sign == ‘+’ или sign == ‘-‘ , как это пришлось бы делать в программе с if-elif:
if sign == '+': print(a + b) elif sign == '-': print(a - b) elif sign == '/': if b != 0: print(round(a / b, 2)) elif sign == '*': print(a * b) else: print('Неверный знак операции')
Код с match выглядит более ясным.
Оператор match языка Python не имеет ветки else . Вместо нее используется ветка case _ .
При одном case через оператор | можно перечислять несколько значений. Если значение переменной соответствует хотя бы одному из них, тело этого case выполнится.
sign = input('Знак операции: ') match sign: case '+' | '-' | '*': a = int(input('Число 1: ')) b = int(input('Число 2: ')) print(eval(f'a> sign> b>')) case _: print('Неверный знак операции')
В коде выше с помощью функции eval() переданная ей строка выполняется как выражение. Например, если были введены числа 3, 5 и знак *, то получится строка «3 * 5» . Вызов eval(«3 * 5») возвращает число 15.
Практическая работа
- Спишите вариант кода программы «про возраст» c if и тремя ветками elif из урока. Дополните его веткой else , обрабатывающие случаи, когда пользователь вводит числа не входящие в заданные четыре диапазона. Подумайте, почему в первой версии программы (когда использовались не связанные друг с другом условные операторы) нельзя было использовать else , а для обработки не входящих в диапазоны случаев пришлось бы писать еще один if ?
- Усовершенствуйте предыдущую программу, обработав исключение ValueError , возникающее, когда вводится не целое число.
- Напишите программу, которая запрашивает на ввод число. Если оно положительное, то на экран выводится цифра 1. Если число отрицательное, выводится -1. Если введенное число – это 0, то на экран выводится 0. Используйте в коде условный оператор множественного ветвления.
Примеры решения и дополнительные уроки в pdf-версии курса
X Скрыть Наверх
Python. Введение в программирование
Множественные ветвления и шаблон «Правила»
Здравствуйте, уважаемые хабрачитатели. В этой статье я хотел бы поделиться знаниями об одном небольшом и простом, но полезном шаблоне, про который обычно не пишут в книжках (возможно, потому, что он является частным случаем шаблона «Команда»). Это шаблон «Правила» (Rules Pattern). Вероятно, для многих он будет очень знакомым, но кому-то будет интересно с ним познакомиться.
Суть вопроса
Очень часто при разработке сложной логики возникает дерево вложенных if-ов, которое может выглядеть, например, так:
Ужасный код
public double CalculateSomething(Condition condition) < //выполняется первое условие if(condition.First. ) . //выполняется второе условие if(condition.Second. ) . //специальное условие номер один if(condition.AnotherFirst. ) < //но при этом выполняется первое условие if(condition.First) . else. >//специальное условие номер два if(condition.AnotherSecond. ) < //но при этом выполняется второе условие if(condition.Second) . else. >//и еще одно добавим if(condition.YetAnotherFirst) < //. if(condition.AnotherFirst && condition.Second) . else < . >> // O_o >
Знакомо? Итак, какие тут встречаются проблемы?
Проблема 1: Растущая цикломатическая сложность. Если говорить просто, то цикломатическая сложность — это глубина вложенности if-ов и циклов c учетом логических операторов. Инструменты анализа кода позволяют оценить этот параметр для всех участков кода. Считается, что параметр цикломатической сложности для отдельного участка кода не должен превышать 10. Из этой проблемы растет следующая.
Проблема 2: Добавление новой логики. С течением времени и добавлением новых условий становится сложно понять, куда именно добавлять новую логику и как.
Проблема 3: Дублирование кода. Если дерево условий разветвлено, то порой нельзя избавиться от ситуации, когда один и тот же код присутствует в нескольких ветках.
Тут и приходит на помощь шаблон «Правила». Его структура очень проста:
Uml-диаграмма структуры
Здесь класс Evaluator содержит коллекцию реализаций интерфейса IRule. Evaluator выполняет правила и решает, какое правило надо использовать для получения результата. Чтобы понять, как это работает и выглядит в коде, рассмотрим небольшой пример на C#.
Пример. Игра в кости (наподобие «Тали»)
Правила игры:
Игрок кидает одновременно 5 кубиков, и в зависимости от их комбинации получает определенное количество очков.
Комбинации могут быть следующими:
1 X X X X — 100 очков
5 X X X X — 50 очков
1 1 1 X X — 1000 очков
2 2 2 X X — 200 очков
3 3 3 X X — 300 очков
4 4 4 X X — 400 очков
5 5 5 X X — 500 очков
6 6 6 X X — 600 очков
Примеры комбинаций:
[1,1,1,5,1] — 1150 очков
[2,3,4,6,2] — 0 очков
[3,4,5,3,3] — 350 очков
Конечно все они могут быть и другими, и их может быть гораздо больше, но об этом позже.
Делай раз! Без шаблонов.
Попробуем описать логику игры без применения шаблона «Правила», так, как бы мы писали на уроке информатики в 8-м классе (естественно, не снабдив наш плохой код комментариями — кому они нужны!)
Плохой, негодный класс Game
public class Game < public int Score(int[] roles) < int score = 0; for(int i=1; ireturn score > private int GetSingleDieScore(int val) < if(val==1) return 100; if(val==5) return 50; return 0; >private int GetSetScore(int val) < if(val==1) return 1000; return val*100; >private int GetSetSize(int val) < return 3; >private int ScoreSetOfN(int count, int setSize, int setScore, ref int score) < if(count>=setSize) < score += setScore; return count - 3; >return count; > private int CountDiceWithValue(int[] roles, int val) < int count = 0; foreach (int r in roles) < if (r == val) count++; >return count; > >
Делай два! Добавление правил? Модульные тесты.
Вроде бы 50 строк кода это очень мало. Но что будет, если правила игры будут изменяться и добавляться?
Например, мы добавим правила для различных комбинаций кубиков:
1 1 1 1 X — 2000
1 1 1 1 1 — 4000
1 2 3 4 5 — 8000
2 3 4 5 6 — 8000
A A B B X — 4000
и так далее.
В этом случае код рискует превратиться в очень запутанный. Чтобы этого избежать, перепишем код с использованием шаблона «Правила».
(Здесь мне также стоило бы сказать о том, что до рефакторинга надо покрыть все случаи модульными тестами, побурчать об их важности и необходимости для рефакторинга кода)
Делай три! Применяем шаблон «Правила»
1. Определим интерфейс IRule с методом Eval , который нужен для оценки количества очков за определенный набор кубиков.
IRule.cs
public interface IRule
2. Создадим класс RuleSet , который будет определять набор правил, логику для добавления правила и логику выбора лучшего из правил, которое можно применить к данному набору кубиков:
RuleSet.cs
public class RuleSet < //коллекция правил private List_rules = new List(); //добавление правила public void Add(IRule rule) < _rules.Add(rule); >//оценка лучшего правила - того, которое возвращает максимальное количество очков public IRule BestRule(int[] dice) < ScoreResult bestResult = new ScoreResult(); foreach(var rule in _rules) < var result = rule.Eval(dice); if(result.Score >bestResult.Score) < bestResult = result; >return bestResult.RuleUsed; > > >
3. Конечно, небольшой класс-помощник
ScoreResult.cs
public class ScoreResult < //результат подсчета очков public int Score //какие кубики были использованы (чтобы кубик не участвовал в оценке другими правилами) public int[] DiceUsed //какое правило было использовано, чтобы определить, какое правило было лучшим (в методе BestRule) public IRule RuleUsed >
4. И определим сами правила.
ConcreteRules.cs
//правило для одного кубика public class SingleDieRule : IRule < private readonly int _value; private readonly int _score; public SingleDieRule(int dieValue, int score) < _dieValue = dieValue, _score = score >//переопределенный метод интерфейса - оценка очков для набора кубиков public ScoreResult Eval(int[] dice) < //класс-помощник var result = new ScoreResult(); //использованные в оценке кубики (кубики с номерами очков) - для дальнейшего исключения result.DiceUsed = dice.Where(d=>d == dieValue).ToArray(); //логика подсчета очков result.Score = result.DiceUsed.Count() * _score; //использованное правило - для определения лучшего правила по очкам result.RuleUsed = this; return result; > > //другие правила в том же духе
5. В нашем случае классом Evaluator со схемы будет класс Game , он не будет содержать почти ничего, кроме логики добавления правил и логики подсчета очков
Game.cs — Evaluator
public class Game < private readonly RuleSet _ruleSet = new RuleSet(); public Game(bool useAllRules) < //старые правила _ruleSet.Add(new SingleDieRule(1,100)); _ruleSet.Add(new SingleDieRule(5,50)); _ruleSet.Add(new TripleDieRule(1,1000)); for(int i=2; i//дополнительные правила if(useAllRules) < _ruleSet.Add(new FourOfADieRule(1,2000)); _ruleSet.Add(new SetOfADieRule(5,1,4000)); _ruleSet.Add(new StraightRule(8000)); _ruleSet.Add(new TwoPairsRule(6000)); for(int i=2; i> > //Пользователь может добавлять к игре свои правила public void AddScoringRule(IRule rule) < _ruleSet.Add(rule); >//подсчет очков public int Score(int[] dice) < int score = 0; var remainingDice = new List(dice); var bestRule = _ruleSet.BestRule(remainingDice.ToArray()); //проходим по правилам последовательно с выбором лучшего и удалением кубиков с подсчитанными очками while(bestRule!=null) < var result = bestRule.Eval(remainingDice.ToArray()); foreach(var die in result.DiceUsed) < remainingDice.Remove(die); >score+=result.Score; bestRule = _ruleSet.BestRule(remainingDice.ToArray()); > return score; > >
Ура! Задача решена! Теперь каждый класс занимается тем, что ему положено, цикломатическая сложность не растет,
а новые правила добавляются легко и просто. Выбор правила теперь осуществляется при помощи класса RuleSet , содержащего набор правил, а добавление правил и подсчет очков — классом Game .
О чем нужно помнить?
При проектировании программы, содержащей логику, основанную на правилах, полезно иметь ввиду следующие вопросы:
— Следует ли правилам быть read-only в отношении системы, чтобы не изменять ее состояние?
— Должны ли быть зависимости между правилами? Стоит ли уделить внимание порядку выполнения правил, в случае, когда одно правило может требовать результат работы другого правила для работы.
— Должны ли порядок выполнения правил быть строго определенным?
— Должны ли быть приоритеты в выполнении правил?
— Стоит ли позволять конечным пользователям редактировать правила?
и многие другие.
Пара слов о системах правил бизнес-логики (Business Rules Engines)
Концепция Business Rules Engines очень близка к идее шаблона «Правила» — это системы, которые позволяют
определять системы правил для бизнес-логики. Обычно они имеют некий графический интерфейс и позволяют пользователям определять правила и иерархии правил, которые могут храниться в базе данных или файловой системе. В частности, данный функционал имеет и Workflow Foundation от Microsoft.
Резюме
1) Используем шаблон «правила», когда надо избавиться от сложности условий и ветвлений
2) Помещаем логику каждого правила и его эффекты в свои классы
3) Отделяем выбор и обработку правил в отдельный класс — Evaluator
4) Знаем, что есть готовые «движковые» решения для бизнес-логики
Большое спасибо за внимание, надеюсь, моя творческая переработка данного учебного материала кому-нибудь поможет.
* Источником вдохновения для данной статьи послужил урок «Rules Pattern» из курса «Design Patterns» сайта pluralsight.com от Стива Смита
- программирование
- рефакторинг
- шаблоны проектирования
- Программирование
- Проектирование и рефакторинг
- C#
Информатика. 10 класс (Повышенный уровень)
Для записи оператора ветвления используется команда if . Формат команды:
if ( < условие >) команды 1
>
else команды 2
>
Оператор ветвления может быть в полной или сокращенной формах. В сокращенной форме отсутствует блок else :
Условие в записи оператора ветвления может быть простым и составным. Фигурные скобки могут быть опущены, если внутри них находится одна команда.
Использование управляющих конструкций в программе предполагает ее запись в структурированном виде. Структурированность программ достигается за счет отступов, регулирующих размещение вложенных алгоритмических конструкций.
Пример 4.1. Задано число x. Определить, является оно положительным или нет. Вывести соответствующее сообщение.
Этапы выполнения задания
I. Исходные данные: x (введенное число).
II. Результат: соответствующее сообщение.
III. Алгоритм решения задачи.
1. Ввод исходных данных.
2. Проверка значения выражения (x > 0) .
3. Вывод результата.
Можно соблюдать следующее правило: при движении курсора вниз от «начала» структуры до ее «конца» на пути курсора могут встретиться только пробелы. Все, что находится «внутри» структуры, размещается правее.
На практике чаще всего пользуются одним из двух правил расстановки фигурных скобок в управляющих конструкциях: