Ich hab mich auch mal drangesetzt.
Erstmal ein paar Tips zum C Programm. Die lassen sich teilweise auch auf Python übertragen. So einer ist zum Beispiel: Wo immer es möglich ist Bibliotheksfunktionen benutzen, anstatt etwas selbst zu schreiben. Das setzt natürlich voraus, dass man die entsprechenden Funktionen kennt. Sowohl Python als auch C kommen mit einer recht umfangreichen Bibliothek daher, wobei Python ziemlich viele "High-Level" Module mitbringt, weil man sich um den Kleinkram nicht so sehr wie in C kümmern muss.
Es gibt in C beispielsweise eine Funktion `isdigit()`, die testet ob ein Zeichen eine Ziffer ist. Das ist kürzer und lesbarer als selbst auf Unter- und Obergrenze des entsprechenden Zeichenkodebereichs zu testen.
Das kann man sich in Deinem Taschenrechner aber auch komplett sparen, weil es eine Funktion gibt, die eine Zeichenkette mit Ziffern in ein `double` umwandelt.
Aber fangen wir mal bei der Eingabe an. Das `scanf()` ist gefährlich, weil man mehr als 20 Zeichen eingeben kann und damit einfach Daten über `eingabe` hinaus in den Speicher geschrieben werden. `fgets()` ist immer sicherer, da man dort eine maximale Anzahl von Zeichen angeben kann.
Für das Umwandeln von Zeichenkette mit Ziffern nach `double` gibt's wie gesagt eine Funktion (`atof()`) und man kann mit `strpbrk()` eines aus einer Menge von Zeichen in einer Zeichenkette suchen lassen. Mit diesen beiden Funktionen lässt sich im Grunde in nur drei Zeilen die `eingabe` in zwei Zahlen und einen Operator zerlegen.
Dann kommt die Rechnung. In C bietet sich für die vier ``if``-Abfragen eine ``switch``/``case`` Konstruktion an, und anstelle der Hexadezimalnotation wären einzelne literale Zeichen für die Operatoren lesbarer. Also statt ``0x2b`` ein ``'+'``.
Code: Alles auswählen
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INPUT_SIZE 20
/* Erlaubte Operatoren. */
static const char *OPERATORS = "+-*/";
int main(void)
{
char eingabe[INPUT_SIZE + 1];
char *operator;
double zahl1, zahl2, ergebnis;
while (1) {
printf("Geben sie die Rechnung ein (zahl op zahl): ");
fgets(eingabe, INPUT_SIZE, stdin);
zahl1 = atof(eingabe);
operator = strpbrk(eingabe, OPERATORS);
if (operator == NULL) {
printf("Keine Operation (%s) angegeben.\n", OPERATORS);
continue;
}
zahl2 = atof(operator + 1);
switch (*operator) {
case '+': ergebnis = zahl1 + zahl2; break;
case '-': ergebnis = zahl1 - zahl2; break;
case '*': ergebnis = zahl1 * zahl2; break;
case '/': ergebnis = zahl1 / zahl2; break;
default:
/* Sollte nie ausgeführt werden, solange für jedes Zeichen
in `OPERATORS` ein Fall existiert. */
printf("PANIK! (%s:%d:'%c')", __FILE__, __LINE__, *operator);
return EXIT_FAILURE;
}
printf("%.2f %c %.2f = %.2f\n\n", zahl1, *operator, zahl2, ergebnis);
}
}
Ist etwas kürzer und ohne die ganzen Schleifen und Details übersichtlicher. Kleiner Testlauf:
Code: Alles auswählen
bj@s8n:~> ./a.out
Geben sie die Rechnung ein (zahl op zahl): 42 + 23
42.00 + 23.00 = 65.00
Geben sie die Rechnung ein (zahl op zahl): 1 / 2
1.00 / 2.00 = 0.50
Geben sie die Rechnung ein (zahl op zahl):
Keine Operation (+-*/) angegeben.
Geben sie die Rechnung ein (zahl op zahl): hallo * welt
0.00 * 0.00 = 0.00
Geben sie die Rechnung ein (zahl op zahl): test*
0.00 * 0.00 = 0.00
Geben sie die Rechnung ein (zahl op zahl): -
0.00 - 0.00 = 0.00
Geben sie die Rechnung ein (zahl op zahl): 0x20 + 8
32.00 + 8.00 = 40.00
Hier sieht man einen interessanten Aspekt von `atof()`: Führende Leerzeichen werden ignoriert und bei dem ersten Auftauchen eines Zeichens, das nicht zu einer Fliesskommazahl gehören kann, wird die Konvertierung abgebrochen. Was im Extremfall, also keine gültigen Ziffern, zu einer Null führt. Wenn man etwas mehr Kontrolle über solche Einfabefehler haben möchte, dann kann man `strtod()` zum Umwandeln benutzen, muss aber natürlich noch extra Quelltext zum Erkennen eines Fehlers und entsprechender Reaktion darauf schreiben.
Neben Fliesskommazahlen kann man übrigens auch Hexadezimalzahlen bzw. alle Zahl-Literale eingeben, die man auch in C-Quelltext angeben könnte. Problematisch ist allerdings die Angabe von ``+`` oder ``-`` innerhalb einer Zahl, da das erste Auftreten von so einem Zeichen vom Programm als Operator erkannt wird. Die Zahl ``1e-2`` macht also Probleme, obwohl es sich um eine gültige Fliesskommazahl handelt.
Das C Programm nach Python portiert kann so aussehen:
Code: Alles auswählen
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
import operator
operations = { '+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv }
def main():
while True:
line = raw_input('Geben sie die Rechnung ein (Zahl op Zahl): ')
try:
components = line.split()
zahl1 = float(components[0])
operation = components[1]
zahl2 = float(components[2])
except ValueError, error:
print 'Fehler in einer Zahl:', error
continue
except IndexError:
print ('Zu wenige Angaben. '
'Bitte zwei Zahlen und eine Operation angeben.')
continue
try:
ergebnis = operations[operation](zahl1, zahl2)
print '%s %s %s = %s' % (zahl1, operation, zahl2, ergebnis)
except KeyError:
print 'Unbekannte Operation %r' % operation
if __name__ == '__main__':
main()
Ich habe `str.split()` verwendet um die Eingabe zu zerlegen, was natürlich den Anwender im Gegensatz zum C-Programm zwingt, Leerzeichen zwischen die Zahlen und den Operator zu schreiben.
Man sieht zwei wichtige Unterschiede zwischen den Programmen. In Python muss man sich um die Fehler kümmern, da im Python-Zen unter anderem der Satz
Errors should never pass silently. steht. Fehler sollten also nie einfach ignoriert oder durch einen vorgegebenen Wert ersetzt werden.
Und es gibt in Python kein ``switch``/``case`` Gegenstück in der Syntax. Das wird oft durch ein Dictionary mit den Fällen als Schlüssel und den dazugehörigen Funktionen als Wert umgesetzt.
Testlauf:
Code: Alles auswählen
Geben sie die Rechnung ein (Zahl op Zahl): 42 + 23
42.0 + 23.0 = 65.0
Geben sie die Rechnung ein (Zahl op Zahl): 1 / 2
1.0 / 2.0 = 0.5
Geben sie die Rechnung ein (Zahl op Zahl): 5 # 5
Unbekannte Operation '#'
Geben sie die Rechnung ein (Zahl op Zahl):
Zu wenige Angaben. Bitte zwei Zahlen und eine Operation angeben.
Geben sie die Rechnung ein (Zahl op Zahl): 7 -
Zu wenige Angaben. Bitte zwei Zahlen und eine Operation angeben.
Geben sie die Rechnung ein (Zahl op Zahl): 0x20 + 8
32.0 + 8.0 = 40.0
Geben sie die Rechnung ein (Zahl op Zahl): abc * def
Fehler in einer Zahl: invalid literal for float(): abc
Für kompliziertere Ausdrücke kann man sich natürlich einen Parser komplett selbst schreiben, aber das sollte man höchstens zu Lernzwecken machen. Für den "echten" Einsatz sind Parsergeneratoren vorzuziehen, denen man eine Grammatik vorgibt und die dann den Quelltext für einen Parser generieren. Für C ist das traditionell ``lex``/``yacc`` bzw. ``bison``. Für Python finde ich
pyparsing sehr interessant. Dort setzt man eine Grammatik zur Laufzeit aus Objekten zusammen. Die Schreibweise erinnert an EBNF-Notation.
EBNF-Notation ist eine gängige Möglichkeit eine Grammatik zu beschreiben. Für die vier Grundrechenarten mit Klammerung für ganze Zahlen könnte eine EBNF-Grammatik so aussehen:
Code: Alles auswählen
<add_op> ::= '+' | '-'
<mult_op> ::= '*' | '/'
<number> ::= <digit> | <digit> <digits>
<digit> ::= '0' | '1' | '2' | ... | '9'
<atom> ::= <number> | '(' <expression> ')'
<term> ::= <atom> | <atom> <mult_op> <term>
<expression> ::= <term> | <term> <add_op> <expression>
Das man nicht einfach ``<expression> <op> <expression>`` schreiben kann, liegt daran, dass man "Punkt-vor-Strichrechnung" und Klammerung damit nicht gewährleistet. Durch das Einfügen von ``<term>`` und die Aufteilung der "Punkt"- und "Strich"rechenzeichen ist der übliche Operatorvorrang sichergestellt.
Der Syntaxbaum für den Ausdruck ``1+2*3`` sieht dann so aus (Übergang von Ziffern zu ``<atom>`` ist verkürzt dargestellt):
Code: Alles auswählen
<expression>
|
+--------+-----------------+
| | |
| | <expression>
| | |
| | <term>
| | |
| | +--------+---------+
| | | | |
<term> | | | <term>
| | | | |
<atom> <add_op> <atom> <mult_op> <atom>
| | | | |
'1' '+' '2' '*' '3'
Das die Klammerung funktioniert kann man am Syntaxbaum für den Ausdruck ``2*(3+4)`` sehen:
Code: Alles auswählen
<expression>
|
<term>
|
+--------+--------------------+
| | |
| | <term>
| | |
| | <atom>
| | |
| | +--------------+----------------+
| | | | |
| | | <expression> |
| | | | |
| | | +--------+--------+ |
| | | | | | |
| | | | | <expression> |
| | | | | | |
| | | <term> | <term> |
| | | | | | |
<atom> <mult_op> | <atom> <add_op> <atom> |
| | | | | | |
'2' '*' '(' '3' '+' '4' ')'
Die Grammatik mit `pyparsing` ausgedrückt, sieht so aus:
Code: Alles auswählen
from pyparsing import Word, Forward, ZeroOrMore, Suppress, Group, oneOf, nums
def grammar():
add_op = oneOf('+ -')
mult_op = oneOf('* /')
number = Word(nums)
expression = Forward()
atom = number | Suppress('(') + expression + Suppress(')')
term = Forward()
term << Group(atom + ZeroOrMore(mult_op + term))
expression << term + ZeroOrMore(add_op + expression)
return expression
def test():
parser = grammar()
for test in ('1 + 2 * 3',
'2 * (3 + 4)',
'1 + 2 - 3',
'2 * 3 / 4'):
result = parser.parseString(test).asList()
print '%15s => %s = %f' % (test, result, evaluate(result))
Das Ergebnis der Testfunktion:
Code: Alles auswählen
1 + 2 * 3 => [['1'], '+', ['2', '*', ['3']]]
2 * (3 + 4) => [['2', '*', [['3'], '+', ['4']]]]
1 + 2 - 3 => [['1'], '+', ['2'], '-', ['3']]
2 * 3 / 4 => [['2', '*', ['3', '/', ['4']]]]
Man bekommt also verschachtelte Listen, bei denen an den geraden Indizes die Zahlen oder Unterlisten und an den ungeraden Indizes die Operationen stehen. Das lässt sich mit einer rekursiven Funktion leicht auswerten.
Code: Alles auswählen
def evaluate(ast):
numbers = list()
for element in ast[::2]:
if isinstance(element, list):
numbers.append(evaluate(element))
else:
numbers.append(int(element))
result = numbers[0]
for operation, number in zip(ast[1::2], numbers[1:]):
result = operations[operation](result, number)
return result
Im Archiv von `pyparsing` befinden sich zwei etwas umfangreichere Beispiele für das Auswerten von Ausdrücken bzw. ein kompletter Taschenrechner mit Funktionen und Variablenzuweisungen.
Die einfachste Art (ausser ``eval``) einen Taschenrechner zu programmieren, dürfte die
umgekehrte Polnische Notation sein (Reverse Polish Notation, kurz RPN), welche eindeutige Auswertung von Ausdrücken ohne Klammerung erlaubt. Dabei werden erst die Operanden und dann die Operationen angegeben. Die Operanden (Zahlen) landen auf einem Stapel und die Operationen holen sich von dort ihre Argumente und legen das Ergebnis wieder auf dem Stapel ab.
Man kann ganz einfach Operationen definieren, die mehr oder weniger als zwei Operanden erwarten. Sogar solche, die keinen Operanden erwarten, also Konstanten.
Code: Alles auswählen
import math
import operator
def create_rpnfunc(func, arg_count):
if arg_count > 0:
def rpn_func(stack):
args = stack[-arg_count:]
stack[-arg_count:] = []
stack.append(func(*args))
else:
rpn_func = lambda stack: stack.append(func())
return rpn_func
def create_rpnfuncs():
operations = (# Operations with 0 operands.
(('PI', lambda: math.pi),
('E', lambda: math.e)),
# Operations with 1 operand.
(('negate', operator.neg),
('abs', operator.abs),
('invert', operator.invert),
('sin', math.sin),
('cos', math.cos),
('radians', math.radians),
('degrees', math.degrees)),
# Operations with 2 operands.
(('+', operator.add),
('-', operator.sub),
('*', operator.mul),
('/', operator.truediv),
('^', operator.pow),
('modulo', operator.mod)))
result = dict()
for arg_count, ops in enumerate(operations):
for name, func in ops:
result[name] = create_rpnfunc(func, arg_count)
return result
rpn_operations = create_rpnfuncs()
def rpn_evaluate(expression):
stack = list()
for element in expression.split():
try:
stack.append(float(element))
except ValueError:
try:
rpn_operations[element](stack)
except KeyError:
raise Exception('Unknown operation %r' % element)
return stack
def rpn_test():
for test in ('1 2 3 * +',
'2 3 4 + *',
'1 2 3 - +',
'2 3 4 / *',
'2 PI * degrees sin'):
print test, '=', rpn_evaluate(test)