СТАТЬЯ

Работа с символьными выражениями

Из книги Пола Орланда Математика для программистов

Эта статья посвящена

● Моделирование алгебраических выражений как структур данных в Python.

● Написание кода для анализа, преобразования или вычисления алгебраического выражения.

● Построение структуры данных из элементов и комбинаторов

____________________________________________________________

Получите скидку 40 % на Математику для программистов. Просто введите fccorland в поле кода скидки при оформлении заказа на manning.com.
____________________________________________________________

В Python и других языках мы часто думаем о функциях как о мини-программах. Это автономные наборы инструкций, которые принимают некоторые входные данные, выполняют с ними определенные упорядоченные вычисления и идентифицируют некоторое результирующее значение как выходное. С точки зрения функционального программирования мы рассматриваем функции как данные, относительно которых мы можем что-то вычислять. Вы уже видели ряд функций, которые принимают другие функции в качестве входных данных или производят функции в качестве выходных данных.

Как вы вычисляете факты о функциях? Скажем, у нас есть математическая функция, например

Мы можем легко перевести его на Python:

from math import sin
 def f(x):
     return (3*x**2 + x) * sin(x)

Можем ли мы написать программу для определения того, зависит ли результат f(x) от x, а не от g(x) = 7 что не так? Можем ли мы определить, содержит ли оно функцию тригонометрического синуса? Еще один вопрос, который мы могли бы задать, заключается в том, можно ли разложить выражение по правилам алгебры. В этом случае это может: f(x) эквивалентно может быть записано как 3x2 sin(x) + x sin(x). Если бы мы могли написать функцию для раскрытия алгебраического выражения, мы могли бы изобразить это так:

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

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

Начнем с того, что по-другому подумаем об алгебраических выражениях, рассматривая их как структурированные наборы символов, а не как процедурные вычисления.

Моделирование алгебраических выражений

Рассмотрим функцию f(x) = (3x2 + x) sin(x), и посмотрим, как мы можем разбить его на части. Как только мы концептуально выполним это упражнение, мы увидим, как перевести наши результаты в код Python.

Первое наблюдение состоит в том, что «f» — это произвольное имя для этой функции. Например, правая часть этого уравнения расширяется одинаково независимо от того, как вы ее называете. По этой причине мы сосредоточимся только на выражении, определяющем функцию, в данном случае это (3x2 + x ) sin(x). Выражение — это набор математических символов — чисел, букв, операций — объединенных определенным образом. Наша первая цель — смоделировать эти символы и способы их составления в Python.

Разбиение выражения на части

Мы можем начать моделировать алгебраические выражения, разбив их на более мелкие выражения. Есть только один осмысленный способ разбить выражение (3x2 + x) sin(x), это произведение 3 x2 + x и sin(x).

Напротив, мы не можем разделить это выражение по знаку плюс. Мы могли бы понять выражения по обе стороны от знака плюс, если бы попытались, но результат не эквивалентен исходному выражению.

Если мы посмотрим на выражение 3x2 + x, его можно разбить на сумму: 3x2 и х. Обычный порядок операций говорит нам, что 3x2 — это произведение 3 и x2, а не 3x, возведенное в степень 2.

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

  • 3x2 — это произведение выражений «3» и «x2».
  • x2 — это степень — одно выражение «x», возведенное в степень другого выражения «2».
  • Выражение sin(x) является приложением функции. Учитывая выражение «sin» и выражение «x», sin(x) — это новое выражение.

Переменная «x», число «2» или имя функции «sin» не могут быть далее разбиты. Чтобы отличить их от комбинаторов, они называются элементами. Урок здесь заключается в том, что хотя «(3x2 + x) sin(x)» — это всего лишь набор символов, напечатанных на этой странице. , символы комбинируются определенным образом для передачи математического значения.

Построение дерева выражений

Элементов «3», «x», «2» и «sin», а также комбинаторов сложения, умножения, возведения в степень и применения функции достаточно, чтобы перестроить все выражение (3x2 + x) sin(x). Давайте пройдемся по шагам и нарисуем структуру, которую мы в итоге построим.

Одна из первых конструкций, которые мы можем собрать, — это x2, которая объединяет x и 2 с комбинатором мощности.

Хорошим следующим шагом является объединение x2 с числом 3 с помощью комбинатора произведений, чтобы получить выражение 3x2:

Эта конструкция состоит из двух слоев: один из входов комбинатора произведений сам является комбинатором. По мере того, как мы добавляем больше терминов выражения, оно становится еще глубже. Следующим шагом является добавление элемента «x» к 3x2 с помощью комбинатора суммы, представляющего операцию сложения.

Наконец, нам нужно использовать комбинатор приложения функции, чтобы применить «sin» к «x», а затем комбинатор произведения, чтобы объединить sin(x) с тем, что мы построили.

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

Перевод дерева выражений в Python

Мы можем думать о каждом комбинаторе как о функции, которая принимает одно или несколько выражений в качестве входных данных и возвращает новое выражение в качестве выходных данных. На практике также полезно думать о комбинаторе как о именованном контейнере, который содержит его входные данные. Например, результатом применения степенного комбинатора к x и 2 должен быть некоторый объект, который содержит как x, так и 2 в качестве данных. Для выражения степени, такого как x2, x называется основанием, а 2 называется показателем степени. Класс Python, оборудованный для хранения базы и экспоненты, может выглядеть так:

class Power():
     def __init__(self,base,exponent):
         self.base = base
         self.exponent = exponent

Затем мы могли бы написать Power(“x”,2) на Python для представления выражения x2. Вместо использования необработанных строк и чисел я создам специальные классы для представления чисел и переменных.

class Number():
     def __init__(self,number):
         self.number = number
  
 class Variable():
     def __init__(self,symbol):
         self.symbol = symbol

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

Power(Variable("x"),Number(2))

Каждый из наших комбинаторов может быть реализован как класс с соответствующим именем, в котором хранятся данные любых выражений, которые он объединяет. Например, комбинатор произведений может быть классом, в котором хранятся два выражения, предназначенные для перемножения.

class Product():
     def __init__(self, exp1, exp2):
         self.exp1 = exp1
         self.exp2 = exp2

Произведение 3x2 можно выразить с помощью этого комбинатора следующим образом.

Product(Number(3),Power(Variable("x"),Number(2)))

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

class Sum():
     def __init__(self, *exps): #<1>
         self.exps = exps
  1. Мы допускаем сумму любого количества терминов, что означает, что мы можем добавить два или более выражений одновременно.
  2. Объект Function хранит строку, которая является его именем, например «sin».
  3. Комбинатор Apply хранит функцию и аргумент, к которому она применяется.
  4. Я использовал дополнительные пробелы, чтобы сделать структуру выражения более понятной.

Это точное представление исходного выражения: (3x2+ x) sin(x). Под этим я подразумеваю, что мы могли бы посмотреть на этот объект Python и увидеть, что он описывает алгебраическое выражение, а не другое. Для другого выражения, например

Apply(Function("cos"),Sum(Power(Variable("x"),Number("3")), Number(-5)))

Мы можем внимательно прочитать его и увидеть, что оно представляет собой другое выражение: cos(x3 + −5). В следующих упражнениях вы можете попрактиковаться в переводе некоторых алгебраических выражений на язык Python и наоборот. Вы увидите, что набирать все представление выражения может быть утомительно. Хорошая новость заключается в том, что после кодирования на Python ручная работа заканчивается. Следующее, что мы увидим, это то, как писать функции Python для автоматической работы с нашими выражениями.

Упражнения

Упражнение. Нарисуйте выражение ln(yz) в виде дерева, построенного из элементов и комбинаторов из этого раздела.

Решение. Самый внешний комбинатор — это «Применить». Применяемая функция — «ln», натуральный логарифм, и аргумент — yz. В свою очередь, yz — это степень с основанием y и показателем степени z. Результат выглядит следующим образом:

Упражнение. Переведите выражение из предыдущего упражнения в код Python. Напишите это и как функцию Python, и как структуру данных, построенную из элементов и комбинаторов.

Решение. Вы можете думать о ln(yz) как о функции двух переменных, y и з. Он напрямую транслируется в Python (где ln называется log):

from math import log
 def f(y,z):
     return log(y**z)

Дерево выражений строится следующим образом:

Apply(Function("ln"), Power(Variable("y"), Variable("z")))

Упражнение. Какое выражение представлено Product(Number(3),Sum(Variable(“y“), Variable(“z“) )) ?

Решение. Это представляет 3·(y + z). Обратите внимание, что скобки необходимы из-за порядка операций.

Упражнение. Реализуйте комбинатор «Quotient», представляющий одно выражение, разделенное на другое. Как вы представляете следующее выражение?

Решение. Частному комбинатору необходимо хранить два выражения: верхнее выражение называется числителем, а нижнее — знаменателем.

class Quotient(Expression):
     def __init__(self,numerator,denominator):
         self.numerator = numerator
         self.denominator = denominator

Пример выражения представляет собой частное суммы a +b с числом 2:

Quotient(Sum(Variable("a"),Variable("b")),Number(2))

Упражнение. Реализуйте комбинатор «Difference», представляющий одно выражение, вычитаемое из другого. Как можно представить выражение b2 – 4ac?

Решение. Комбинатору Difference необходимо хранить два выражения, и он представляет второе, вычитаемое из первого.

class Difference(Expression):
     def __init__(self,exp1,exp2):
         self.exp1 = exp1
         self.exp2 = exp2

Выражение b2–4ac представляет собой разность выражений b2 и 4ac и представляется следующее.

Difference(
     Power(Variable('b'),Number(2)),
     Product(Number(4),Product(Variable('a'), Variable('c'))))

Упражнение. Реализуйте комбинатор «Отрицательный», представляющий отрицание выражения. Например, отрицание x2 + y равно −(x2 + y). Представьте последнее выражение в коде, используя новый комбинатор.

Решение. Комбинатор Negative — это класс, который содержит одно выражение.

class Negative():
     def __init__(self,exp):

Чтобы инвертировать x2 + y, мы передаем его конструктору Negative.

Negative(Sum(Power(Variable("x"),Number(2)),Variable("y")))

Упражнение. Добавьте функцию sqrt, представляющую квадратный корень, и используйте ее для кодирования следующей формулы:

Решение. Чтобы не печатать, мы можем заранее назвать переменные и функцию извлечения квадратного корня.

A = Variable('a')
 B = Variable('b')
 C = Variable('c')
 Sqrt = Function('sqrt')

Затем нужно перевести алгебраическое выражение в соответствующую структуру элементов и комбинаторов. На самом высоком уровне вы можете видеть, что это частное суммы (сверху) и произведения (снизу).

Quotient(
     Sum(
         Negative(B),
         Apply(
             Sqrt,
             Difference(
                 Power(B,Number(2)),
                 Product(Number(4), Product(A,C))))),
     Product(Number(2), A))

Мини-проект: создайте абстрактный базовый класс с именем Expression и сделайте от него наследование всех элементов и комбинаторов. Например, class Variable() должно стать class Variable(Expression). Затем перегрузите арифметические операции Python +, -, * и / для создания объектов Expression. Например, код 2 * Variable("x") + 3 должен дать: Sum(Product(Number(2), Variable(“x”)), Number(3)).

Решение. См. исходный код.

Запуск символического выражения

Для функции, которую мы изучали до сих пор, мы написали функцию Python, которая ее вычисляет:

def f(x):
     return (3*x**2 + x)*sin(x)

Как сущность в Python, эта функция хороша только для одного: возвращать выходное значение для заданного входного значения x. Значение «f» в Python не позволяет особенно легко программно ответить на вопросы, которые мы задавали в начале статьи, например, зависит ли «f» от своих входных данных, содержит ли «f» тригонометрическую функцию или что тело «f» выглядело бы, если бы оно было расширено алгебраически. В этом разделе мы увидим, что, переведя выражение в структуру данных в Python, построенную из элементов и комбинаторов, мы можем ответить на все эти и другие вопросы.

Поиск всех переменных в выражении

Понятно, что выходное значение f(x) зависит от входного значения

, а для сравнения g(x) = 7 — нет. Некоторые случаи сложны, например h(x) = x − x, p(x) = 4x2 — (2x + 1)(2x − 1) или q(x) = sin(x)2 + cos(x)2, которые также не зависят от значение x (понимаете, почему?), но мы не будем на них сосредотачиваться. Давайте зададим более общий вопрос: для заданного выражения какие различные переменные появляются в нем? Мы можем написать функцию Python distinct_variables, которая принимает выражение (имеющее в виду любой из наших элементов или комбинаторов) и возвращает набор Python, содержащий переменные.

Если наше выражение представляет собой элемент, такой как z или 7, ответ ясен. Выражение, которое является только переменной, содержит одну отдельную переменную, но выражение, которое является только числом, вообще не содержит переменных. Мы ожидаем, что наша функция будет вести себя соответствующим образом:

>>> distinct_variables(Variable("z"))
 {'z'}

Ситуация усложняется, когда выражение состоит из нескольких комбинаторов, например y · z + x2. Человеку легко прочитать все переменные, такие как y, z и x, но как их извлечь из выражение в Python? Это комбинатор Sum, представляющий сумму y · z и xz. В первом выражении суммы есть y и z, а во втором — x и z. Сумма содержит все переменные в этих двух выражениях.

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

def distinct_variables(exp):
     if isinstance(exp, Variable):
         return set(exp.symbol)
     elif isinstance(exp, Number):
         return set()
     elif isinstance(exp, Sum):
         return set().union(*[distinct_variables(exp) for exp in exp.exps])
     elif isinstance(exp, Product):
         return distinct_variables(exp.exp1).union(distinct_variables(exp.exp2))
     elif isinstance(exp, Power):
         return distinct_variables(exp.base).union(distinct_variables(exp.exponent))
     elif isinstance(exp, Apply):
         return distinct_variables(exp.argument)
     else:
         raise TypeError("Not a valid expression.")

Как и ожидалось, наше выражение f_expression содержит только переменную x:

>>> distinct_variables(f_expression)
 {'x'}

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

Вычисление выражения

Теперь у нас есть два представления одной и той же математической функции f(x). Одной из них является функция Python f, которая удобна для оценки функции при заданном входном значении x. Новым является древовидная структура данных, описывающая структуру выражения, определяющего f(x). Оказывается, последнее представление имеет лучшее из обоих миров — мы можем использовать его и для вычисления f(x), только немного поработав.

Механически, вычисление функции f(x), скажем, при x = 5 означает подстановку значения 5 вместо x везде, а затем выполнение арифметика, чтобы найти результат. Если бы выражение было f(x) = x, подстановка x = 5 говорит нам, что f(5) = 5. Другой простой пример равно g(x) = 7, где подстановка 5 вместо x не дает никакого эффекта — x в правой части не появляется, а результат g(5) равен 7.

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

Исходные данные, которые нам нужны, — это значения, которые необходимо вставить, и переменные, которые они заменят. Например, выражение с двумя разными переменными, такими как z(x,y) = 2xy3, требует двух значений для получения результата, например x = 3 и y = 2. В терминологии информатики это называется привязка переменных. С их помощью мы можем оценить подвыражение y3 как (2)3, что равно 8. Другое подвыражение — 2x, которое оценивается как 2 · (3 ) = 6. Эти два числа объединяются с помощью комбинатора произведения, и значение всего выражения равно произведению 6 и 8, или 48.

Когда мы будем переводить эту процедуру в код Python, я покажу вам немного другой стиль, чем в предыдущем примере. Вместо отдельной функции «оценки» мы можем добавить метод оценки к каждому классу, представляющему выражение. Чтобы обеспечить это, мы можем создать абстрактный базовый класс Expression с абстрактным методом оценки и наследовать от него все виды выражений. Вот базовый класс Expression с методом оценки.

from abc import ABC, abstractmethod
  
 class Expression(ABC):
     @abstractmethod
     def evaluate(self, **bindings):
         pass

Поскольку выражение может содержать более одной переменной, я настроил его так, чтобы вы могли передавать привязки переменных в качестве аргументов ключевого слова. Например, привязки {x : 3, y : 2} означают «заменить 3 на x и 2 на y». Это дает нам хороший синтаксический сахар при вычислении выражения. Если z представляет выражение 2xy3, то, когда мы закончим, мы сможем выполнить следующее.

>>> z.evaluate(x=3,y=2)
 48

Пока у нас есть только абстрактный класс. Нам нужно сделать так, чтобы все наши классы выражений наследовались от Expression. Например, экземпляр Number — это выражение, так как число само по себе, например «7», является допустимым выражением. Независимо от предоставленных привязок переменных, число вычисляется само по себе.

class Number(Expression):
     def __init__(self,number):
         self.number = number
     def evaluate(self, **bindings):
         return self.number

Например, оценка Number(7).evaluate(x=3,y=6,q=-15) или любая другая оценка в этом отношении возвращает основное число: семь.

Обработка переменных также проста. Если мы смотрим на выражение Variable («x»), нам нужно только свериться с привязками и посмотреть, какое число установлено для переменной «x». Когда мы закончим, мы сможем запустить Variable("x").evaluate(x=5) и получить в результате 5. Если мы не можем найти привязку для «x», значит, мы не можем завершить оценку и нам нужно вызвать исключение. Вот обновленное определение класса Variable:

class Variable(Expression):
     def __init__(self,symbol):
         self.symbol = symbol
     def evaluate(self, **bindings):
         try:
             return bindings[self.symbol]
         except:
             raise KeyError("Variable '{}' is not bound.".format(self.symbol))

Разобравшись с элементами, нам нужно обратить внимание на комбинаторы. (Обратите внимание, что мы не будем рассматривать объект Function как выражение сам по себе, потому что такая функция, как «sin», не является самостоятельным выражением. Она может быть оценена только тогда, когда ей задан аргумент в контексте комбинатора Apply. ) Для комбинатора, такого как Product, правило, используемое для его вычисления, простое: вычислить оба выражения, содержащиеся в произведении, а затем перемножить результаты. В продукте не нужно выполнять подстановку, но мы передадим привязки к обоим подвыражениям, если одно из них содержит переменную.

class Product(Expression):
     def __init__(self, exp1, exp2):
         self.exp1 = exp1
         self.exp2 = exp2
     def evaluate(self, **bindings):
         return self.exp1.evaluate(**bindings) * self.exp2.evaluate(**bindings)

С этими тремя классами, дополненными методами оценки, мы теперь можем вычислять любое выражение, построенное из переменных, чисел и произведений. Например:

>>> Product(Variable("x"), Variable("y")).evaluate(x=2,y=5)
 10

Точно так же мы можем добавить метод «вычислить» к комбинаторам Sum, Power, Difference или Quotient. Как только мы оцениваем их подвыражения, имя комбинатора сообщает нам, какую операцию мы используем для получения общего результата. Комбинатор «Применить» работает несколько иначе и заслуживает особого внимания. Нам нужно динамически просмотреть имя функции, такое как «sin» или «sqrt», и придумать рецепт для вычисления ее значения. Это можно сделать несколькими способами, но я решил хранить словарь известных функций в качестве данных в классе Apply. В качестве первого прохода мы можем сообщить нашему оценщику о трех именованных функциях:

_function_bindings = {
     "sin": math.sin,
     "cos": math.cos,
     "ln": math.log
 }
  
 class Apply(Expression):
     def __init__(self,function,argument):
         self.function = function
         self.argument = argument
     def evaluate(self, **bindings):
         return _function_bindings[self.function.name](self.argument.evaluate(**bindings))

Вы можете попрактиковаться в написании остальных методов «оценки» самостоятельно или найти их в исходном коде. Как только вы полностью реализовали их все, вы сможете оценить наше f_expression.

>>>  f_expression.evaluate(x=5)
 -76.71394197305108

Результат здесь не важен, важно лишь то, что он такой же, как и у обычной функции Python f(x).

>>> f(x)
 -76.71394197305108

Оснащенные функцией оценки, наши объекты Expression могут выполнять ту же работу, что и соответствующие им обычные функции Python.

Расширение выражения

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

