Привет! Сегодня миниатюрный туториал о том, как сделать разбор строки с математическим выражением и вычислить его используя нечеткие треугольные числа. При соответствующих изменениях кода туториал сгодится для работы и с другими «кастомными» переменными.
Требования:
- Язык программирования python 3.x (приводимый в статье код проверялся на python 3.5)
- библиотека sympy, можно установить через терминал (консоль):
pip install sympy
Порядок решения задачи:
- Подключаем библиотеки
from fractions import Fraction import re from typing import Iterable from random import random import sympy
Подключение fractions необязательно, будем использовать Fraction для хранения вещественных чисел в виде дроби (с целью минимальной потери точности). Библиотеку re будем использовать для парсинга строки и автоматического формирования списка символьных переменных. Использование библиотеки typing необязательно, используем ее для явного указания типов параметров функций. Библиотека random будет использоваться для формирования тестовых значений нечетких переменных. sympy — отличная библиотека для символьных вычислений в Python, с ее помощью мы будем работать с самой строкой-выражением.
- Опишем класс нечетких треугольных чисел и операций над ними. В данном примере достаточно трех операций (сложение, вычитание и деление). Вводить операции будем с помощью перегрузки «магических» методов соответствующего класса:
class FuzzyTriangular(object): """Описание класса FuzzyTriangular""" def __init__(self, floatdigit = None, ABC = None, CAB = None, CDD = None): super(FuzzyTriangular, self).__init__() if ABC or floatdigit: if isinstance(floatdigit, (int, float)): self._a = Fraction(floatdigit) #левый "0" self._b = Fraction(floatdigit) #центр ("1") self._c = Fraction(floatdigit) #правый "0" elif isinstance(floatdigit, (tuple,list)): self._a = Fraction(floatdigit[0]) #левый "0" self._b = Fraction(floatdigit[1]) #центр ("1") self._c = Fraction(floatdigit[2]) #правый "0" else: self._a = Fraction(ABC[0]) #левый "0" self._b = Fraction(ABC[1]) #центр ("1") self._c = Fraction(ABC[2]) #правый "0" self._center = self._b #центр self._alpha = self._b - self._a #отклонение от центра влево self._beta = self._c - self._b #отклонение от центра вправо self._d = (self._alpha + self._beta)/2 self._delta = (self._beta - self._alpha)/2 elif CAB: self._center = Fraction(CAB[0]) #центр self._alpha = Fraction(CAB[1]) #отклонение от центра влево self._beta = Fraction(CAB[2]) #отклонение от центра вправо self._d = (self._alpha + self._beta)/2 self._delta = (self._beta - self._alpha)/2 self._b = self._center #центр ("1") self._a = self._center - self._alpha #левый "0" self._c = self._center + self._beta #правый "0" elif CDD: self._center = Fraction(CDD[0]) #центр self._d = Fraction(CDD[1]) self._delta = Fraction(CDD[2]) self._alpha = self._d - self._delta #отклонение от центра влево self._beta = self._d + self._delta #отклонение от центра вправо self._b = self._center #центр ("1") self._a = self._center - self._alpha #левый "0" self._c = self._center + self._beta #правый "0" else: raise Exception("No input data to create class") def __repr__(self): return str((round(float(self._a), 12), round(float(self._b), 12), round(float(self._c), 12))) def __CDD_add(self, other): center = self._center + other._center d = self._d + other._d delta = self._delta + other._delta return FuzzyTriangular(CDD = (center, d, delta)) def __CDD_sub(self, other): center = self._center - other._center d = self._d + other._d delta = self._delta - other._delta return FuzzyTriangular(CDD = (center, d, delta)) def __CDD_mul(self, other): center = self._center*other._center d = abs(self._center)*other._d + abs(other._center)*self._d delta = self._center*other._delta + other._center*self._delta return FuzzyTriangular(CDD = (center, d, delta)) def __add__(self, other): if isinstance(other, FuzzyTriangular): return self.__CDD_add(other) else: return self.__CDD_add(FuzzyTriangular(other)) def __sub__(self, other): if isinstance(other, FuzzyTriangular): return self.__CDD_sub(other) else: return self.__CDD_sub(FuzzyTriangular(other)) def __mul__(self,other): if isinstance(other, FuzzyTriangular): return self.__CDD_mul(other) else: return self.__CDD_mul(FuzzyTriangular(other)) def __pos__(self): return FuzzyTriangular(1)*self def __neg__(self): return FuzzyTriangular(-1)*self def __eq__(self, other): return (self._a == other._a) and (self._b == other._b) and (self._c == other._c)
Формы представления нечетких треугольных чисел могут быть разные, не будем углубляться. В представленном коде обратим внимание на методы __add__ (оператор сложения), __sub__ (оператор вычитания), __mul__ (оператор умножения). Если попытаться к нечеткому треугольному числу прибавить вещественное число, то оно будет преобразовано в нечеткое треугольное. Аналогичная ситуация с кортежем или списком из вещественных чисел — первые три числа будут восприниматься как нечеткое треугольное (и также преобразовываться в класс FuzzyTriangular). Метод __pos__ переопределяет унарный оператор "+". Метод __neg__ — унарный "-". Метод __eq__ переопределяет оператор "==". При желании можно дополнительно переопределить такие операции как:
- деление
- возведение в степень
- модуль числа
- сравнения (больше/меньше, больше либо равно/меньше либо равно)
- скаляризация (приведение к int, float, complex числам, округление)
- инверсия и др...
Проверить адекватность введенных операций можно небольшим набором тестов, например таких:
ZERO = FuzzyTriangular((0,0,0)) ONE = FuzzyTriangular((1,1,1)) A = FuzzyTriangular((0.3,0.5,0.9)) B = FuzzyTriangular((0.2,0.4,0.67)) C = FuzzyTriangular((0,0.33,0.72)) print('ZERO = '+str(ZERO)) print('ONE = '+str(ONE)) print('A = '+str(A)) print('B = '+str(B)) print('C = '+str(C)) #some tests print('nСЛОЖЕНИЕ') print('A + B = ', A + B) print('A + B == B + A', A + B == B + A) #введение оператора сравнения print('A + C = ', A + C) print('A + C == C + A', A + C == C + A) print('B + C = ', B + C) print('B + C == C + B', B + C == C + B) print('A + B + C = ', A + B + C) print('(A + B) + C == A + (B + C) == (A + C) + B', (A + B) + C == A + (B + C) == (A + C) + B) print('C + 1 = ', C + 1) print('1 + C = ', ONE + C) print('nВЫЧИТАНИЕ') print('A - A =', A - A) print('A - A == 0', A - A == ZERO) print('A - B = ', A - B) print('B - A = ', B - A) #введение унарных операторов "-" и "+" print('A - B == -(B - A)', A - B == -(B - A)) print('(A + B + C) - (A + B) = ', (A + B + C) - (A + B)) #необходимость использования рациональных дробей print('(A + B + C) - (A + B) == C', (A + B + C) - (A + B) == C) print('1 - A = ', ONE - A) print('A - 1 = ', A - 1) print('1 - A == -(A - 1)', ONE - A == -(A - 1)) print('nУМНОЖЕНИЕ') print('A*B == B*A', A*B == B*A) print('-1*C =', -ONE*C) print('-1*C == -C', -ONE*C == -C) print('-1*C == C*-1', -ONE*C == C*-1) print('C*-1 = ', C*-1) print('C*-1 =', C*-1) print('-C*1 == -C', -C*1 == -C) print('-C*1 =', -C*1) print('-C =', -C) print('C*-1 == -C', C*-1 == -C) print('(A + B)*C == A*C + B*C', (A + B)*C == A*C + B*C) print('(A - B)*C == A*C - B*C', (A - B)*C == A*C - B*C) print('A*C = ', A*C) print('B*C = ', B*C) print('-B*C = ', -B*C) print('-B*C == B*-C', -B*C == B*-C) print('B*C == -B*-C', B*C == -B*-C)
Эти проверочные операции сложения, деления и умножения задаются в коде и выполняются согласно переопределению «магических» методов. Нам бы хотелось иметь возможность осуществлять такие же операции с использованием символьных переменных в заранее неизвестных выражениях. Для этого требуется ввести несколько вспомогательных функций.
- Вводим вспомогательные функции:
-
def symbols_from_expr(expr_str: str, pattern=r"[A-Za-z]d{,2}") -> tuple: """Возвращает все найденные символические переменные по заданному шаблону""" symbols_set = set(re.findall(pattern, expr_str)) symbols_set = sorted(symbols_set) symbols_list = tuple(sympy.symbols(symbols_set)) return symbols_list
Эту функцию будем использовать для поиска символьных переменных в строке-выражении (шаблон по умолчанию — символ от A до Z или от a до z и целое число после него длиной до 2х знаков (или отсутствие числа).
-
def expr_subs(expr_str: str, symbols: Iterable, values: Iterable): """Возвращает результат подстановки значений values вместо символов symbols в выражение-строку expr_str""" expr = sympy.sympify(expr_str) func = sympy.lambdify(tuple(symbols), expr, 'sympy') return func(*values)
Эта функция позволяет вычислить значение выражения-строки с подстановкой вместо символьных переменных переменных любого допустимого типа (если для него переопределены операции, содержащиеся в самом выражении-строке). Это возможно благодаря функции sympy.lambdify, которая преобразует выражение sympy в лямбда-функцию, воспринимающую «магические» методы. Важным условием для адекватной работы функции является правильный порядок элементов в symbols и values (соответствие символов и подставляемых значений).
- Каждый раз создавать лямбда-функцию дорогое удовольствие. Если требуется множественное использование одного и того же выражения, то рекомендуется воспользоваться следующими двумя функциями:
def lambda_func(expr_str: str, symbols: Iterable) -> callable: """Возвращает лямбда-функцию, полученную путем преобразования выражения-строки expr_str с символами symbols""" expr = sympy.sympify(expr_str) func = sympy.lambdify(tuple(symbols), expr, 'sympy') return func def func_subs(expr_func: callable, values: Iterable): """Возвращает результат вызова лямбда-функции expr_func с параметрами values""" return expr_func(*values)
Первая возвращает самую лямбда-функцию, а вторая — позволяет вычислять результирующие значения с помощью подстановки списка значений. В очередной раз акцентируется внимание на то, что используемые значения вовсе не обязаны быть треугольными нечеткими числами.
-
- Читаем строку-формулу из файла
with open('expr.txt', 'r') as file: expr_str = file.read() print('expr_str', expr_str)
В качестве формулы-строки для файла expr.txt можно использовать что-то вроде этого:
p36*q67*p57*p26*p25*p13*q12*q15 + + p36*q67*p47*p26*p24*p13*q12 + + p67*q57*p26*p25*q12*p15 + + q57*p47*p25*p24*q12*p15 + + p57*p25*p12*q15 + + p36*p67*p13 + + p67*p26*p12 + + p47*p24*p12 + + p57*p15 - - p57*p47*p24*p12*p15 - - p67*p47*p26*p24*p12 - - p67*p57*p26*p12*p15 + + p67*p57*p47*p26*p24*p12*p15 - - p36*p67*p26*p13*p12 - - p36*p67*p47*p24*p13*p12 - - p36*p67*p57*p13*p15 + + p36*p67*p57*p47*p24*p13*p12*p15 + + p36*p67*p47*p26*p24*p13*p12 + + p36*p67*p57*p26*p13*p12*p15 - - p36*p67*p57*p47*p26*p24*p13*p12*p15 - - p36*p67*p57*p25*p13*p12*q15 - - p67*p57*p26*p25*p12*q15 - - p57*p47*p25*p24*p12*q15 + + p67*p57*p47*p26*p25*p24*p12*q15 + + p36*p67*p57*p26*p25*p13*p12*q15 + + p36*p67*p57*p47*p25*p24*p13*p12*q15 - - p36*p67*p57*p47*p26*p25*p24*p13*p12*q15 - - p36*p67*q57*p47*q26*p25*p24*p13*q12*p15 - - p67*q57*p47*p26*p25*p24*q12*p15 - - p36*p67*q57*p26*p25*p13*q12*p15 - - p36*q67*q57*p47*p26*p25*p24*p13*q12*p15 - - p36*q67*p57*p47*p26*p24*p13*q12*p15 - - p36*q67*p57*p47*p26*p25*p24*p13*q12*q15
- Вытаскиваем символьные переменные из выражения-строки:
symbols = symbols_from_expr(expr_str) print('AutoSymbols', symbols)
- Генерируем тестовые случайные треугольные числа:
values = tuple([FuzzyTriangular(sorted([random(),random(),random()])) for i in range(len(symbols))])
Сортировка случайных значений требуется для соответствия очередности значений левого «0», центра и правого «0».
- Преобразуем строку-формулу в выражение:
func = lambda_func(expr_str, symbols) print('func', '=', func)
- Вычисляем значение формулы с помощью лямбда-функции (используем func_subs и expr_subs для того, чтобы удостовериться в совпадении результатов):
print('func_subs', '=', func_subs(func, values)) print('expr_subs', '=', expr_subs(expr_str, symbols, values))
Пример вывода:
expr_str p36*q67*p57*p26*p25*p13*q12*q15 +
+ p36*q67*p47*p26*p24*p13*q12 +
+ p67*q57*p26*p25*q12*p15 +
+ q57*p47*p25*p24*q12*p15 +
+ p57*p25*p12*q15 +
+ p36*p67*p13 +
+ p67*p26*p12 +
+ p47*p24*p12 +
+ p57*p15 -
- p57*p47*p24*p12*p15 -
- p67*p47*p26*p24*p12 -
- p67*p57*p26*p12*p15 +
+ p67*p57*p47*p26*p24*p12*p15 -
- p36*p67*p26*p13*p12 -
- p36*p67*p47*p24*p13*p12 -
- p36*p67*p57*p13*p15 +
+ p36*p67*p57*p47*p24*p13*p12*p15 +
+ p36*p67*p47*p26*p24*p13*p12 +
+ p36*p67*p57*p26*p13*p12*p15 -
- p36*p67*p57*p47*p26*p24*p13*p12*p15 -
- p36*p67*p57*p25*p13*p12*q15 -
- p67*p57*p26*p25*p12*q15 -
- p57*p47*p25*p24*p12*q15 +
+ p67*p57*p47*p26*p25*p24*p12*q15 +
+ p36*p67*p57*p26*p25*p13*p12*q15 +
+ p36*p67*p57*p47*p25*p24*p13*p12*q15 -
- p36*p67*p57*p47*p26*p25*p24*p13*p12*q15 -
- p36*p67*q57*p47*q26*p25*p24*p13*q12*p15 -
- p67*q57*p47*p26*p25*p24*q12*p15 -
- p36*p67*q57*p26*p25*p13*q12*p15 -
- p36*q67*q57*p47*p26*p25*p24*p13*q12*p15 -
- p36*q67*p57*p47*p26*p24*p13*q12*p15 -
- p36*q67*p57*p47*p26*p25*p24*p13*q12*q15
AutoSymbols (p12, p13, p15, p24, p25, p26, p36, p47, p57, p67, q12, q15, q26, q57, q67)
func = <function <lambda> at 0x06129C00>
func_subs = (-0.391482058715, 0.812813114469, 2.409570627378)
expr_subs = (-0.391482058715, 0.812813114469, 2.409570627378)
[Finished in 1.5s]
Туториал окончен. Надеюсь, что вы нашли здесь для себя что-то полезное!
P.S. основная «фишка» описанного подхода — это возможность выйти за рамки стандартных для python и sympy типов переменных и операций над ними. Объявив свой класс и перегрузив «магические» методы вы можете вычислять заранее неизвестные математические выражения с помощью sympy (создавая лямбда-функции, воспринимающие как стандартные так и пользовательские типы и операции). Спасибо за внимание!
Автор: Palich239