![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Los lenguajes de programación orientados a objetos permiten a los programadores crear nuevos tipos de datos que se comporten de manera muy parecida a los tipos de datos nativos. Exploraremos esta posibilidad construyendo una clase Fraccion que funcione de manera muy similar a los tipos numéricos nativos, enteros, enteros largos y flotantes.
Las fracciones, también conocidas como números racionales, son valores que pueden expresrse como la proporción entre dos números enteros, tal como 5/6. Al número superior se se le llama numerador y al inferior se se le llama denominador.
Comenzamos definiendo la clase Fraccion con un método de inicialización que nos surta de un numerador y un demonimador enteros:
class Fraccion:
def __init__(self, numerador, denominador=1):
self.numerador = numerador
self.denominador = denominador
El denominador es opcional. Una Fraccion con un sólo parámetro representa un número entero. Si el numerador es n, construimos la fracción n/1.
El siguente paso es escribir un método __str__ para que imprima las fracciones de forma que tenga sentido. La forma natural de hacerlo es "numerador/denominador":
class Fraccion:
...
def __str__(self):
return "%d/%d" % (self.numerador, self.denominador)
Para probar lo que tenemos hasta ahora, lo ponemos en un fichero llamado Fraccion.py y lo importamos desde el intérprete de Python. Entonces creamos un objeto fracción y lo imprimimos.
>>> from Fraccion import fraccion
>>> mortadela = Fraccion(5,6)
>>> print "La fracción es", mortadela
La fracción es 5/6
Como siempre, la función print invoca implícitamente al método __str__.
Nos gustaría poder aplicar las operaciones normales de suma, resta, multiplicación y división a las fracciones. Para ello, podemos sobrecargar los operadores matemáticos para los objetos de clase Fraccion.
Comenzaremos con la multiplicación porque es la más fácil de implementar. Para multiplicar dos fraciones, creamos una nueva fracción cuyo numerador es el producto de los numeradores de los operandos y cuyo denominador es el producto de los denominadores de los operandos. __mul__ es el nombre que Python utiliza para el método que sobrecarga al operador *:
class Fraccion:
...
def __mul__(self, otro):
return Fraccion(self.numerador*otro.numerador,
self.denominador*otro.denominador)
Podemos probar este método calculando el producto de dos fracciones:
>>> print Fraccion(5,6) * Fraccion(3,4)
15/24
Funciona, pero ¡podemos hacerlo mejor! Podemos ampliar el método para manejar la multiplicación por un entero. Usamos la función type para ver si otro es un entero y convertirlo en una fracción en tal caso.
class Fraccion:
...
def __mul__(self, otro):
if type(otro) == type(5):
otro = Fraccion(otro)
return Fraccion(self.numerador * otro.numerador,
self.denominador * otro.denominador)
Ahora funciona la multiplicación para fracciones y enteros, pero sólo si la fracción es el operando de la izquierda.
>>> print Fraccion(5,6) * 4
20/6
>>> print 4 * Fraccion(5,6)
TypeError: __mul__ nor __rmul__ defined for these operands
Para evaluar un operador binario como la multiplicación, Python comprueba primero el operando de la izquierda para ver si proporciona un método __mul__ que soporte el tipo del segundo operando. En este caso, el operador nativo de multiplicación del entero no soporta fracciones.
Después, Python comprueba el segundo operando para ver si provee un método __rmul__ que soporte el tipo del primer operando. En este caso, no hemos provisto el método __rmul__, por lo que falla.
Por otra parte, hay una forma sencilla de obtener __rmul__:
class Fraccion:
...
__rmul__ = __mul__
Esta asignación hace que el método __rmul__ sea el mismo que __mul__. Si ahora evaluamos 4 * Fraccion(5,6), Python llamará al método __rmul__ del objeto Fraccion y le pasará 4 como parámetro:
>>> print 4 * Fraccion(5,6)
20/6
Dado que __rmul__ es lo mismo que __mul__, y __mul__ puede manejar un parámetro entero, ya está hecho.
La suma es más complicada que la multiplicación, pero aún es llevadera. La suma de a/b y c/d es la fracción (a*d+c*b)/b*d.
Usando como modelo el código de la multiplicación, podemos escribir __add__ y __radd__:
class Fraccion:
...
def __add__(self, otro):
if type(otro) == type(5):
otro = Fraccion(otro)
return Fraccion(self.numerador * otro.denominador +
self.denominador * otro.numerador,
self.denominador * otro.denominador)
__radd__ = __add__
Podemos probar estos métodos con Fracciones y enteros.
>>> print Fraccion(5,6) + Fraccion(5,6)
60/36
>>> print Fraccion(5,6) + 3
23/6
>>> print 2 + Fraccion(5,6)
17/6
Los dos primeros ejemplos llaman a __add__; el último llama a __radd__.
En el ejemplo anterior, computamos la suma de 5/6 + 5/6 y obtuvimos 60/36. Es correcto, pero no es la mejor forma de representar la respuesta. Para reducir la fracción a su expresión más simple, hemos de dividir el numerador y el denominador por el máximo común divisor (MCD) de ambos, que es 12. El resultado sería 5/3.
En general, siempre que creamos un nuevo objeto Fraccion, deberíamos reducirlo dividiendo el numerador y el denominador por el MCD de ambos. Si la fracción ya está reducida, el MCD es 1.
Euclides de Alejandría (aprox. 325--265 a.~C.) prensentó un algoritmo para encontrar el MCD de dos números entermos m y n:
Si n divide a m sin resto, entonces n es el MCD. De lo contrario, el MCD es el MCD de n y el resto de dividir m entre n.
Esta definición recursiva puede expresarse concisamente como una función:
def mcd (m, n):
if m % n == 0:
return n
else:
return mcd(n, m%n)
En la primera línea del cuerpo, usamos el operador de módulo para comprobar la divisibilidad. En la última línea, lo usamos para calcular el resto de la división.
Dado que todas las operaciones que hemos escrito creaban un nuevo objeto Fraccion para devolver el resultado, podemos reducir todos los resultados modificando el método de inicialización.
class Fraccion:
def __init__(self, numerador, denominador=1):
m = mcd (numerador, denominador)
self.numerador = numerador / m
self.denominador = denominador / m
Ahora siempre que creemos una Fraccion quedará reducida a su forma canónica:
>>> Fraccion(100,-36)
-25/9
Una característica estupenda de mcd es que si la fracción es negativa, el signo menos siempre se trasladará al numerador.
Supongamos que tenemos dos objetos Fraccion, a y b, y evaluamos a == b. La implemetación por defecto de == comprueba la igualdad superficial, por lo que sólo devuelve true si a y b son el mismo objeto.
Queremos más bien devolver verdadero si a y b tienen el mismo valor
eso es, igualdad en profundidad.
Hemos de enseñar a las fracciones cómo compararse entre sí. Como vimos en la Sección 15.4, podemos sobrecargar todos los operadores de comparación de una vez proporcionando un método __cmp__.
Por convenio, el método __cmp__ devuelve un número negativo si self es menor que otro, zero si son lo mismo, y un número positivo si self es mayor que otro.
La forma más simple de comparar dos fracciones es la multipicación cruzada. Si a/b > c/d, entonces ad > bc. Con esto en mente, aquí está el código para __cmp__:
class Fraccion:
...
def __cmp__(self, otro):
dif = (self.numerador * otro.denominador -
otro.numerador * self.denominador)
return dif
Si self es mayor que otro, entonces dif será positivo. Si otro is mayor, entonces dif será ngativo. Si son iguales, dif es cero.
Por supuesto, aún no hemos terminado. Todavía hemos de implementar la resta sobrecargando __sub__ y la división sobrecargando __div__.
Una manera de manejar estas operaciones es implementar la negación sobrecargando __neg__ y la inversión sobrecargando __invert__. Entonces podemos restar negando el segundo operando y sumando, y podemos dividir invirtiendo el segundo operando y multiplicando.
Luego, hemos de suministrar los métodos __rsub__ y __rdiv__. Desgraciadamente, no podemos usar el mismo truco que usamos para la suma y la multiplicación, porque la resta y la división no son conmutativas. No podemos igualar __rsub__y __rdiv__ a __sub__ y __div__. En estas operaciones, el orden de los operandos tiene importancia.
Para manejar la negación unitaria, que es el uso del signo menos con un único operando, sobrecargamos el método __neg__.
Podemos computar potencias sobrecargando __pow__, pero la implementación tiene truco. Si el exponente no es un número entero podría no ser posible representar el resultado como una Fraccion. Por ejemplo, Fraccion(2) ** Fraccion(1,2) es la raiz cuadrada de 2, que es un número irracional (no se puede representar como una fracción). Por lo tanto, no es fácil escribir la versión más general de __pow__.
Existe otra extensión a la clase Fraccion que cabría considerar. Hasta ahora, hemos asumido que el numerador y el denominador son enteros. Podríamos considerar la posibilidad de pertimirles que sean enteros largos.
Como ejercicio, termine la implementación de la clase Fraccion de forma que pueda manejar resta, división, exponenciación y enteros largos como numerador y denominador.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |