Saltar al contenido
Cómo hacer una calculadora en Python

Desarrollar una calculadora es un problema clásico en cualquier lenguaje de programación. Y como no podía ser de otra manera, también lo es en Python.

He buscado ejemplos interesantes de este problema en internet, pero la verdad es que no los he encontrado, al menos en español. Todas las soluciones que he visto se reducen a unas pocas líneas en las que se realizan operaciones muy básicas.

Escribo esta entrada en Código Pitón porque quiero compartir contigo mi versión de este problema en donde lo que pretendo es simular el comportamiento de una calculadora real, aunque simplificada, claro.

Es un problema muy interesante y con el que se puede aprender mucho porque vamos a ver varios conceptos: programación orientada a objetos, arquitectura de software, patrones de diseño, interfaces de usuario, etc.

Vamos a ello, entonces. Primero voy a introducir el problema explicándote brevemente cómo funciona una calculadora (aunque esto ya lo conocerás de sobra si has utilizado una). Después explicaré la arquitectura del proyecto, en el que veremos que vamos a separar el núcleo de la calculadora de la interfaz de usuario. Tras esto realizaremos el diseño pasaremos a la implementación del proyecto, en el que, además, haremos una interfaz en modo gráfico con tkinter. Para terminar, veremos el resultado final y las conclusiones.

Funcionamiento de una calculadora clásica

Vamos a desarrollar una aplicación que simule el comportamiento de una calculadora real, una calculadora física de las de toda la vida, como la que puedes ver en la imagen de arriba, la que ilustra este artículo.

Este tipo de calculadoras permiten la introducción de dígitos de manera secuencial para ir construyendo números de mayor tamaño, así como la introducción de distintos operadores para realizar diversas operaciones con los números introducidos.

Las calculadoras tienen una pequeña pantalla en la que se van viendo los números y los resultados de esas operaciones.

Lo importante aquí es que veremos cómo todo funciona secuencialmente según se pulsan los botones de la calculdora, es decir, no se interpretará toda una expresión matemática.

Esto es precisamente lo que vamos a emular con nuestro proyecto. ¡Allá vamos!

Arquitectura del proyecto

Aunque puede parecer un proyecto sencillo, es algo más complejo de lo que parece. Es por esto, que antes de empezar a escribir código hay que realizar un proceso de análisis y diseño.

Así pues, y simplificando, en nuestra calculadora existen dos elementos claramente diferenciados y con cierta independencia:

  • La interfaz de usuario. Es el componente con el que interactuará el usuario. Es decir, es la manera que tiene el usuario de introducir números y operadores en la calculadora. También podrá comprobar el resultado de sus operaciones. También solemos llamar a esta parte "capa vista".
  • El modelo. Se llama modelo o capa modelo a aquella parte del software que se encarga de representar y mantener la lógica del negocio o del problema. En nuestro proyecto el modelo se encargará de mantener el estado de la calculadora (los números y operadores introducidos) y realizar los cálculos.

El modelo no debe conocer a la interfaz de usuario. Esto permite reducir el acoplamiento entre componentes y aumentar la cohesión. Además, esto permite crear varias interfaces, si lo necesitamos, sin necesidad de alterar el modelo.

Por otro lado, la interfaz sí debe conocer al modelo, pues es necesario comunicarse con este para proporcionarle los números y operadores según los va introduciendo el usuario, así como obtener el resultado de las operaciones para poder mostrarlo en la pantalla de la calculadora.

Esta una versión simplificada del patrón arquitectónico MVC o modelo - vista - controlador.

Diseño

Como ya he dicho antes y en otros artículos, un buen desarrollador de software debe realizar etapas de análisis y diseño antes de escribir una sola línea de código. Eso es lo que vamos a hacer aquí pues estamos delante de un problema que entraña cierta complejidad.

A ver, te cuento...

Te envío todos los días un consejo para que cada día seas mejor en Python.
Puede ser un truco, una píldora, un tip, una historia de mentalidad o motivación, cualquier cosa...
Siempre sobre Python y programación.
Más de 2500 personas como tú los reciben cada día.

Día que estás fuera, consejo sobre Python que te pierdes.
Abajo te suscribes a la lista de correo y te regalo un montón de cosas: La Hoja de Referencia de Python, el Tutorial Interactivo, 30 Ejercicios de Python y la Guía de ChatGPT y Python. By the face.

✅ Contenido exclusivo de alto valor      ✅ Sin spam      ✅ Cancela cuando quieras



Antes de suscribirte consulta aquí la 
Información Básica sobre Protección de Datos. Responsable de datos: Juan Monroy Camafreita.
Finalidad de recogida y tratamiento de datos personales: enviarte boletín informativo de Python y comunicaciones comerciales.
Legitimación: tu consentimiento.
Destinatarios: no se ceden a terceros. Los datos se almacenan en los servidores de marketing (GetResponse).
Derechos: podrás ejercer tus derechos de acceso, rectificación, limitación y supresión de datos en info @ codigopiton.com así como presentar una reclamación ante una autoridad de control.
Más información: política de privacidad, encontrarás información adicional sobre la recopilación y el uso de tu información personal.

El principal problema que nos encontramos en una calculadora es que podemos tomar diferentes acciones (introducir un dígito, pulsar un operador unario, pulsar un operador binario, pulsar el botón de obtener resultado, pulsar el botón de borrar, etc.) y en cualquier orden. La calculadora debe funcionar y ser coherente con independencia de la secuencia de acciones que tome el usuario.

Además, internamente, la calculadora pasa por diferentes estados, como pueden ser el "estado inicial", "introduciendo un operando", "error", etc.

Combinando los estados internos de la calculadora con el abanico de acciones que puede tomar el usuario llegamos a un problema en el que surgen muchas posibilidades y muchos casos a tomar en cuenta. Esto suele derivar en un código que contiene una cantidad elevada de condicionales, y que lo convierte en difícil de implementar, de mantener y de leer.

Pero, ¡no pasa nada! El diseño de software está aquí para ayudarnos. Y es que vamos a aplicar un patrón de diseño que resuelve nuestro problema. Los patrones de diseño son soluciones preestablecidas a problemas comunes.

Un libro fundamental, que me ha permitido aprender muchísimo y ser mejor programador es Patrones de Diseño de Erich Gamma (y otros). No lo puedo recomendar bastante y es que pienso que este libro debería estar en la biblioteca de todo programador o desarrollador.

De este libro vamos a utilizar el patrón estado. El patrón de diseño estado nos sirve para solucionar un problema muy común, que es el de un programa o componente que debe cambiar su comportamiento ante determinadas entradas según el estado en que se encuentre. Esto es exactamente lo que pasa con una calculadora.

Vamos a verlo.

Estados de la calculadora

Para entender correctamente la calculadora antes de pasar a implementarla, tenemos que determinar los diferentes estados por los que va a transitar la calculadora y su comportamiento en cada uno de ellos.

Vamos a considerar, de momento, que solo hay tres acciones posibles que puede tomar el usuario:

  • Introducir un dígito. Es la manera de ir construyendo un número, que puede ser de más de un cifra, en la calculadora. Pulsando varias veces alguno de los dígitos de manera secuencial el usuario va creando el número que necesita. Esto nos lleva ya a pensar que existe al menos un estado en la calculadora que nos sirve para construir el número. Podría llamarse algo así como "introduciendo operando". Pero esto lo cuento más adelante. Por comodidad, vamos a suponer que el punto separador de decimales también es un dígito. Es decir, las opciones aquí son introducir alguno de los siguientes caracteres: ., 0, 1, 2, 3, 4, 5, 6, 7, 8 y 9.
  • Introducir un operador. Una vez que hemos terminado de construir un número, el usuario va a querer indicar una operación para realizar con él. Este operador puede ser unario (de un solo operando) o binario (de dos operandos). Entre los unarios vamos a considerar el operador de cambio de signo y la raíz cuadrada: ± y . Entre los operadores binarios vamos a considerar la suma, la resta, la multiplicación y la división: +, -, × y ÷.
  • Pulsar el botón de calcular. Cuando el usuario termina de introducir la operación que quiere calcular tiene que pulsar el botón =. Esto realiza la operación y muestra el resultado por la pantalla de la calculadora, que está lista para realizar una nueva operación, esta vez, partiendo del valor mostrado por pantalla.

Para ir viendo los estados que necesitamos y las transiciones entre ellos vamos a empezar por el principio y es pensar en un estado inicial, que es aquel en el que empieza la calculadora cuando la encendemos. En este estado se suele mostrar un 0 en la pantalla de la misma. Como vemos más adelante, la situación en la que el usuario acaba de pulsar el botón de calcular y tenemos un resultado por pantalla también puede ser considerada como un estado inicial, solo que en lugar de partir de 0 partimos de cualquier otro valor.

Así, en este "estado inicial" pueden suceder varias cosas, pues el usuario puede introducir un número o un operador, y la calculadora debe tener un comportamiento diferente para cada una de estas acciones en el estado inicial.

Estado "inicial"

En el estado inicial podemos considerar que el usuario no ha comenzado a realizar ninguna operación todavía y la calculadora muestra un número en pantalla. O bien se muestra el 0 porque se acaba de encender la calculadora, o bien se muestra otro valor cualquier porque ya se ha realizado una operación, es decir, se está mostrando el resultado de una operación. En ambos casos, ese número está almacenado en la memoria interna de la calculadora.

Estos son los comportamientos del estado "inicial":

  • El usuario introduce un dígito. Cuando sucede esto, empieza a construirse un número desde el principio, así que hay que almacenar el primer dígito y mostrarlo por pantalla. Además, la calculadora se encuentra ahora en un nuevo estado, pues está preparada para seguir recibiendo dígitos e ir construyendo un número más grande. Es por esto que la calculadora deja de estar en el estado inicial y pasa a otro estado llamado "introduciendo primer operando". Como hay operadores binarios vamos a distinguir entre los estados de introducir primer operando e introducir segundo operando, como veremos más adelante.
  • El usuario introduce un operador. Aquí existen dos opciones, que el usuario introduzca un operador unario o que introduzca uno binario, así que vamos a distinguir entre ellas:
    • El usuario introduce un operador unario. En este caso, hay que tomar el número que está almacenado en la memoria de la calculadora (que es el que se está mostrando en la pantalla) y aplicar la operación. Hay que recordar que en las calculadoras las operaciones unarias suelen ser postfijas, es decir, primero se escribe el número y después la operación. Así pues, tras introducir el operador unario, se calcular el resultado, se actualiza el número en la memoria interna y se muestra por pantalla. La calculadora permanece en el estado inicial.
      Aquí hay otra posibilidad, y es que si el número almacenado es negativo y se introduce el operador de calcular la raíz cuadrada tenemos que llevar la calculadora a un estado de error, pues en el dominio de los números reales no está permitida esa operación.
    • El usuario introduce un operador binario. Si el usuario introduce un operador binario se asume que el número almacenado en el estado inicial es el primer operando y se almacena el operador. Además, cambiamos a un nuevo estado llamado "resultado parcial" en el que se está mostrando por pantalla un valor que se puede considerar un resultado parcial pues todavía nos falta el segundo operando. Volveremos más tarde sobre este estado.
  • El usuario pulsa el botón de calcular. No sucede nada.

Estado "introduciendo primer operando"

En el estado "introduciendo primer operando" ya tenemos un primer dígito pues el usuario ya ha lo ha introducido. Ahora la calculadora está lista para seguir construyendo el número final dígito a dígito hasta que se pulse un operador.

Así, en este estado los comportamientos de la calculadora son los siguientes:

  • El usuario introduce un dígito. Se añade el nuevo dígito al número almacenado, que ahora es un poco más grande y se muestra por pantalla el nuevo número compuesto de todos los dígitos introducidos hasta el momento. Permanecemos en este estado para recibir más dígitos o un operador para realizar una operación.
  • El usuario introduce un operador unario. Ante esta acción el comportamiento es parecido al del estado inicial. Se calcula la operación, se almacena el resultado, se muestra por pantalla y la calculadora cambia al estado inicial. Nuevamente, si la operación es una raíz negativa, la calculadora pasa al estado de error.
  • El usuario introduce un operador binario. El comportamiento en este caso es igual al del estado inicial ante esta acción, es decir, se almacena el número construido hasta el momento como primer operando, se almacena el operador y pasamos al estado de "resultado parcial".
  • El usuario pulsa el botón de calcular. Igual que en el caso anterior, no sucede nada.

Estado "resultado parcial"

Este es un estado similar al inicial pero con la diferencia de que la pantalla está mostrando un número que es el primer operando de una operación binaria. Tanto este número como el operador binario introducido por el operador están almacenados en la memoria de la calculadora.

Veamos los comportamientos de la calculadora en este estado para cada una de las posibles acciones.

  • El usuario introduce un dígito. Empieza a construirse el número que representa el segundo operador de la operación, de manera similar a lo que sucedía cuando se introducía un dígito en el estado inicial. Se muestra el nuevo número por pantalla, se almacena el dígito y la calculadora cambia al estado "introduciendo segundo operador".
  • El usuario introduce un operador unario. Se calcula la operación, se actualiza el valor del primer operando y se muestra por pantalla. Se mantiene almacenado el operador binario introducido previamente. La calculadora permanece en este estado. Si la operación es inválida (raíz de número negativo) pasamos al estado de error.
  • El usuario introduce un operador binario. Se sustituye el operador almacenado previamente por el nuevo. Es útil por si el usuario se equivoca al introducir un operador.
  • El usuario pulsa el botón de calcular. Cuando sucede esto se toma el valor mostrado por pantalla, que además es el valor del primero operando, como segundo operando y se calcula el resultado. Se almacena este resultado en la calculadora, se muestra por pantalla y volvemos al estado inicial. Si la operación no es válida (división por 0) pasamos al estado de error.

Estado "introduciendo segundo operando"

Este estado es muy similar al estado "introduciendo primer operando" con la salvedad de que la calculadora tiene almacenado un primer operando y un operador para poder realizar una operación binaria.

15 conceptos fundamentales que necesitas conocer para aprender y dominar Python

Te voy a hacer cuatro regalos (no uno, no dos, no tres, cuatro) que hablan de estos 15 conceptos fundamentales de Python: mi Tutorial Básico Interactivo de Python, una cheat sheet de Python en español: La Hoja de Referencia de Python, una guía de ChatGPT y Python y 30 ejercicios de Python (es un reto para ti).

Estos regalos son exclusivos para los suscriptores de Código Pitón.

👍 Quiero mis cuatro regalos

Los comportamientos de este estado ante las acciones del usuario son las siguientes:

  • El usuario introduce un dígito. Se añade el dígito al número construido hasta el momento, se almacena y se muestra por pantalla. La calculadora permanece en este estado.
  • El usuario introduce un operador unario o binario. En ambos casos se calcula la operación y se almacena el primer operando y el operador, y después se pasa al estado de "resultado parcial", pero con alguna diferencia. Si se introduce un operador binario, el resultado de la operación se almacena como primer operando y se almacena el nuevo operador. Si el operador es unario, se aplica la operación sobre el segundo operando. El primer operando continua siendo el mismo, así como el operador binario introducido previamente. Como en otros casos, si la operación es una raíz cuadrada y el segundo operando es menor que cero, la calculadora pasa al estado del error.
  • El usuario pulsa el botón de calcular. En este caso la operación es igual que en el estado anterior. Se calcula el resultado de la operación y se almacena. Se vuelve al estado inicial. Si la operación es errónea (división por cero) se transita hacia el estado de error.

Estado de error

Es un estado final que representa que ha habido algún error o problema con alguna operación y no se puede continuar. Ninguna acción del usuario tendrá efecto excepto una. De esta operación no había hablado de momento pues no era necesaria para las explicaciones. Es la operación de "reset" o "AC" que borra todo lo almacenado en la calculadora (números, operandos, operadores o errores), coloca el valor 0 en la pantalla y transita al estado "inicial" para comenzar de nuevo si se desea.

Diagrama de estados

Se muestra a continuación un diagrama UML (Unified Modeling Language, o Lenguaje Unificado de Modelado) que representa todos los estados de la calculadora, así como las transiciones entre ellos, las acciones que disparan estas transiciones y el resultado de las mismas.

Por si no conoces UML o no habías visto antes un diagrama de estados, te lo explico brevemente y verás que se entiende muy bien:

  • Cada caja con las esquinas redondeadas representa un estado de la calculadora. Como puedes ver, los cinco estados que aparecen representan cada uno de los cinco estados que te he contado antes.
  • El punto negro que puedes ver a la izquierda de todo y etiquetado como "Inicio" indica el comienzo del flujo de transiciones entre estados. Claramente se puede ver que el primer estado es el estado "inicial".
  • El punto negro dentro de una circunferencia que puedes ver a la derecha representa el final del flujo de transiciones entre estados. He representado esto así porque el estado de error finaliza todo, pues la operación no puede continuar si la calculadora entra en ese estado. Aunque esto es discutible, pues con la acción de "reset" o "AC" se puede saltar al estado "inicial".
  • Cada flecha representa una posible transición de un estado a otro, o deja explícita que no hay cambio de estado si la transición es hacia el propio estado.
    Las flechas llevan una inscripción que se compone de varios elementos en el siguiente formato: evento [condición] / acción, donde evento es lo que dispara la transición (como pulsar un dígito de la calculadora), [condición] es la condición que debe cumplirse para que pueda tener lugar esa transición y acción son las operaciones que tienen lugar al darse esa transición.

Ahora lee el diagrama de estados con calma y podrás comprobar que recoge toda la información que hemos visto en la sección anterior. Como ves, resulta de mucha ayuda para reflejar el comportamiento de nuestro sistema sin necesidad de escribir tanto como yo lo he hecho.

Por simplicidad no he querido reflejar las transiciones que van desde cualquier estado hacia el de "inicio" cuando el usuario pulsa la tecla de reset o de "AC", cosa que puede suceder en cualquier momento y siempre tiene el mismo resultado que es almacenar el valor 0 en la calculadora y mostrarlo en la pantalla.

Diagrama de Estados - Calculadora en Python
Diagrama de estados de la calculadora

Clases de la calculadora

Bien, con el diagrama de estados hemos terminado de hacer parte del diseño de nuestra aplicación, en concreto hemos reflejado el comportamiento de la calculadora, pero solo del modelo, claro (de momento no hay nada acerca de la interfaz, recuerda que el modelo es independiente de la interfaz de usuario).

Toca ahora ver la parte estática del diseño, para lo cual haremos un diagrama de clases, en el que se reflejen las clases que tendremos que implementar. En cada una de estas clases, podremos ver los atributos y operaciones (funciones o métodos) necesarios para la correcta implementación del sistema.

El diagrama que te muestro a continuación es un diagrama en el que se aplica la estructura básica del patrón de diseño estado:

Cómo hacer una calculadora en Python – Diagrama de Clases
Diagrama de clases de la calculadora

Las dos clases superiores son las clases principales de nuestra sistema. La clase Calculadora representa, evidentemente, la calculadora y como tal tendrá operaciones definidas para introducir dígitos, operadores, calcular operación, obtener el valor actual (para poder ser mostrado por pantalla) y hacer reset. Todas estas operaciones serán públicas (símbolo + delante de la operación). Además, tendremos una operación que nos va a permitir cambiar la calculadora de un estado a otro (visbilidad de paquete indicada mediante el símbolo ~, pues a este método solo acceden el resto de clases del mismo paquete, que son las de los estados).

La otra clase principal es EstadoCalculadora. Esta es una clase abstracta (nombre de la clase en cursiva) que define la estructura común que deben tener todas las clases que representen los distintos estados. Si te fijas, las operaciones que define esta clase son iguales que las de la calculadora. Esto es así porque, como vimos, las funciones de la calculadora tendrán un comportamiento distinto según el estado en el que se encuentre. De esta manera, cada vez que invoquemos a una función de un objeto de la clase Calculadora, esta llamará la acción correspondiente de la clase estado (utilizando el concepto de ligadura dinámica), delegando en ella la operación. Este es el principio fundamental del patrón estado.

Por comodidad se ha creado una función cambiar_estado con visibilidad protegida (solo acceden los objetos de la propia jerarquía de clases, indicado con el símbolo #) para realizar de manera más comoda los cambios de estado y que delega en el método de cambio de estado de Calculadora.

El resto de clases representan a cada uno de los estados de la calculadora. Estas subclases heredan de la clase padre Estado. Podrás ver diferentes atributos en cada clase según la información que necesitamos guardar para el correcto funcionamiento del estado. También puedes ver que algunos de las funciones o métodos están redefinidos en las subclases pues alteran el comportamiento base. Otra particularidad es que, como algunos comportamientos son iguales en distintos estados, podemos volver a hacer uso de la herencia. Así, la clase EstadoIntroducirSegundoOperando puede heredar de EstadoIntroducirPrimerOperando pues operaciones como introducir_digito o valor_actual son iguales para ambas clases.

Fíjate que, como manda el principio de encapsulación del diseño orientado a objetos, todos los atributos son privados (símbolo - delante del nombre del atributo).

Ahora que todo ha cogido ha cogido forma y ya tenemos un diseño viable, podemos pasar a implementar la calculadora. ¡Fíjate en que no hemos visto una sola línea de código hasta el momento!

Implementación de la calculadora en Python

A la hora de implementar nuestra calculadora vamos a seguir teniendo en cuenta la independencia del modelo respecto de la interfaz de usuario. Así, este apartado se divide en dos, uno para la implementación del modelo y otra para el interfaz.

El modelo de la calculadora

Gracias al diseño que hemos hecho, realizar la implementación nos será mucho más sencillo. La estructura de clases la tenemos. El comportamiento según cada estado lo tenemos. Solo queda materializar todo esto en código.

Todo el código que te muestro a continuación lo vamos a ubicar en un fichero llamado calculadora_modelo.py.

Recibe contenido exclusivo de Python y programación y obtén gratis mi reto para ti: 30 Ejercicios de Python.
Todos los días te envío un consejo sobre trucos de Python, ejercicios, secretos, mentalidad, sintaxis, historias de motivación, etc.
Día que estás fuera, consejo sobre Python que te pierdes.
✅ Contenido de alto valor      ✅ Sin spam      ✅ Cancela cuando quieras


Antes de suscribirte consulta aquí la 
Información Básica sobre Protección de Datos. Responsable de datos: Juan Monroy Camafreita.
Finalidad de recogida y tratamiento de datos personales: enviarte boletín informativo de Python y comunicaciones comerciales.
Legitimación: tu consentimiento.
Destinatarios: no se ceden a terceros. Los datos se almacenan en los servidores de marketing (GetResponse).
Derechos: podrás ejercer tus derechos de acceso, rectificación, limitación y supresión de datos en info @ codigopiton.com así como presentar una reclamación ante una autoridad de control.
Más información: política de privacidad, encontrarás información adicional sobre la recopilación y el uso de tu información personal.

Solo vamos a necesitar importar la función sqrt de la librería math, para realizar raíces cuadradras, y ABC y abstractmethod de abc para poder implementar clases y métodos abstractos.

Antes de entrar en el desarrollo de las clases ya vistas, vamos a definir las operaciones matemáticas que realizará la calculadora. Además, agregaremos estas funciones a dos diccionarios, uno para los operadores binarios y otros para los operadores unarios. De esta manera, tendremos asociados a un símbolo una función matemática, para poder invocarlas de manera sencilla, como podrás comprobar más adelante.

Recuerda que las funciones en Python son objetos de primer orden y las podemos guardar en variables para poder invocarlas a través de las mismas.

from math import sqrt
from abc import ABC, abstractmethod

suma = lambda x, y: x + y
resta = lambda x, y: x - y
multiplicacion = lambda x, y: x * y
division = lambda x, y: x / y
cambio_de_signo = lambda x: -x
raiz = lambda x: sqrt(x)

binarios = {
    '+': suma,
    '-': resta,
    '*': multiplicacion,
    '/': division
}

unarios = {
    's': cambio_de_signo,
    'r': raiz
}

Fíjate en que he usado expresiones lambda para crear las funciones de las operaciones ya que son muy sencillas de realizar. En los diccionarios binarios y unarios he asociado cada una de las funciones creadas a un carácter representativo de la operación.

Pasamos a ver el código de la clase Calculadora:

class Calculadora:

    def __init__(self):
        self.reset()

    def reset(self):
        self._estado = EstadoInicial(self, 0)

    def introducir_digito(self, digito):
        self._estado.introducir_digito(digito)

    def introducir_operador(self, operador):
        try:
            self._estado.introducir_operador(operador)
        except:
            self.cambiar_estado(EstadoError(self))

    def calcular(self):
        try:
            self._estado.calcular()
        except:
            self.cambiar_estado(EstadoError(self))

    def valor_actual(self):
        return self._estado.valor_actual()

    def cambiar_estado(self, estado):
        self._estado = estado

Puedes observar que la clase calculadora parece un poco vacía, pues apenas tiene código. He aquí la magia del patrón estado, pues como los comportamientos se delegan a los estados solo tenemos que invocar a las funciones correspondientes del estado actual.

Podrás ver que no hay ninguna variable o atributo en esta clase, pues los datos que necesitemos se almacenarán en los propios estados como vimos en el diagrama de clases. Solo hay una excepción, que es el atributo _estado, que guarda una referencia al objeto estado en el que se encuentra la calculadora, necesario para poder delegar las operaciones en los estados.

Cuando se crea un objeto Calculadora se llama al método reset que coloca la calculadora en el estado incial. Para esto se guarda el objeto recién creado de la clase EstadoIncial en el atributo _estado.

La función cambiar_estado nos va a permitir que los estados cambien la calculadora de estado, es decir, que apliquen una transición, cuando haga falta.

El resto de operaciones de la calculadora delegan la operación en el estado. Puedes ver que, además, un par de funciones realizan un try except para capturar un error cuando suceda (por ejemplo, se introduce un operador no existente o se realiza una operación matemática no válida). Si sucede alguno de estos errores se pone la calculadora en el estado de error.

¿Hasta aquí se va comprendiendo todo? Eso espero...

Vamos a pasar a ver la implementación de los estados. Empezamos por la clase padre abstracta EstadoCalculadora:

class EstadoCalculadora(ABC):

    def __init__(self, calculadora):
        self._calculadora = calculadora

    def introducir_digito(self, digito):
        pass

    def introducir_operador(self, operador):
        pass

    def calcular(self):
        pass

    @abstractmethod
    def valor_actual(self):
        pass

    def cambiar_estado(self, nuevo_estado):
        self._calculadora.cambiar_estado(nuevo_estado)

Nuestra clase EstadoCalculadora extiende (y por tanto hereda) a la clase ABC de la librería abc para indicar que es abstracta (es decir, no se podrá instanciar directamente).

En la inicialización __init__ guardamos el objeto calculadora en una variable _calculadora para poder trabajar con ella desde los estados (la necesitamos para poder cambiar el estado).

Puedes ver que las operaciones de introducir dígitos, operadores o calcular no hacen nada. Esto es así porque esta clase no define ningún comportamiento base para estas funciones, pues son diferentes en cada estado.

El método valor_actual que nos permite obtener el valor que hay que mostrar en la pantalla de la calculadora tampoco hace nada, pero se define como abstracto. Esto fuerza a que cualquier subclase de EstadoCalculadora tenga que redefinir y por tanto dar una implementación a ese método, que siempre debe devolver un valor, sea cual sea el estado de la calculadora.

El resto de operaciones no son abstractas porque el comportamiento por defecto es "no hacer nada" y queremos que en algunos casos sea así. Por ejemplo, en el estado inicial, si pulsamos el botón de = para calcular, no se hace nada.

Vamos a ver la implementación de las clases estado concretas. Empezamos por la clase EstadoInicial. Ten muy en cuenta, para comprender este código, tanto la explicación de dicho estado que hicimos previamente como el diagrama de estados que puedes encontrar más arriba.

class EstadoInicial(EstadoCalculadora):

    def __init__(self, calculadora, numero):
        super().__init__(calculadora)
        self._numero = numero

    def introducir_digito(self, digito):
        self.cambiar_estado(EstadoIntroducirPrimerOperando(self._calculadora, digito))

    def introducir_operador(self, operador):
        if operador in unarios:
            self._numero = unarios[operador](self._numero)
        elif operador in binarios:
            self.cambiar_estado(EstadoResultadoParcial(self._calculadora, self._numero, self._numero, operador))
        else:
            raise ValueError(f'No existe el operador "{operador}".')

    def valor_actual(self):
        return self._numero

Como puedes ver, en este estado, cuando se crea el objeto, además de la calculadora se recibe un número que es el que se muestra por pantalla. Ese número se guarda en un atributo. Fíjate que debemos llamar al constructor de la clase padre ya que somos una subclase y la calculadora recibida se guarda en la clase padre: super().__init__(calculadora).

Veamos las operaciones:

  • La operación valor_actual solo devuelve el número almacenado.
  • La operación introducir_digito realiza el cambio de estado hacia EstadoIntroducirPrimerOperando como podemos ver en el diagrama de estados.
  • La operación introducir_operador funciona diferente según el operador sea unario o binario:
    • Si el operador es unario, como indica el diagrama de estados, calculamos el resultado y no hacemos cambio de estado porque permanecemos en el estado inicial. Para calcular la operación obtenemos la función correspondiente del diccionario de operadores unarios.
    • Si el operador es binario, se cambia al estado EstadoResultadoParcial guardando los datos del primer operando, el número a mostrar por pantalla y el operador. Nuevamente, como indica el diagrama de estados.

Creo que puedes empezar a apreciar cómo se implementan los diferentes estados. Cómo ves, es sencillo y directo gracias a todo el diseño previo que realizamos.

Vamos con el siguiente, EstadoIntroducirPrimerOperando:

class EstadoIntroducirPrimerOperando(EstadoCalculadora):

    def __init__(self, calculadora, digito):
        super().__init__(calculadora)
        self._digitos = []
        self.introducir_digito(digito)

    def introducir_digito(self, digito):
        digito = str(digito)
        if digito == '.':
            if '.' not in self._digitos:
                if len(self._digitos) == 0:
                    self._digitos.append('0')
                self._digitos.append(digito)
        else:
            if len(self._digitos) == 1 and self._digitos[0] == '0':
                self._digitos.clear()
            self._digitos.append(digito)

    def introducir_operador(self, operador):
        if operador in unarios:
            numero = self._get_numero()
            numero = unarios[operador](numero)
            self.cambiar_estado(EstadoInicial(self._calculadora, numero))
        elif operador in binarios:
            numero = self._get_numero()
            self.cambiar_estado(EstadoResultadoParcial(self._calculadora, numero, numero, operador))
        else:
            raise ValueError(f'No existe el operador "{operador}".')

    def _get_numero(self):
        return float(''.join(self._digitos))

    def valor_actual(self):
        return ''.join(self._digitos)

Esta clase es un poco más grande y compleja, pero vamos a desmenuzarla poco a poco para que no te quede ninguna duda.

Si te fijas en la operación __init__ he decidido almacenar los dígitos que va introduciendo el usuario como una lista de dígitos en lugar de ir creando el número a medida que llega un dígito. Esto facilita las diversas operaciones y comprobaciones que tenemos que hacer. En el momento en que necesite el número final puedo invocar a la función protegida _get_numero que transforma la lista de dígitos en un número real.

A la hora de introducir un dígito realizo una serie de comprobaciones para poder gestionar que solo exista un punto de separación de decimales y para que, en caso de introducir un punto cuando tenemos solo un cero en la calculadora, no sustituya este cero como pasaría si introducimos cualquier otro número, sino que añada el punto al final del cero. De la misma manera, si introduzco un cero cuando ya hay un solo cero, hay que asegurarse de que no se duplique.

Finalmente, a la hora de introducir un operador, vuelvo a distinguir entre los tipos de operadores:

  • Si el operador es unario, construyo el número final a partir de la lista de dígitos, aplico el operador unario y transito hacia el estado inicial.
  • Si el operador es binario, construyo el número final y transito hacia EstadoResultadoParcial indicando el primer operando, el número a mostrar por pantalla y el operador introducido.

Como ves, estas transiciones de estados también están recogidas en el diagrama de estados.

Veamos ahora el estado de resultado parcial:

class EstadoResultadoParcial(EstadoCalculadora):

    def __init__(self, calculadora, primer_operando, numero, operador):
        super().__init__(calculadora)
        self._primer_operando = primer_operando
        self._numero = numero
        self._operador = operador

    def introducir_digito(self, digito):
        self.cambiar_estado(EstadoIntroducirSegundoOperando(self._calculadora, self._primer_operando, self._operador, digito))

    def introducir_operador(self, operador):
        if operador in unarios:
            self._numero = unarios[operador](self._numero)
        elif operador in binarios:
            self._operador = operador
        else:
            raise ValueError(f'No existe el operador "{operador}".')

    def calcular(self):
        resultado = binarios[self._operador](self._primer_operando, self._numero)
        self.cambiar_estado(EstadoInicial(self._calculadora, resultado))

    def valor_actual(self):
        return self._numero

Pocas cosas nuevas hay que añadir aquí que no se hayan dicho para los estados anteriores. La mención destacable esta vez es para la función calcular. Como nos encontramos en un estado que está teniendo en cuenta una operación binaria y todavía no se ha introducido ningún segundo operando se hace la operación entre el primer operado y el número que se está mostrando por pantalla. Después se transita hacia el estado inicial indicando el resultado de la operación.

Vayamos ahora con EstadoIntroduciendoSegundoOperando:

class EstadoIntroducirSegundoOperando(EstadoIntroducirPrimerOperando):

    def __init__(self, calculadora, primer_operando, operador, digito):
        super().__init__(calculadora, digito)
        self._primer_operando = primer_operando
        self._operador = operador

    def introducir_operador(self, operador):
        if operador in unarios:
            resultado = unarios[operador](self._get_numero())
            self.cambiar_estado(EstadoResultadoParcial(self._calculadora, self._primer_operando, resultado, self._operador))
        elif operador in binarios:
            resultado = binarios[self._operador](self._primer_operando, self._get_numero())
            self.cambiar_estado(EstadoResultadoParcial(self._calculadora, resultado, resultado, operador))
        else:
            raise ValueError(f'No existe el operador "{operador}".')

    def calcular(self):
        resultado = binarios[self._operador](self._primer_operando, self._get_numero())
        self.cambiar_estado(EstadoInicial(self._calculadora, resultado))

Esta clase hereda de EstadoIntroducirPrimerOperando porque comparte algunos comportamientos y atributos con ella. De hecho, solo es necesario redefinir las operaciones de introducir operador y calcular, pues toda la gestión de dígitos para obtener un número se realiza igual que en la clase padre.

Nuevamente, con todas las explicaciones dadas previamente, y teniendo en cuenta el diagrama de estados, poco más hay que añadir respecto de este código.

Y, finalmente, vayamos con el estado de error:

class EstadoError(EstadoCalculadora):

    def __init__(self, calculadora):
        super().__init__(calculadora)

    def valor_actual(self):
        return '- Error -'

Es la clase más sencilla de todas, no redefine los comportamientos por defecto de la clase padre porque dichos comportamientos son no hacer nada, que es lo que se pretende en este estado. La única obligación es dar implementación a la función valor_actual, que en este caso devuelve el mensaje de error para mostrar en la pantalla de la calculadora.

¡Listo! Ya tenemos el modelo de nuestra calculadora. Y es totalmente funcional.

Gracias a haber aplicado el patrón de diseño estado, hemos podido repartir las responsabilidades de la calculadora en varias clases.

¿Te imaginas como hubiera quedado un código en el que no se usan estados? Puedes visualizar un montón de condicionales anidadas para gestionar los diferentes comportamientos que tiene que tener la calculadora según su estado. Te invito a hacer la prueba...

Como no tenemos interfaz gráfica todavía (en breve crearemos una muy chula) podemos hacer pruebas invocando directamente a los métodos de la calculadora y mostrando por consola el resultado que mostraría la pantalla en la calculadora. Esto nos servirá de pequeño test, pero ten en cuenta que en un proyecto real tendríamos que realizar, por lo menos, tests de unidad con todos los elementos de nuestro sistema.

Pues vamos a hacer una pequeña prueba y a ver qué pasa. Vamos a realizar la siguiente operación: -34 + √4cuyo resultado debe ser -32:

if __name__ == '__main__':

    calculadora = Calculadora()  # creamos la calculadora
    print(calculadora.valor_actual())  # mostramos el valor de la pantalla de la calculadora

    calculadora.introducir_digito('3')  # introducimos un 34
    print(calculadora.valor_actual())

    calculadora.introducir_digito('4')
    print(calculadora.valor_actual())

    calculadora.introducir_operador('s')  # cambiamos el 34 de signo
    print(calculadora.valor_actual())

    calculadora.introducir_operador('+')  # vamos a sumarle algo
    print(calculadora.valor_actual())

    calculadora.introducir_digito('4')  # introducimos un 4
    print(calculadora.valor_actual())

    calculadora.introducir_operador('r')  # calculamos la raíz cuadra del 4
    print(calculadora.valor_actual())

    calculadora.calcular()  # calculamos el resultado final
    print(calculadora.valor_actual())  # mostramos el resultado final

El resultado que obtenemos por pantalla es el siguiente:

0
3
34
-34.0
-34.0
4
2.0
-32.0

Como puedes ver, solo usamos la clase Calculadora y no las de los estados. A estas clases solo accede la clase Calculadora.

Otro aspecto a considerar es que el resultado es -32.0 y no lo que mostraría una calculadora que sería -32. Esto es debido a usar el tipo float para las operaciones. En cualquier caso, la forma de representar los números en pantalla es una responsabilidad de la capa vista, es decir, de la interfaz de usuario. La única responsabilidad del modelo es poder procesar la entrada del usuario y hacer correctamente las operaciones.

La interfaz de usuario de la calculadora con tkinter

Una vez con el modelo ya funcional y (más o menos) probado, podemos pasar a implementar una interfaz de usuario.

En esta ocasión utilizaremos la librería tkinter.

Para establecer correctamente la separación entre el modelo y la interfaz, todo el código relacionado con esta última lo escribiremos en un nuevo fichero .py, que será calculadora_gui.py.

Lo primero que haremos será importar las librerías y clases necesarias, que en este caso serán tkinter y nuestra clase Calculadora.

import tkinter as tk
from calculadora_modelo import Calculadora

El primer paso será crear una clase CalculadoraTK y proporcionar un constructor __init__, que se encargará de almacenar una instancia de Calculadora y de crear la ventana principal:

class CalculadoraTK():

    def __init__(self, calculadora):
        self.calculadora = calculadora
        self.ventana, self.pantalla = self._crear_interfaz()

Para crear el interfaz gráfico se invoca a función privada llamada _crear_interfaz, que vamos a definir e implementar de la siguiente manera:

    def _crear_interfaz(self):

        # creamos la ventana y le damos un título y un tamaño
        ventana = tk.Tk()
        ventana.title('La Calculadora de Código Pitón')
        ventana.geometry('700x700')

        # configuramos un grid (rejilla o cuadrícula) de 5 x 5 para ubicar los botones y pantalla
        for i in range(0, 5):
            ventana.rowconfigure(i, weight=10)
            ventana.columnconfigure(i, weight=10, uniform='grupo')

        # creamos y ubicamos la pantalla de la calculadora
        pantalla = tk.Label(ventana, text='0', anchor=tk.E, relief='sunken', background='#ffffff',
                            font=('Helvetica', 24), padx=15)
        pantalla.grid(column=0, row=0, columnspan=5, padx=5, pady=5, sticky='nesw')

        # creamos los botones de dígitos, de operadores binarios, de operadores unarios y el botón de reset/AC
        botones_digito = [tk.Button(ventana, text=digito, command=lambda x=digito: self.presionar_digito(x))
                          for digito in '.0123456789']
        botones_operador = [tk.Button(ventana, text=operador, command=lambda x=operador: self.presionar_operador(x))
                            for operador in '*/+-']
        boton_cambio_signo = tk.Button(ventana, text='±', command=lambda: self.presionar_operador('s'))
        boton_raiz = tk.Button(ventana, text='√', command=lambda: self.presionar_operador('r'))
        boton_calcular = tk.Button(ventana, text='=', command=self.presionar_calcular)
        boton_reset = tk.Button(ventana, text='AC', command=self.presionar_reset)

        # configuramos la fuente de todos los botones
        botones_todos = botones_digito + botones_operador + [boton_cambio_signo, boton_raiz, boton_calcular, boton_reset]
        for boton in botones_todos:
            boton.config(font=('Helvetica', 17))

        # ubicamos los botones en la cuadrícula
        botones_cuadricula = botones_digito[0:2][::-1] + [boton_cambio_signo] + botones_digito[2:]
        for fila in range(0, 4):
            for columna in range(0, 3):
                botones_cuadricula[3 * (3 - fila) + columna].grid(row=fila + 1, column=columna, padx=5, pady=5,
                                                                  sticky='nesw')

        boton_raiz.grid(row=1, column=3, padx=5, pady=5, sticky='nesw')
        boton_reset.grid(row=1, column=4, padx=5, pady=5, sticky='nesw')

        iterador = iter(botones_operador[0:4])
        for fila in (2, 3):
            for columna in (3, 4):
                next(iterador).grid(row=fila, column=columna, padx=5, pady=5, sticky='nesw')

        boton_calcular.grid(row=4, column=3, padx=5, pady=5, sticky='nesw', columnspan=2)
        
        # devolvemos una referencia a la ventana y otra a la pantalla
        return ventana, pantalla

Este código es un poco largo y tal vez algo complejo si no conoces algo tkinter (puedes ver otro ejemplo de uso de esta librería en este otro artículo donde te cuento cómo hacer un menú de usuario en Python).

Vamos a ir desemenuzándolo poco a poco para que no te pierdas, y me pararé en algunos detalles que a primera vista pueden parecer un poco raros.

En las líneas 4 a 6 creamos una ventana, y le asignamos un título y un tamaño.

En las líneas 9 a 11 creamos un grid (o rejilla o cuadrícula) que nos sirve para ubicar elementos por sus coordenadas dentro del grid. Llamamos a rowconfigure y a columconfigure 5 veces, una por cada fila y columna, asignándole de peso 10 a los elementos (para que todos tiendan a ocupar lo mismo). Adicionalmente indicamos el parámetro uniform='grupo' para que todos los elementos dentro de grupo ocupen exactamente lo mismo (ancho uniforme).

En la línea 14 creamos la pantalla como un tk.Label con determinadas configuraciones para conseguir el aspecto deseado:

  • ventana, que será el elemento que lo contiene.
  • text='0', que indicará el valor inicial que se mostrará en la pantalla.
  • anchor=tk.E, sirve para alinear el texto a la derecha (al este) de la pantalla.
  • relief='sunken', para que la pantalla presente aspecto de "hundida".
  • background='#ffffff', para poner fondo blanco.
  • font=('Helvetica', 24), para configurar la fuente.
  • padx=15, para configurar un margen horizontal de 15 píxeles.

Una vez tenemos la pantalla configurada la ubicamos, en la línea 15, en la fila 0 y columna 0 del grid, indicando que ocupe 5 columnas (columnspan=5), es decir, toda la parte superior de la venta. Además indicamos márgenes de 5 píxeles e indicamos que se estire para pegarse a todos los bordes con sticky='nesw'.

Tras configurar y ubicar la pantalla pasamos a crear y configurar los botones.

En la línea 19 creamos una lista con todos los botones de dígito. Para esto hacemos uso de la comprensión de listas, con la que creamos un tk.Button para cada uno de los elementos de la cadena de texto .0123456789 (recuerda que el punto de separador de decimales lo consideramos como un dígito).

A la hora de crear un botón le indicamos 3 parámetros:

  • ventana, que es el componente que lo contiene.
  • text=digito, que es el texto que mostrara el botón.
  • command=lambda x=digito: self.presionar_digito(x) que nos sirve para indicar el comando (echa un ojo a cómo implementar el patrón de diseño comando en Python) o la función que debe ejecutarse al presiona el botón. Aquí la función que vamos a invocar será una función anónima (con funciones lambda) que invocará a presionar_digito con el dígito correspondiente.

Fíjate en que la expresión lambda está creada de una forma un poco extraña, pues en el parámetro x indicamos x=digito. Esto es necesario por motivo de una característica de Python llamada late binding closures, que se puede traducir como vinculación de cierres tardía, donde un cierre es una función que recuerda valores del contexto en el que se encuentra incluso aunque no estén presentes en memoria. De no indicar que el valor por defecto para x es digito se asignaría siempre a cualquier llamada de presionar_digito el último valor de la cadena .0123456789, es decir, 9.

Este concepto es un poco avanzado y difícil de entender si estás comenzando con Python y se escapa del alcance de este artículo así que no vamos a profundizar en él.

Con los botones de dígitos creados pasamos a crear, en la línea 21, los botones de operadores binarios de una forma semejante.

En las líneas 23 a 26 terminamos de crear los botones que faltan, que son los de los operadores unarios así como el botón para calcular y el de reset.

En las líneas 29 a 31 terminamos de configurar todos los botones para indicar la fuente a usar para sus etiquetas.

En las líneas 34 a 48 ubicamos todos los botones en la cuadrícula de la ventana y terminamos de configurarlos. Para evitar repetir código (y cumplir el pricipio de diseño DRY, don't repeat youself, o "no te repitas") realizo un par de bucles para ubicar los botones y configurarlos. Aprovecho para indicar los márgenes que deben de tener los botones.

Usar estos bucles para evitar repetir codigo también hace más difícil de leer el código y menos explicito, contradiciendo un poco el código zen de Python recogido en el PEP 20.

Queda a tu criterio crear los botones de esta manera o ir añadiendo uno por uno a la rejilla sin realizar estos bucles.

Finalmente, en la línea 51 devolvemos referencias a la ventana creada y a su pantalla.

Una vez que tenemos el código para crear el interfaz vamos a ver las funciones que nos faltan para que la interfaz sea funcional. Lo primero es contar con una función para actualizar el valor de la pantalla:

    def actualizar_pantalla(self, valor, eliminar_ceros_derecha=False):
        valor_pantalla = str(valor)
        if eliminar_ceros_derecha:
            valor_pantalla = valor_pantalla.rstrip('0').rstrip('.')
        self.pantalla.configure(text=valor_pantalla)

Es una función simple que recibe el valor a mostrar por pantalla y un parámetro en el que indicamos si queremos eliminar los ceros a la derecha en valores con decimales del estilo 0.500. Para mostrar algo en el Label de la pantalla basta con llamar su función configure indicando el valor que queremos mostrar en el parámetro text.

Ya vamos acabando. ¿Recuerdas esas funciones de presionar_digito, presionar_operador, etc., que vimos cuando creábamos los botones? Es ahora cuando vemos su implementación:

    def presionar_digito(self, digito):
        calculadora.introducir_digito(digito)
        self.actualizar_pantalla(calculadora.valor_actual())

    def presionar_operador(self, operador):
        calculadora.introducir_operador(operador)
        self.actualizar_pantalla(calculadora.valor_actual(), True)

    def presionar_calcular(self):
        calculadora.calcular()
        self.actualizar_pantalla(calculadora.valor_actual(), True)

    def presionar_reset(self):
        calculadora.reset()
        self.actualizar_pantalla(calculadora.valor_actual())

¡Estas son funciones importantes! Es aquí donde se establece la conexión entre la interfaz y el modelo. Esto es lo que se conoce, con matices, como controlador.

Recordemos que estas funciones se ejecutan cuando se presionan los botones de la interfaz. Cada una de estas funciones hace la llamada correspondiente al modelo (por ejemplo, en el caso de presionar_digito se llama a la función introducir_digito de Calculadora).

Tras una llamada a una función de Calculadora es posible que necesitemos actualizar la pantalla de la interfaz, de forma que lo que hay que hacer es tomar el valor que almacene la calculadora en ese momento, invocando a la función valor_actual y, después, ya podemos llamar a la función que actualiza nuestra pantalla.

¡Y listo! Solo queda ya proporcionar otra función para mostrar la ventana después de creada:

    def mostrar(self):
        self.ventana.mainloop()

Y por fin, nos acercamos al final del artículo, que no está quedando corto precisamente.

Resultado

Solo nos queda crear una instancia de la calculadora, crear una interfaz y mostrar la ventana, que podemos hacer en un pequeña función main como esta:

if __name__ == '__main__':

    calculadora = Calculadora()  # creamos el modelo
    interfaz_calculadora = CalculadoraTK(calculadora)  # creamos la interfaz
    interfaz_calculadora.mostrar()  # mostramos la interfaz

Nada más ejecutar esas líneas se mostrará el interfaz de nuestra calculadora, que ya está lista para ser usada.

Aquí te dejo una imagen animada con algunas operaciones para que compruebes el resultado de todo nuestro esfuerzo.

Ejemplo animado del uso de la calculadora
Animación del uso de la calculadora

Consideraciones finales

Si has llegado hasta aquí, solo me queda darte la enhorabuena, pues el artículo es largo y denso. Si te ha gustado, te animo a que lo revises pues tiene mucha información y contenido de valor, tanto de Python, como de programación en general y de diseño y arquitectura de software.

Dos aspectos claves que quiero remarcar aquí son los siguientes:

  1. Es importante separar e independizar la interfaz del modelo. Esto nos permitiría cambiar o mejorar la interfaz sin necesidad de tocar el código del modelo. Además de reducir el acoplamiento y mejorar la cohesión y el reparto de responsabilidades.
  2. El patrón estado se utiliza cuando tenemos una clase o componente que debe cambiar su comportamiento en función de los valores de sus atributos, es decir, de su estado. Estúdialo y tenlo en cuenta para el futuro. Lo más complicado aquí es darse cuenta de que el problema que tenemos que resolver plantea esta situación. Una vez identificado, aplicar el patrón resultará sencillo. Nuevamente, quiero reiterar mi recomendación del libro Patrones de Diseño. Si no lo tienes, hazte con una copia. ¡Fundamental!

Algunas últimas cosas que hay que tener en cuenta:

  • La solución que he propuesto en este artículo para la implementación de una calculadora en Python no es la única y, probablemente, no sea la mejor. Nunca hay una sola forma correcta de hacer las cosas.
  • Los comportamientos para cada estado de la calculadora pueden diferir de los que yo planteo aquí. No todas las calculadoras funcionan igual. Y no todos los requisitos son los mismos.
  • El buen diseño de esta solución permite añadir operaciones binarias o unarias de manera sencilla. Si tuvieras que hacerlo... ¿por dónde empezarías? ¿qué cambios implicaría?
  • Se pueden considerar más estados u otros diferentes en función de lo que se quiera lograr. Es necesario estudiar el problema para hacer un correcto planteamiento y lograr un diseño que satisfaga todos los requisitos.
  • También es posible añadir otro tipo de operaciones con la calculadora, como las operaciones de memoria (Min, MR, M+ y M-).
  • Se podrían incorporar operaciones ternarias, como por ejemplo ab/c. En tus manos queda pensar cómo se resolvería.

Como comentario final, fíjate en que no se ha tratado el problema de la representación de los números con decimales en un ordenador. Si realizamos la operación 0.1 + 0.2 no obtendremos 0.3 como se cabría esperar. Lo probamos en una consola de Python:

>>> 0.1 + 0.2
0.30000000000000004

Este problema se detalla en esta página con URL curiosa: https://0.30000000000000004.com/. Una manera de solucionarlo en Python sería trabajar con la clase Decimal en lugar de usar el tipo float. Ten esto muy en cuenta para aquellos proyectos en los que la precisión sea importante.

Y bueno, hasta aquí el artículo, espero que lo hayas disfrutado y, sobre todo, que hayas aprendido alguna cosa.

¡Feliz programación!

Si te ha gustado lo que has leído, ESTO TE VA A ENCANTAR...

Te envío todos los días un consejo para que cada día seas mejor en Python.
Puede ser un truco, una píldora, un tip, una historia de mentalidad o motivación, cualquier cosa...
Siempre sobre Python y programación.
Más de 2500 personas como tú los reciben cada día.

Día que estás fuera, consejo sobre Python que te pierdes.
Abajo te suscribes a la lista de correo y te regalo un montón de cosas: La Hoja de Referencia de Python, el Tutorial Interactivo, 30 Ejercicios de Python y la Guía de ChatGPT y Python. By the face.

✅ Contenido exclusivo de alto valor      ✅ Sin spam      ✅ Cancela cuando quieras



Antes de suscribirte consulta aquí la 
Información Básica sobre Protección de Datos. Responsable de datos: Juan Monroy Camafreita.
Finalidad de recogida y tratamiento de datos personales: enviarte boletín informativo de Python y comunicaciones comerciales.
Legitimación: tu consentimiento.
Destinatarios: no se ceden a terceros. Los datos se almacenan en los servidores de marketing (GetResponse).
Derechos: podrás ejercer tus derechos de acceso, rectificación, limitación y supresión de datos en info @ codigopiton.com así como presentar una reclamación ante una autoridad de control.
Más información: política de privacidad, encontrarás información adicional sobre la recopilación y el uso de tu información personal.
30 ejercicios de Python

Mira, te envío 30 ejercicios de Python para practicar todo el Python que necesitas saber para programar con soltura.

Son variados y de distintas dificultades. Tendrás un poco de todo: variables, condicionales, funciones, ficheros, listas, diccionarios, etc.

Y de regalo: La Hoja de Referencia de Python. Pon tu email aquí abajo ahora y te los envío.

Información Básica de Protección de Datos. Responsable de datos: Juan Monroy Camafreita.
Finalidad de recogida y tratamiento de datos personales: enviarte boletín informativo de Python y ofertas de productos y servicios propios.
Legitimación: tu consentimiento.
Destinatarios: no se ceden a terceros. Los datos se almacenan en los servidores de marketing (GetResponse).
Derechos: podrás ejercer tus derechos de acceso, rectificación, limitación y supresión de datos en info @ codigopiton.com así como presentar una reclamación ante una autoridad de control.
Más información: política de privacidad, encontrarás información adicional sobre la recopilación y el uso de tu información personal.