Соответствующим правилом алгебры является дистрибутивность свойство сумм и произведений. Это правило гласит, что произведение вида (a + b)c равно ac + bc и, аналогично, что x( y + z) = xy + xz. Например, наше выражение (3x2 + x)sin(x) равно 3x2sin (x) + xsin(x), что называется развернутой формой первого произведения. Вы можете использовать это правило несколько раз, чтобы раскрыть более сложные выражения, например:

Как видите, расширение короткого выражения, такого как (x + y)3, может занять много времени. В дополнение к расширению этого выражения я также немного упростил результат, переписав некоторые продукты, которые выглядят как xyx или xxy как x2y, например. Затем я еще больше упростил, объединив похожие термины, отметив, что есть три суммированных копии каждого из x2y и y2x и сгруппировать их вместе в 3x2y и 3y2x. В этом примере я буду смотреть только на то, как сделать расширение — вы можете реализовать упрощение в качестве упражнения.

Мы можем начать с добавления абстрактного метода expand в базовый класс Expression.

class Expression(ABC):
     ...
     @abstractmethod
     def expand(self):
         pass

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

class Number(Expression):
     ...
     def expand(self):
         return self

Суммы уже считаются расширенными выражениями, но отдельные члены суммы не могут быть расширены. Например, 5 + a(x + y) — это сумма, в которой первый член, 5, полностью расширен, а второй член, a(x + y), не 'т. Чтобы расширить сумму, нам нужно расширить каждое из условий и просуммировать их.

class Sum(Expression):
     ...
     def expand(self):
         return Sum(*[exp.expand() for exp in self.exps])

Та же процедура работает для применения функции. Мы не можем расширить сам Apply, но мы можем расширить аргументы функции. Это расширяет выражение вроде sin(x(y + z)) до sin(xy + xz).

class Apply(Expression):
     ...
     def expand(self):
         return Apply(self.function, self.argument.expand())

Настоящая работа начинается, когда мы расширяем продукты или возможности, когда структура выражения полностью меняется. Например, a(b + c) — это произведение переменной на сумму двух переменных, а его расширенная форма — ab + ac, сумма двух переменных. произведения двух переменных каждый. Чтобы реализовать распределительный закон, мы должны рассмотреть три случая: первый член произведения может быть суммой, второй член может быть суммой или ни один из них не может быть суммой. В последнем случае расширение не требуется.

class Product(Expression):
     ...
     def expand(self):
         expanded1 = self.exp1.expand() #<1>
         expanded2 = self.exp2.expand()
         if isinstance(expanded1, Sum): #<2>
             return Sum(*[Product(e,expanded2).expand() for e in expanded1.exps])
         elif isinstance(expanded2, Sum): #<3>
             return Sum(*[Product(expanded1,e) for e in expanded2.exps])
         else:
             return Product(expanded1,expanded2) #<4>
  1. Во-первых, разверните оба термина продукта
  2. Если первый член произведения равен Sum, возьмите произведение, умножив каждый из его членов на второй член произведения. Вызовите расширение результата, если второй член произведения также равен Sum.
  3. Если второй член произведения равен Sum, умножьте каждый из его членов на первый член произведения.
  4. В противном случае ни один из терминов не является Sum, и распределительный закон не нужно вызывать.

После реализации всех этих методов мы можем протестировать функцию расширения. При соответствующей реализации __repr__ (см. упражнения) вы можете увидеть его в действии на интерактивном сеансе. Он правильно расширяет (a + b)(x + y) до ax + ay + bx + by :

>>> Product(Sum(A,B),Sum(Y,Z)).expand()
 Product(Sum(Variable("a"),Variable("b")),Sum(Variable("x"),Variable("y")))

И наше выражение (3x2 + x)sin(x) правильно расширяется до 3x2sin(x) + xsin(x):

>>> f_expression
 Sum(Product(Product(3,Power(Variable("x"),2)),Apply(Function("sin"),Variable("x"))),Product(Variable("x"),Apply(Function("sin"),Variable("x"))))

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

Если вы хотите узнать больше о книге, ознакомьтесь с ней в нашей браузерной читалке liveBook здесь.

Эта статья изначально была размещена здесь: https://freecontent.manning.com/working-with-symbolic-expressions/