![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Los lenguajes de programación orientados a objetos permiten a los programadores crear nuevos tipos de datos que se comportan en buena medida como los tipos ya incorporados. Estudiaremos esta característica elaborando una clase Fracción que funciona de forma muy parecida a los tipos numéricos ya integrados: números enteros, números de muchas cifras y números con decimales.
Las fracciones, también conocidas como números racionales, son valores que se pueden expresar como el cociente de números totales, como por ejemplo 5/6. Al número superior se le conoce como numerador y al número inferior, como denominador.
Empezaremos por definir una clase Fracción con un método de inicialización que establece el numerador y el denominador como números enteros:
class Fraccion:
def __init__(self, numerador, denominador=1):
self.numerador = numerador
self.denominador = denominador
El denominador es opcional. Una Fracción de sólo un parámetro representa a un número total. Si el numerador es n, construimos la Fracción n/1.
El siguiente paso es escribir un método __str__ que muestre las fracciones de manera que tengan sentido. La fórmula "numerador/denominador" es la más apropiada:
class Fracción:
...
def __str__(self):
return "%d/%d" % (self.numerador, self.denominador)
Para comprobar lo que hemos hecho hasta ahora, lo ponemos en un archivo al que llamamos Fraccion.py y lo importamos desde el intérprete de Python. Luego creamos un objeto fracción y lo imprimimos.
>>> from Fraccion import fraccion
>>> spam = Fraccion(5,6)
>>> print "La fracción es", spam
La fracción es 5/6
Como siempre, la orden print invoca ímplicitamente el método __str__.
¿Y si quisiéramos aplicar las operaciones normales de suma, resta, multiplicación y división a las fracciones? Para hacerlo, podemos sobrecargar los operadores matemáticos para objetos Fracción.
Empezaremos con la multiplicación porque es la más fácil de llevar a cabo. Para multiplicar fracciones, creamos una nueva fracción con un numerador que es el producto de los numeradores originales y un denominador que es el producto de los denominadores originales. __mul__ es el nombre que Python da a un método de sobrecarga del operador * :
class Fraccion:
...
def __mul__(self, objeto):
return Fraccion(self.numerador*objeto.numerador,
self.denominator*objeto.denominador)
Podemos comprobar este método computando el producto de dos fracciones:
>>> print Fraccion(5,6) * Fraccion(3,4)
15/24
Funciona, pero aún se puede mejorar. Podemos ampliar este método a la multiplicación de un número entero. Usamos la función type para comprobar si otro es un número entero y en ese caso convertirlo en una fracción.
class Fraccion:
...
def __mul__(self, otro):
if type(otro) == type(5):
otro = Fraccion(otro)
return Fraccion(self.numerador * otro.numerador,
self.denominador * otro.denominador)
Ya podemos multiplicar fracciones y enteros, pero sólo si la fracción se deja como operando:
>>> 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 primero revisa el operando de la izquierda para ver si nos da un __mul__ que sea compatible con el tipo del segundo operando. En este caso, el operador integrado no es compatible con las fracciones.
Luego, Python revisa el operando de la derecha para ver si nos da un método __rmul__ que sea compatible con el primer tipo. En este caso no hemos hecho un __rmul__ y por eso falla.
Pero hay una manera sencilla de hacer un __rmul__:
class Fraccion:
...
__rmul__ = __mul__
Esta asignación nos dice que __rmul__ es lo mismo que __mul__. Si ahora evaluamos 4 * Fraccion (5,6), Python invoca a __rmul__ en el objeto de Fraccion y usa 4 como parámetro.
>>> print Fraccion(5,6) * Fraccion(3,4)
20/6
Dado que __rmul__ es lo mismo que __mul__ y que __mul__ puede operar con parámetros de enteros, ya lo tenemos.
La suma es más complicada que la multiplicación, pero no es muy difícil. La suma de a/b y c/d es la fracción (a*d+c*b)/b*d.
Si tomamos la multiplicación como modelo, 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.denominator)
__radd__ = __add__
Podemos probar estos métodos con Fraccion y números 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 invocan __add__; el último invoca __radd__.
En el ejemplo anterior, computamos la suma 5/6 + 5/6 y nos dio 60/36. Aunque es correcto, no es la mejor manera de representar la respuesta. Para reducir la fracción a su expresión más simple, tenermos que dividir el numerador y el denominador por su máximo común divisor (mcd), que es 12. El resultado es5/3.
Por lo general, cada vez que creamos un nuevo objeto de Fracción debemos reducirlo dividiendo el numerador y el denominador por su mcd. Si la fracción ya está reducida, el mcd es 1.
Euclides de Alejandría (c. 325--265 a.C.) presentó un algoritmo para encontrar el mcd de dos números enterosm y n:
Si n divide a m uniformemente, entonces n es el mcd. Si no, el mcd es el mcd de n y el resto de m dividido por n.
Esta definición recursiva se puede expresar de manera concisa 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 del mensaje, usamos el módulo operador para comprobar la divisibilidad. En la última línea, lo usamos para computar el resto tras la división.
Dado que todas las operaciones que hemos escrito crean una nueva Fracciónpara 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 cada vez que creemos una Fraccion, se reduce a su forma más simple:
>>> Fraccion(100,-36)
-25/9
Una curiosidad del mcd es que si la fracción es negativa, el signo menos siempre se coloca en el numerador.
Supongamos que tenemos dos objetos de Fraccion, a y b, y queremos evaluar a == b. Por defecto, las comprobaciones de igualdad superficial ==sólo responden positivamente si ayb son el mismo objeto.
Pero es más probable que nos interese saber si a yb tienen el mismo valor. Es decir, si hay igualdad profunda.
Tenemos que enseñar a las fracciones a autocompararse. Tal y como vimos en la Sección 15.4, podemos sobrecargar todos los operadores de comparación, estableciendo un método __cmp__ .
Está establecido que el método __cmp__ nos de un número negativo si self es menor que otro, cero si son el mismo, y un número positivo si self es mayor que otro.
La forma más sencilla de comparar fracciones es la multiplicación cruzada. Si a/b > c/d, entonces ad > bc. Teniendo esto presente, éste es el código para __cmp__:
class Fraccion:
...
def __cmp__(self, otro):
diff = (self.numerador * otro.denominador -
otro.numerator * self.denominador)
return diff
Si self es mayor que otro, entonces diff será positivo. Si otro es mayor, entonces diff será negativo. Si son el mismo, diff es cero.
Por supuesto, aún no hemos acabado. Aún tenemos que implementar la resta imponiendo __sub__ y la división, imponiendo __div__.
Una forma de tratar estas operaciones es implementar la negación imponiendo__neg__ y la inversión, imponiendo __invert__. Luego, podremos restar negando el segundo operando y haciendo la suma, y podremos también dividir invirtiendo el segundo operando y haciendo la multiplicación.
A continuación, tendremos que efectuar __rsub__ y __rdiv__. Desgraciadamente, no podemos hacer el mismo truco que usamos para la multiplicación y la suma porque la resta y la división no son conmutativas. Podemos simplemente establecer que __rsub__ y __rdiv__ son lo mismo que __sub__ y __div__. En estas operaciones, el orden de los operandos afecta al resultado.
Para efectuar la negación unaria , que es el uso del signo menos con un solo operando, imponemos __neg__.
Podemos computar las potencias imponiendo __pow__, pero ponerlo en práctica requiere un ligero truco. Si el exponente es un número entero, puede que no sea posible representar el resultado como una Fracción. Por ejemplo, Fracción(2) ** Fracción(1,2)es la raíz cuadrada de 2, que es un número irracional (se puede representar como una fracción). Por eso no es fácil escribir la versión más general de __pow__.
Hay otra extensión de la clase Fraccion que podría interesarte. Hasta ahora hemos asumido que el numerador y el denominador son números enteros. Pero podríamos también hacer que fueran número enteros largos.
Para practicar, termina la implementación de la clase Fraccion de manera que pueda operar con la resta, división, exponenciación y números enteros largos como numeradores y denominadores.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |