Cómo crear una calculadora RPN con Python y Tkinter

| Última modificación: 22 de abril de 2024 | Tiempo de Lectura: 12 minutos

Algunos de nuestros reconocimientos:

Premios KeepCoding
Hola programadores, les saluda Antonio Alfonso Martínez. ¿Saben cómo crear una calculadora RPN con Python y Tkinter? Como recordarán, hace unos meses, veíamos una forma de crear una calculadora con interfaz gráfica en python, haciendo uso de la librería “tkinter” (al final de este articulo dejo el enlace a dicha entrada). En aquella ocasión creamos una calculadora en la que, la introducción de los datos, debía realizarse en el mismo orden y de la misma forma que empleamos para calcular mediante la función “eval“. No obstante aquel sistema nos obligaba a usar (para operaciones de cierta complejidad) el paréntesis (“()“), lo cual, limitaba, en gran medida, la longitud y complejidad de las operaciones. Partiendo de aquella limitación, en este artículo, nos proponemos crear una calculadora con Python, que use un método alternativo para la introducción de datos: La “Notación Polaca Inversa” (“rpn” por sus siglas en inglés), en la que los valores implicados en una operación, se introducen antes del operador, a diferencia de los que sucede en la notación tradicional (infija), en la que el operador se pone en medio, y la polaca (en la que se pone al principio).

Creando la calculadora “RPN” con Python y Tkinter

Pasamos a continuación a iniciar la creación de nuestra calculadora RPN con Python y Tkinter (cuyo código completo podéis ver en el enlace al final del artículo). Para ello empezaremos importando la librería “tkinter” y el módulo “math”. En esta misma fase, procederemos a crear la ventana (nuestra futura calculadora) definiendo algunos aspectos fundamentales como su título (con “.title()“), dimensiones (con “.geometry()“),y color (con “.configure(background)“. A su vez definiremos el color que tendrán los botones y los caracteres escritos sobre ellos (con las variables “color_boton” y “cn“, respectivamente):
from tkinter import *
ventana=Tk()
ventana.title("CALCULADORA-RPN")
ventana.configure(background="gray20")
ventana.geometry("392x488")
color_boton=("gray50")
cn=("white")
actb="LightCyan3"
#activebackground
from math import *
Otro elemento importante que tendremos que definir son los distintos botones de la calculadora. Dado el método de introducción de datos que va a tener nuestra calculadora (polaca invertida) no se necesita el uso de paréntesis, prescindiremos da tales botones (al igual que el “=“, que sustituiremos por el botón “ENTER“): calculadora RPN con Python y Tkinter Para representar los botones haremos uso de la función “Button” mediante la que definiremos aspectos tales como la altura, anchura, color y emplazamiento de los mismos. También usaremos “command” para definir la función (que veremos luego) que queremos que se ejecute al pinchar sobre dicho botón y crear nuestra calculadora RPN con Python y Tkinter:
Button(ventana,text="0",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("0")).place(x=21,y=180)
Button(ventana,text="1",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("1")).place(x=80,y=180)
Button(ventana,text="2",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("2")).place(x=139,y=180)
Button(ventana,text="3",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("3")).place(x=198,y=180)
Button(ventana,text="4",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("4")).place(x=21,y=228)
Button(ventana,text="5",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("5")).place(x=80,y=228)
Button(ventana,text="6",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("6")).place(x=139,y=228)
Button(ventana,text="7",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("7")).place(x=198,y=228)
Button(ventana,text="8",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("8")).place(x=257,y=228)
Button(ventana,text="9",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:digit("9")).place(x=316,y=228)
Button(ventana,text="π",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=pee).place(x=21,y=276)
Button(ventana,text=".",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=coma).place(x=80,y=276)
Button(ventana,text="+",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:operacion("+")).place(x=139,y=276)
Button(ventana,text="-",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:operacion("-")).place(x=198,y=276)
Button(ventana,text="*",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:operacion("*")).place(x=257,y=276)
Button(ventana,text="/",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:operacion("/")).place(x=316,y=276)
Button(ventana,text="√",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("sqrt")).place(x=21,y=324)
Button(ventana,text="1/x",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("1/")).place(x=198,y=324)
Button(ventana,text="log",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=loga).place(x=257,y=324)
Button(ventana,text="%",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:operacion("%")).place(x=80,y=324)
Button(ventana,text="ln",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("log")).place(x=21,y=372)
Button(ventana,text="sin",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("sin")).place(x=80,y=372)
Button(ventana,text="cos",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("cos")).place(x=139,y=372)
Button(ventana,text="tan",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("tan")).place(x=198,y=372)
Button(ventana,text="R",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:funci("round")).place(x=257,y=372)
Button(ventana,text="CE",bg="red",fg=cn,activebackground="indianred1",width=ancho_boton,height=alto_boton,command=clear_error).place(x=257,y=180)
Button(ventana,text="+/-",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=cambia_signo).place(x=139,y=324)
Button(ventana,text="C",bg="red",fg=cn,activebackground="indianred1",width=ancho_boton,height=alto_boton,command=clear).place(x=316,y=180)
Button(ventana,text="EXP",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=lambda:operacion("**")).place(x=316,y=324)
Button(ventana,text="ENTER",bg=color_boton,fg=cn,activebackground=actb,width=ancho_boton,height=alto_boton,command=enter).place(x=316,y=372)


Entry(ventana,font=('Arial',20,"bold"),width=21,textvariable=input_text,bd=20,insertwidth=4,bg="lavender",justify="right").place(x=16,y=60)
Una vez creados los elementos gráficos (proceso que describo con mayor detalle en la 1ª parte acerca de la creación de una calculadora con interfaz gráfica) , definiremos las variables que van a emplear las funciones de las que nos vamos a vales para hacer que nuestra calculadora funcione:
ancho_boton=6
numero=("")
blocked_ce=False
comas=0
alto_boton=2
input_text=StringVar()
clear()#MUESTRA VALOR "0" AL INICIAR LA CALCULADORA
bd=10

FUNCIONES DE LA CALCULADORA.

Entre tales variables de la calculadora RPN con Python y Tkinter, cabe destacar “input_text” que va a ser igual al string (“StringVar()“) que se irá mostrando en la pantalla de la calculadora y la variable “numero” que usaremos para introducir los valores y que también será igual a los distintos resultados de las operaciones. También en este punto haremos una llamada a la función “clear()” que mostrará el string “0” al iniciar la calculadora:
def clear():
    global numero
    global l_numeros
    global comas
    numero=""
    l_numeros=[]
    input_text.set("0")
    comas=0
La función “clear()” (la cual se ejecutará también al pinchar en el botón “C“) representa el estado inicial de las variables principales “l_numeros” y “numero” de las que hablaremos más adelante. Antes de continuar, hemos de precisar que el funcionamiento interno de nuestra calculadora se va a basar en el almacenamiento en pila de pares de valores introducidos por el usuario. Para ello crearemos una lista (a la que llamaremos “l_numeros“), que en ningún momento va a tener más de 2 elementos, a la que se añadirán los valores suministrados para la variable “numero“. Es sobre los valores añadidos a dicha lista, sobre los que se van a ejecutar las sucesivas operaciones. Para la introducción de los sucesivos valores en la calculadora RPN con Python y Tkinterutilizaremos principalmente dos funciones: “digit()“, para la construcción del valor a introducir, y “enter()” para añadirlo a la memoria interna de la calculadora:
def digit(n):
    global numero
    global l_numeros
    global blocked_ce
    blocked_ce=False
    long=len(l_numeros)
    if long<2 and numero!=str(pi):
        if numero=="0":
            numero=numero.replace("0",n)
        else:
            numero=numero+n
        input_text.set(numero)
Como se ve, la función “digit()“, la cual, se ejecutará al pinchar sobre los botones destinados a la introducción de valores numéricos de la calculadora RPN con Python y Tkinter (a excepción de la coma “.” y el número “pi”, para los que emplearemos sendas funciones especificas), hace uso de una variable “numero” a la que se irá añadiendo de modo sucesivo, los dígitos que compondrán el valor a memorizar (“numero=numero+n“, donde “n” es el dígito asociado al correspondiente botón). Puesto que hemos fijado en 2 la cantidad máxima de valores a memorizar, condicionaremos la ejecución de esta función a que la longitud de “l_numeros” (donde se almacenan los valores a calcular) sea inferior a 2. De modo que los botones para introducción de dígitos quedarán bloqueados cuando hayan memorizados 2 valores, hasta que se efectúe la correspondiente operación. A su vez, tampoco permitiremos la adición de más dígitos si hemos introducido el número “pi”. También, para evitar que puedan introducirse dígitos a la derecha del 0, no habiendo coma, usaremos la función “replace” para sustituir el primer 0 por el dígito introducido. Finalmente, usaremos la variable “input_text” y el método “.set()” para ir viendo el valor en la pantalla de la calculadora, conforme lo vamos introduciendo mediante los botones. De la variable “blocked_ce” hablaremos más adelante cuando tratemos la función “clear_error()“.

INTRODUCCIÓN DE COMA Y NÚMERO “PI”.

En la calculadora RPN con Python y Tkinter, como dijimos antes, la inserción de la coma y del número “pi”, se llevará a cabo mediante las funciones especificas “coma()” y “pee()” respectivamente:
def coma():
    global numero
    global comas
    if numero!="" and comas==0:
        numero=numero+"."
        input_text.set(numero)
        comas+=1
Como se aprecia en la imagen, la función “coma()” (que se ejecutará al pinchar en el botón “.”) lo que hace es añadir el signo “.” al string “numero” (equivalente al valor a introducir en memoria). Condicionaremos la ejecución de esta función al hecho de que ya se haya introducido algún dígito, para evitar errores de ejecución (“if numero!=””“) y al hecho de que no se haya introducido, antes, la coma (“if comas==0“). Una vez introducida la coma, haremos “comas+=1” para bloquear su botón, para el valor actual de “numero“.
def pee():
    global numero
    global l_numeros
    global comas
    global blocked_ce
    if len(l_numeros)<2 and numero=="":
        numero=str(pi)
        input_text.set(numero)
        comas+=1
        blocked_ce=False
Con la función “pee()” lo que haremos es hacer el valor a memorizar igual al número “pi” (hay que recordar aquí que la variable “numero” tendrá siempre, formato cadena, para facilitar los posteriores cálculos, mediante la función “eval()“). Puesto que queremos que la cadena del número “pi” no se añada a una anterior, nos aseguraremos de que el valor de “numero” en ese momento sea nulo (“numero==””“). Puesto que el número “pi” contiene una coma, haremos “comas+=1” para bloquear el botón “.” para ese valor. Otra función de interés aquí, es la función “cambia_signo()” mediante la cual, podemos cambiar el signo (positivo o negativo) de “numero”, antes de introducirlo en memoria:
def cambia_signo(): 
    global numero
    global l_numeros
    if numero!="0" and numero!="":
        numero=str(eval(numero+"*(-1)"))
        input_text.set(numero)
    elif numero=="" and len(l_numeros)==1: #nuevo
        if l_numeros[0]!="0":
            l_numeros[0]=str(eval(l_numeros[0]+"*(-1)"))
            input_text.set(l_numeros[0])
Con esta función cambiaremos el signo de “numero” mediante su multiplicación por “-1“. A su vez, nos aseguraremos, para realizar dicho cambio, de que haya un valor a cambiar (“numero!=””“) y que dicho valor no sea 0 (“numero!=”0″“).

GUARDADO EN MEMORIA (FUNCIÓN “enter()”).

Una vez que hemos definido el valor que deseamos introducir de la calculadora RPN con Python y Tkinter, el siguiente paso es el de su introducción en la memoria de nuestra calculadora (pinchando en el botón “ENTER“). Dicha memoria, consistirá, simplemente en una lista, a la que damos el nombre de “l_numeros“, en la que se almacenará el valor (o valores) que vamos a usar para realizar la operación que deseemos. Esta acción de almacenamiento de valores en la lista, la llevará a cabo una nueva función, de nombre “enter()“:
def enter():
    global numero
    global l_numeros
    global comas
    global blocked_ce
    if numero!="" and numero!="0.":
        l_numeros.append(numero)
        input_text.set(numero)
        numero=""
        comas=0
        blocked_ce=True
Como se aprecia en la imagen, condicionaremos la posible ejecución de la función, al hecho de que se haya introducido, al menos, algún dígito (“if numero!=””“) para evitar errores derivados de intentar operar con valores nulo. También impediremos la ejecución de la función para el caso de que se haya escrito solo “0.“. Una vez que el valor actual de “numero” se haya añadido a la lista “l_numeros“, su valor se hará nulo, de nuevo, quedando, así, libre para recibir un nuevo valor por el usuario. Igualmente, la variable “comas” se hará 0 de nuevo, para desbloquear la posibilidad de introducir dicho signo, de cara al nuevo valor.

FUNCIONES DE CÁLCULO.

Una vez que nuestra calculadora tenga memorizados el valor (o valores, según el caso) introducidos por el usuario, el siguiente paso es el de efectuar la operación aritmética que le indiquemos, pinchando en el correspondiente botón. Para ello, crearemos dos funciones (“operacion()” y “funci()“) de las cuales, en cada vez, usaremos una u otra, en función de si el número de operandos (que se corresponderá con el número de elementos de “l_numeros“) es de 2 o 1: La primera de estas funciones que vamos a ver es “operacion()“, la cual, se ejecutará para aquellas operaciones que requieren dos valores, para su realización (“+”,”-“,”*”,”EXP”,”log”,”/”,”%”) a excepción de la operación “log” a la que aplicaremos una función aparte (“loga()“):
def operacion(s):
    global numero
    global l_numeros
    if len(l_numeros)==2:
        try:
            numero=str(eval(l_numeros[0]+s+l_numeros[1]))
            input_text.set(numero)
            l_numeros[0]=numero
            l_numeros.pop()
        except:
            input_text.set("ERROR")
            l_numeros=[]
        numero=""
Como se ve, para su ejecución, esta función requiere que la longitud de la lista “l_numeros” sea de 2 (“if len(l_numeros)==2:“) una vez que dicha condición se cumpla, la función “intentará” (mediante la sentencia “try“), realizar la correspondiente operación (que en la función, vendrá dada por el valor que adopte la variable “s“) con la cadena (usando la función “eval”) tomando los valores sitos en las posiciones 0 y 1 de la lista (“l_numeros[0]” y “l_numeros[1]“). De modo que de no poder efectuarse (lo que puede suceder si se introduce una operación prohibida, como la división por 0) la función “except” prevé la aparición del mensaje de “ERROR” en la pantalla. Una vez calculado el resultado de la operación, la posición 0 de “l_numeros” pasará a ser igual a dicho resultado (“l_numeros[0]=numero“) para, acto seguido, usar la función “.pop()” para sacar de la lista el segundo valor introducido (correspondiente a “l_numero[1]“), dejando su posición, libre para recibir un nuevo valor. Para aquellas operaciones que requieran, unicamente, un valor (“ln”,”sin”,”cos”,”tan”,”1/x”,”√”) para devolver un resultado, emplearemos otra función: “funci()“:
def funci(s):
    global numero
    global l_numeros
    if len(l_numeros)==1:
        try:
            numero=str(eval(s+"("+l_numeros[0]+")"))
            input_text.set(numero)
            l_numeros[0]=numero
        except:
            input_text.set("ERROR")
            l_numeros=[]
        numero=""
El esquema de esta función (la cual se ejecutará para “len(l_numeros)==1“) emplea un esquema muy similar a la anterior, con la principal diferencia que en este caso no hacemos uso de la función “.pop“, ya que ene este caso solo contamos en la lista, con un solo elemento, que será igual al resultado de la operación realizada. Mención aparte requiere el calculo del logaritmo de un número con una base determinada por el usuario (“log”) debido a que el modo de calcularla es “log(x)/log(B)” (donde “x” es el número cuyo logaritmo queremos calcular y “B” la base para dicho cálculo) no se ajusta al esquema que empleamos para el resto de operaciones a realizar con “funci()“.
def loga():
    global l_numeros
    global numero
    if len(l_numeros)==2:
        try:
            numero=str(eval("log("+l_numeros[0]+")/log("+l_numeros[1]+")")) 
            l_numeros.pop()
        except:
            input_text.set("ERROR")
            l_numeros=[]
        numero=""

CORRECCIÓN DE VALORES

Como sabemos, el modo para suministrarle los datos a nuestra calculadora, consiste en introducir el valor (o valores) mediante los botones, y luego darle al botón “ENTER“. No obstante, en ocasiones el usuario puede cometer errores a la hora de introducir los dígitos, deseando es ese caso, cambiar el valor a introducir. Es por ello que vamos a incluir una nueva función, de nombre “clear_error()“:
def clear_error():
    global numero
    global comas
    global blocked_ce
    if blocked_ce==False:
        numero=""
        input_text.set("0")
        comas=0
        blocked_ce=True
Sin embargo, con respecto a esta función, la cual, se ejecutará al darle al botón “CE” de la calculadora, tenemos que especificar en qué supuestos queremos permitir que, efectivamente se ejecute. En nuestro caso, vamos a hacer que pueda cambiarse el valor, antes de dar a “ENTER“, de modo que la función quede bloqueada con posterioridad para ese número. Es por ello que hemos creado la variable booleana “blocked_ce“, la cual, cuando sea “True” bloqueará la función (dejando sin efecto el acto de pinchar sobre “CE“), mientras que en el caso que sea “False“, permitirá la ejecución de la función. Puesto que queremos que la función vuelva a desbloquearse, cando, tras haber sido memorizado el número, empecemos a introducir un nuevo valor, haremos “blocked_ce==”False” al ejecutar la función “digit()“. Y con todo esto, tendríamos creada nuestra calculadora en notación polaca invertida. Ahora, lo único que nos queda es poner al final del código, nuestra variable “ventana” seguido de “.mainloop()” para que nuestra calculadora permanezca en pantalla hasta que decidamos cerrarla.
ventana.mainloop()
Saludos.

Antonio Alfonso MartínezProgramador y desarrollador autodidacta. Semanalmente publica en el blog El programador Chapuzas (wordpress) y también colaboro en las páginas “Código Comentado” y “Algoritmos MathPy” de Facebook.

Si quieres formar parte de la comunidad KeepCoding, compartiendo información relevante sobre desarrollo Web, Mobile, Big Data o Blockchain puedes escribirnos a [email protected] .
Fernando Rodríguez

iOS Developer & Co-Fundador de KeepCoding

Posts más leídos