Калькулятор выражений в Python


Этот калькулятор принимает четыре операторы: +, -, *, /, и работает со скобками. Отзывы о том, как я мог бы сделать его легче читать и более эффективной будет высоко ценится.

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

from sys import exit


#Whenever is_number(x) exists, it is checking to see if x is a number, obviously.
def is_number(item):
    try:
        float(item)
        return True
    except ValueError:
        return False


def set_up_list():
    #First part gets string and deletes whitespace
    astring = raw_input("Calculation: ")
    astring = astring.replace(" ", "")
    #Next it will check if there are any unsupported characters in the string
    for item in astring:
        if item not in ["0", "1", "2", "3" , "4", "5", "6", "7", "8", "9", "+", "-", "*", "/", ".", "(", ")"]:
            print ("Unsupported Character: " + item)
            exit()
    #Then it creates the list and adds each individual character to the list
    a_list = []
    for item in astring:
        a_list.append(item)
    count = 0
    #Finally it combines individual numbers into actual numbers based on user input
    while count < len(a_list) - 1:
        if is_number(a_list[count]) and a_list[count + 1] == "(":
            print ("Program does not accept parentheses directly after number, must have operator in between.")
            exit()
        if is_number(a_list[count]) and is_number(a_list[count + 1]):
            a_list[count] += a_list[count + 1]
            del a_list[count + 1]
        elif is_number(a_list[count]) and a_list[count + 1] == ".":
            try:
                x = a_list[count+2]
            except IndexError:
                print ("Your formatting is off somehow.")
                exit()
            if is_number(a_list[count + 2]):
                a_list[count] += a_list[count + 1] + a_list[count + 2]
                del a_list[count + 2]
                del a_list[count + 1]
        else:
            count += 1
    return a_list


def perform_operation(n1, operand, n2):
    if operand == "+":
        return str(float(n1) + float(n2))
    elif operand == "-":
        return str(float(n1) - float(n2))
    elif operand == "*":
        return str(float(n1) * float(n2))
    elif operand == "/":
        try:
            n = str(float(n1) / float(n2))
            return n
        except ZeroDivisionError:
            print ("You tried to divide by 0.")
            print ("Just for that I am going to terminate myself")
            exit()


expression = set_up_list()
emergency_count = 0
#Makes code shorter, short for parentheses
P = ["(", ")"]
#If the length of the list is 1, there is only 1 number, meaning an answer has been reached.
while len(expression) != 1:
    #If there are parentheses around a single list item, the list item is obviously just a number, eliminate parentheses
    #Will check to see if the first parentheses exists first so that it does not throw an index error
    count = 0
    while count < len(expression) - 1:
        if expression[count] == "(":
            if expression[count + 2] == ")":
                del expression[count + 2]
                del expression[count]
        count += 1
    #After that is done, it will multiply and divide what it can
    count = 0
    while count < len(expression) - 1:
        if expression[count] in ["*", "/"] and not (expression[count+1] in P or expression[count-1] in P):
            expression[count - 1] = perform_operation(expression[count - 1], expression[count], expression[count + 1])
            del expression[count + 1]
            del expression[count]
        count += 1
    #Then it will add and subtact what it can
    count = 0
    while count < len(expression) - 1:
        if expression[count] in ["+", "-"] and not (expression[count+1] in P or expression[count-1] in P):
            expression[count - 1] = perform_operation(expression[count - 1], expression[count], expression[count + 1])
            del expression[count + 1]
            del expression[count]
        count += 1
    #The steps will repeat until only one character is left. Operations that fail will be stopped by emergency count.
    emergency_count += 1
    if emergency_count >= 1000:
        print ("Operation was too long or was bugged")
        exit()

print (float(expression[0]))


Комментарии
2 ответа

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


  • Смотреть в PEP8. Вы на самом деле довольно близко к нему, но это стандартное форматирование, которое наиболее pythoners использовать. Есть замечательные инструменты, как flake8, которая позволит применить стиль для вас.


    • Комментарии нужен один пробел после #

    • Есть некоторые пробелы между слов не хватает

    • Без пробела после print!

    • Оберните линии до 79 столбцы


  • Комментарий перед is_number должно быть """Doc comment."""

  • set_up_list не хорошее название. Мне пришлось прочитать код и увидеть, что она используется в контексте, чтобы понять, что он сделал. read_expression могло бы быть лучше.

  • У вас есть много использования while С какой-то счетчик (т. е. while count < len(expression) - 1:). В Python, мы почти исключительно это как заклинание for e in expression:. Если вам нужен индекс, то for i, e in enumerate(expression).

  • Мы обычно используем 'single quoted strings'. "Double quotes" обычно зарезервированы для форматирования (исх. "1 + 1 is {}".format(1 + 1)).

  • Использование in приятно. Но, вы знаете, вместо этого большой длинный список, вы можете просто сделать item not in '01234567890+-*/.()'? Струны ведут себя как списки таким образом.

  • Сделать то же самое для P = ['(', ')']. P также непонятно название. Не называйте то, что просто сделать строки короче. Если линии длинные, Вы, наверное, пытаетесь сделать слишком много в каждой строке. Но, x in '()' почти как короткие и затрагивает намного лучше.

  • Нет необходимости для вашего creates the list and adds each individual character to the list (нет необходимости a_listпросто используйте astring и дать ему лучшее имя!). Строки представляют собой последовательности, так что вы можете сделать len(astring), astring[23]и т. д.

  • У вас есть print()S и exit()в этот код. Хотя это нормально, я бы рекомендовал вытащить их оттуда. Сейчас это трудно проверить свой код, потому что он имеет побочные эффекты (печати и выхода). Для калькулятор, вы наверняка хотите, чтобы убедиться, что он правильно работает! Простой способ сделать ваш калькулятор тестирования является оптимизация его функции, как calculate(), которая принимает строку и возвращает число с плавающей точкой (результатом). Он может выбрасывать исключения (а возможно, вы хотите создать свой собственный путем расширения Exception для таких вещей, как UnclosedParenError или UnexpectedCharacterError) в условиях ошибки. Это означает, что вы можете проверить это вот так:

from unittest import main, TestCase
from yourcalculator import calculate

class TestCalculator(TestCase):
def test_basic_sums(self):
self.assertEqual(42, calculate('40 + 2'))

if __name__ == '__main__':
main()


  • Копилку отступать, что последний пункт, вы все еще хотите какой-то РЕПЛ интерфейс, если вы рефакторинг вещи в calculate функция. Вы должны использовать main() функция для этого и использовать стандартные if __name__ == '__main__': main():

def main():
try:
while True:
try:
print(calculate(input('Enter an expression: ')))
except CalculatorError as e:
print(e)
except KeyboardInterrupt:
break

if __name__ == '__main__':
main()


  • Вы, вероятно, хотите лучше ошибки, чем Your formatting is off somehow

  • В perform_operationвы используете float() (предположительно на цифры, которые ты уже назвал is_number о). В Python, мы предпочитаем "сделать в первую очередь, извиниться позже" подход (т. е. звоните поплавок и если он выдает ошибку, то мы будем вынуждены остановить выполнение и обработать его). Мы, как правило, не занимаются if foo_wont_raise(bar): foo(bar)

  • Я вижу много del. Для этого приложения, я рекомендовал бы избежать этого (и мутируют, что список в целом). Изменения списка, что делает его гораздо труднее для кого-то пытается следовать вашим кодом, потому что тогда в их сознании они должны отслеживать, что вы сделали в списке (а они не могут просто перейти к точке в коде, чтобы увидеть, что происходит, они должны проследить весь путь до того момента, чтобы увидеть, если какие-либо мутации были проведены по списку)

  • Там действительно нет необходимости emergency_count. Ваш код не рекурсивный, так что вы не попали в лимит стека (и если вы не исключение)

  • Я в последнее время стало мнение о том, что вы должны вводить ничего нетривиального в Python. Python-это конечно известен как нетипизированный, но mypy действительно шло много, и вполне достаточно для многих случаев использования. Вы поймете, почему я думаю, что вы хотите печатать (это заставит сделать ваш калькулятор правильный намного проще). Читал об этом: https://mypy.readthedocs.io/en/latest/getting_started.html

С конкретными замечаниями о коде сделано. Давайте подумаем вообще о калькуляторах и говорить о полезных шаблонов для построения их (и компиляторы для языков программирования, в какой-то степени). Конечный продукт здесь, вероятно, будет намного больше, чем кода, который вы написали, но это будет намного легче рассуждать и продлить! В результате его длительного и мне становится голоден, я опускаю многие неинтересной ее части (такие, как исключения, которые я просто предполагаю, что существует).

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

По сути, мы имеем две разные проблемы здесь:


  1. Мы хотим быть в состоянии обрабатывать множество произвольный интервал (т. е. 1+1 такой же, как и 1 + 1 такой же, как и 1 + 1) и убедиться в отсутствии недопустимых символов (или плохие цифры 123.456.789)

  2. Как только у нас есть допустимые символы удалены и лишние пробелы, мы хотим убедиться, что выражение является правильно сформированным (скобки сбалансированы и у каждого оператора есть что-то, определяющее количество до после него) и потом уже оценивать

В компилятор дизайн этих двух называется сканером и парсером, соответственно (я использую парсер слабо, поскольку в настоящий компилятор синтаксический анализатор производит АСТ, но в случае с калькулятором, это эргономичнее всего делать математику в парсере). Их работы являются:


  • Сканер: убрать лишний интервал и убедитесь, что все символы являются допустимыми c in '01234567890+-*/.()' и от каждого действительного характера производить "маркер" (исх. в выражении 2 * 20 + 2 маркеры 2, '*', 20, '+', 2)

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

Начнем сканер. Он имеет три основные вещи он должен делать:


  1. Игнорировать пробелы (хотя это имеет свои пределы, мы, вероятно, не хотите 123 456 чтобы стать 123456- вместо этого мы, вероятно, хотите, чтобы ошибки, которые мы не можем иметь два последовательных числа, мы оставим этот синтаксический анализатор так что за сканер просто излучают два маркера для этого 123 и 456)

  2. Превращать символы в одном жетоны (экс c in '+-/*()')

  3. Проанализировать числа (может быть несколько символов, может быть плохо отформатированы в 123.456.789)

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

Сканер может выглядеть примерно так:

class Scanner(BaseParser):
"""Scanner scans an input string for calculator tokens and yields them.

>>> list(Scanner('11 * (2 + 3)').scan())
[11, '(', 2, '+', 3, ')']
"""

def scan(self):
"""Yields all tokens in the input."""
while not self._done():
# Ignore any whitespace that may be next
self._consume_whitespace()

# Emit any symbol tokens that may be next
yield from self._take(lambda char: char in '+-*/()')

# Emit any number token that may be next
yield from self._take_number()

def _consume_whitespace(self):
"""_take()s whitespace characters, but does not yield them."""
# Note we need the list here to force evaluation of the generator
list(self._take(lambda char: char.isspace()))

def _take_number(self):
"""Yields a single number if there is one next in the input."""

# Gather up the digits/. forming the next number in the input
number = ''.join(self._take(lambda c: c.isdigit() or c == '.'))

# If number is empty, we didn't scan digits, so don't try to float it
if number:
try:
yield float(number)
except ValueError:
raise BadNumberError(number)

Не слишком догнал в некоторых более продвинутых моделей на языке Python я использовал, основная идея заключается в (единственный открытый метод) scan(). Это генератор. Пока у нас еще есть вещи во входной строке, это именно то, что мы обсуждали: (1) игнорирует пробельные символы (2) отдача (доходность в генератор языком, который свободно означает, что он может вернуть многих) маркеры для математических операторов, а затем (3) дает число, если он обнаруживает один.

Если вам интересно, раньше, когда я сказал, что Python-это не очень хорошо подходит из коробки для этого, эти проблемы проявляются в приведенном выше коде:


  • Необходимость self._done() (мы бы оптимально, как одна из функций, которые мы используем, чтобы справиться с этим для нас, но, к сожалению, их StopIterationы потребляется, когда мы yield from них)

  • Тот факт, что этот зацикливания на неожиданные персонажи. Чтобы справиться с этим где-то в этом цикле нужно ввести _check_for_invalid_chars() что делает что-то вроде self._take(lambda c: c not in '01234567890.+-/*()') и потом, если это не пустой он вызывает исключение о неожиданный характер. Я оставлю это в качестве упражнения для вас.

Теперь, надеюсь, вы смогли выяснить, что _take() делает (это предусмотрено BaseParser). Если нет, то резюме является то, что она дает символы из входного покуда они удовлетворяют предикату (это лямбда, который передается). Ради полноты картины, здесь представлены служебные классы, необходимые, чтобы сделать эту работу (я их разделил, потому что наш парсер будет использовать их тоже!):

class BaseParser:
"""A base class containing utilities useful for a Parser."""

def __init__(self, items):
self._items = PeekableIterator(items)

def _take(self, predicate):
"""
Yields a contiguous group of items from the items being parsed for
which the predicate returns True.

>>> p = BaseParser([2, 4, 3])
>>> list(p._take(lambda x: x % 2 == 0))
[2, 4]
"""
while predicate(self._items.peek()):
yield next(self._items)

def _done(self):
"""Returns True if the underlying items have been fully consumed."""
try:
self._items.peek()
return False
except StopIteration:
return True

class PeekableIterator:
"""An iterator that supports 1-lookahead (peek)."""

def __init__(self, iterable):
self._iterator = iter(iterable)

# NOTE: We use None here to denote that we haven't peeked yet. This
# doesn't work if None can occur in the iterable (so this
# doesn't generalize!), but for our purposes here it's fine!
self._next_item = None

def peek(self):
"""
Return the next item that will be returned by the iterator without
advancing the iterator. Raises StopIteration if the iterator is done.

>>> i = PeekableIterator([1, 2, 3])
>>> i.peek()
1
>>> i.peek()
1
>>> next(i)
1
>>> next(i)
2
>>> i.peek()
3
"""
if self._next_item is None:
self._next_item = next(self._iterator)

return self._next_item

def __next__(self):
if self._next_item is not None:
next_item = self._next_item
self._next_item = None
return next_item

return next(self._iterator)

def __iter__(self):
return self

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

Теперь у нас есть рабочий сканер, который можно использовать вот так:

>>> list(Scanner('11 * (2 + 3)').scan())
[11, '(', 2, '+', 3, ')']

Посмотрим, как он дает нам список лексем нам нужно? Теперь, мы можем перейти на парсер (без того, чтобы беспокоиться о таких вещах, как плохая пробелы или цифры, на которые float будут вызывать ошибки). Это в этом пункте что печатать будет гораздо безопаснее (заметьте, как наш список поплавков и ниточки, которой мы доверяем, операторы), но я решил опустить его, чтобы не перегрузить.

Теперь, давайте поговорим о парсере. Причина, почему мы собираемся использовать BaseParser ведь, по сути, Scanner это парсер, который принимает итератор (в частности строки повторяемое из символов) и производит что-то (жетоны). Парсер также примет повторяемое (маркеры, производимые сканер) и вывода (для калькулятора это один поплавок, или он будет вызывать исключение).

Теперь, прежде чем мы сделаем это, мы должны понять немного о EBNFs. Это хороший способ представления регулярных языков (входные в нашем калькуляторе). Для вашего калькулятор, это будет выглядеть примерно так:

Expression = Term {("+"|"-") Term} .
Term = Factor {("*"|"/"} Factor} .
Factor = number | "(" Expression ")" .

Теперь-что все это значит? В правой части равенства называются производств. По умолчанию первый из них является "основным". В ("+"|"-") значит, нам нужно либо + или - далее. В { } укажем, что все, что находится внутри них могут возникать ноль или более раз. Так, Expression может быть просто Term, но это также может быть Term "+" Term "-" Term "+" Term.

Если вы попробуете некоторые примеры, как 1 * (2 - 3) + 4вы увидите, как она распадается на спектакли в EBNF. Обратите внимание, как Expression и Factor группы вещей, поэтому порядок операций работает (самое глубокое в вложенности являются * и /это должно произойти в первую очередь). Надеюсь, вы видите, как если бы мы сумели обернуться потока лексем в эту вложенную структуру, мы могли бы оценить его (исх. оценить Expression оценивая все ее TermС, а затем сложение/вычитание результатов по мере необходимости, оценки Term оценивая все ее FactorS и затем умножением/делением результаты соответствующим образом, и оценить Factor либо возвращая число и вычисления выражения и возвращал). Это займет немного, чтобы обернуть вашу голову вокруг, но провести некоторое время с ним, и все станет яснее: http://www.cs.utsa.edu/~wagner/CS3723/grammar/examples2.html

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

import operator

class Parser(BaseParser):
"""Parser for tokenized calculator inputs."""

def parse(self):
"""Parse calculator input and return the result of evaluating it.

>>> Parser([1, '*', '(', 2, '+', 3, ')']).parse()
5
"""
return self._parse_expression()

def _parse_expression(self):
"""Parse an Expression and return the result of evaluating it.

>>> Parser([1, '+', 2])._parse_expression()
3
"""

# Parse the first (required) Term
terms = [self._parse_term()]

# Parse any following: ("*"|"/") Factor
op = lambda t: t in '+-'
terms += flatten((op, self._parse_term()) for op in self._take(op))

return evaluate(terms)

def _parse_term(self):
"""Parse a Term and return the result of evaluating it.

>>> Parser([1, '*', 2])._parse_term()
2
"""

# Parse the first (required) Factor
factors = [self._parse_factor()]

# Parse any following: ("*"|"/") Factor
op = lambda t: t in '*/'
factors += flatten((op, self._parse_factor()) for op in self._take(op))

return evaluate(factors)

def _parse_factor(self):
"""Parse a Factor and return the result of evaluating it.

>>> Parser([1])._parse_factor()
1

>>> Parser(['(', 1, '+', 2, '*', 3, ')'])._parse_factor()
7
"""

# NOTE: Here's where Python gets a little cumbersome. This isn't really
# a for, we're just using it to handle the case where it doesn't
# find a number (gracefully skip). If it finds one, we return the
# number.
for n in self._take(lambda t: isinstance(t, float)):
return n

# If we failed to parse a number, then try to find a '('
for _ in self._take(lambda t: t == '('):
# If we found a '(', parse the subexpression
value = self._parse_expression()
# Make sure the subexpression is followed by a ')'
self._expect(')')

# Both parsing the number and subexpresion failed
raise self._unexpected('number', '(')

def _expect(self, char):
"""Expect a certain character, or raise if it is not next."""
for _ in self._take(lambda t: t == char):
return

raise self._unexpected(char)

def _unexpected(self, *expected):
"""Create an exception for an unexpected character."""
try:
return UnexpectedCharacterError(self._items.peek(), expected)
except StopIteration:
return UnexpectedEndError(expected)

def evaluate(items):
"""
Evaluate a list of floats separated by operators (at the same level of
precedence). Returns the result.

>>> evaluate([3, '*', 4, '/', 2])
6
"""

assert items, 'cannot evaluate empty list'
# x, x + x, x + x + x, etc. all have an odd number of tokens
assert len(items) % 2 == 1, 'list must be of odd length'

while len(items) > 1:
items[-3:] = [_evaluate_binary(*items[-3:])]

return items[0]

def _evaluate_binary(lhs, op, rhs):
"""Evalutates a single binary operation op where lhs and rhs are floats."""
ops = {'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv}

return ops[op](lhs, rhs)

Есть много, чтобы распаковать здесь! Во-первых, взгляните на parse. Он просто разбирает выражение и возвращает результат оценки его (вспомните, как первое производство в EBNF-это "основной"?). Для разбора Expression мы разбираем термин , собрать любое количество ("+"|"-") Term следовать это, и оценить, что математика. Мы делаем что-то похожее на Term. Для Factor мы либо попробуем разобрать ряд (всего float) или "(" (подвыражения) и если мы не можем найти, что мы поднимаем ошибку.

Обратите внимание, как фактическую работу делать математику в сделано в evaluate и _evaluate_binary. Но, главное, обратите внимание, что первые должны только быть даны списки плавает (уже должно быть оценено, парсер обрабатывает это через рекурсию), разделенных операторов одного старшинства. Это значит evaluate намеренно не справиться [1, '+', 2, '*', 3]. Парсер обрабатывает это. Он бы сначала разобрать Term и позвонить evaluate([2, '*', 3]). Это будет возвращен, и тогда он мог бы закончить разбор Expression и позвонить evaluate([1, '+', 6]).

Для простого примера калькулятор, этот код вполне приличное. Но вы можете увидеть, как вещи быстро выйти из-под контроля для более сложных EBNFs. Это то, что я говорил ранее.

Этот код должен следующие вспомогательные:

def flatten(iterable):
"""Flattens a nested iterable by one nesting layer.

>>> flatten([[1,2], [3]])
[1, 2, 3]
"""
return [x for l in iterable for x in l]

Теперь все, что осталось, это подключить сканер и парсер вместе, как мы обсуждали ранее:

def calculate(expression):
"""Evaluates a mathematical expression and returns the result.

>>> calculate('3 * (1 + 6 / 3)')
9
"""
return Parser(Scanner(expression).scan()).parse()

И мы это сделали!

Надеюсь, что дал вам обзор способов анализаторы предназначены и как можно использовать шаблоны, парсер, чтобы сделать ваш калькулятор много проще. Из-за этого дизайна, вы должны уметь делать следующее упражнение достаточно легко:


  • Поддержка шестнадцатеричных чисел (вы только должны изменить сканер!)

  • Поддержка математических констант, как pi и e (вы только должны изменить сканер!)

  • Поддержка ведущих негативных признаков (вам нужно только модифицировать парсер!)

  • Поддержка новых операций, таких как % (по модулю)

Обратите внимание, что этот код только слабо протестированы. Вы должны писать тесты для каждого компонента. Как мне нужно свободно проверить, я полностью выполнены все и поместите его в корень в случае, если вы застряли, соединяющий точки (но на самом деле единственное, что я пропустил был исключением): https://gist.github.com/baileyparker/309436dddf2f34f06cfc363aa5a6c86f

4
ответ дан 24 марта 2018 в 02:03 Источник Поделиться

Вот несколько простых советов...


редактировать: как уже указывалось, isdigit() работает только с целыми числами


Ваш is_number(пункт) функция может быть встроенной isdigit():

if a_list[count].isdigit() and ...

Вы могли бы цепочку вызовов методов, не делает его менее читабельным ИМО:

astring = raw_input("Calculation: ").replace(" ", "")

Первые две петли, вы могли бы быть сведены в одну:

# Next it will add only supported characters to the list
a_list = []
for item in astring:
if item not in set(["0", "1", "2", "3" , "4", "5", "6", "7", "8", "9", "+", "-", "*", "/", ".", "(", ")"]):
print ("Unsupported Character: " + item)
exit()
a_list.append(item)


Кроме того, постарайтесь лучше использовать имена переменных, чем AString и a_list.

2
ответ дан 22 марта 2018 в 01:03 Источник Поделиться