Реализация класса Fraction рациональных чисел

В этом листке мы реализуем класс рациональных чисел подобно тому, что уже есть в стандартной библиотеке питона. Только мы реализуем не все его возможности, а достаточную для разумной работы часть. Тем самым мы научимся перегружать часть возможных операторов. А после этого прикрутим к нашим рациональным числам новые фичи.

Итак, рациональное число. Очевидно, что внутри нужно хранить целые числа — числитель и знаменатель. Это будут внутренние атрибуты. Чтобы перегрузить стандартные операторы, нужно определить внутри класса «волшебные» (magic) функции (вернее, методы). Они все имеют вид __ZZZZ__, то есть окружены двойными подчёркиваниями. Все встроенные в питон методы с таким именем — «волшебные», они используются при арифметических операциях, сравнениях, приведении типов, итерации и т.д. Ничего не мешает создать атрибут или метод с подобным именем, только он уже не будет «волшебным». Поэтому использовать такие имена не принято.

Вычитание: __sub__, __rsub__ и __isub__, в чём разница?

Оказывается, для переопределения вычитания есть целых три магических метода: __sub__, __rsub__ и __isub__. Работает это следующим образом.

Для удобства введём обозначение: fr, fr1, fr2 — это будут рациональные числа, экземпляры нашего нового класса Fraction. Переменные m, n и т.п. — это строго целые числа. Пусть у нас есть такой код:

class Fraction:
    ...
    def __sub__(self, other):
        ...
    def __rsub__(self, other):
        ...

fr1 = Fraction(1, 3)
fr2 = Fraction(3, 4)
n = 5
fr3 = fr1 - fr2  # На самом деле fr3 = Fraction.__sub__(fr1, fr2)
fr4 = fr1 - n    # На самом деле fr4 = Fraction.__sub__(fr1, n)
fr5 = n - fr2    # На самом деле fr5 = Fraction.__rsub__(fr2, n)

Строка fr1 - fr2 на самом деле превращается в сначала в fr1.__sub__(fr2), а затем — в Fraction.__sub__(fr1, fr2).
Строка fr1 - n сначала превращается в fr1.__sub__(n), а затем — в Fraction.__sub__(fr1, n). Здесь вроде бы никаких отличий, только вторым параметром в метод __sub__ придёт целое цисло, а не рациональное.
А вот с n - fr2 более хитрая магия. Подобно прошлому разу это превращается в n.__sub__(fr2). Тип переменной n — это int, поэтому дальше это превращается в int.__sub__(fr1, n). Но тот метод __sub__, что реализован для целых чисел, ничего не знает про наши новые рациональные. Он не знает, где там числитель и знаменатель, и даже не знает, что эта штука вообще по смыслу число. Поэтому вместо разности возвращается специальная константа NotImplemented. В этом случае питон понимает, что левый операнд (целое число) не умеет вычитать из себя правый операнд (рациональное). Тогда он обращается ко второму операнду: «Эй, вычти себя из целого, он говорит, что сам не умеет». В результате происходит вызов fr2.__rsub__(n), который в свою очередь превращается в Fraction.__rsub__(fr2, n).

То есть если целые числа не умеют вычитать из себя наши новые рациональные (а они, конечно же не умеют, откуда им?), то мы сами должны научить наши новые рациональные числа вычитать себя из целых (и из любых других типов, если нам это надо). И это самое «научить» реализуется в магических __rZZZ__ методах.

А с __isub__ всё гораздо проще. Этот метод вызывается, когда мы пишем fr1 -= n. Если этот метод в классе не реализован, то код fr1 -= n интерпретируется просто как fr1 = fr1 - n, то есть используя «обычный» __sub__.

Список операторов, которые мы перегрузим в этом листке

Полный список волшенбных методов можно найти в документации (их довольно много на все случаи жизни): http://docs.python .org/py3k/reference/datamodel.html.

Далее идут только те методы, которые нам в этом листке понадобятся. Для удобства введём обозначение: fr — это рациональные числа, экземпляры нашего нового класса Fraction. Переменные n — это строго целые числа.

Как выглядит в коде Что на самом деле вызывается Комментарий
Арифметические операторы
Сложение
fr + n Fraction.__add__(fr, n)
n + fr int.__add__(n, fr) -> NotImplemented -> Fraction.__radd__(fr, n)
Вычитание
fr - n Fraction.__sub__(fr, n)
n - fr int.__sub__(n, fr) -> NotImplemented -> Fraction.__rsub__(fr, n)
Умножение
fr * n Fraction.__mul__(fr, n)
n * fr int.__mul__(n, fr) -> NotImplemented -> Fraction.__rmul__(fr, n)
Деление
fr / n Fraction.__truediv__(fr, n)
n / fr int.__truediv__(n, fr) -> NotImplemented -> Fraction.__rtruediv__(fr, n)
Отрицание, модуль
+fr Fraction.__pos__(fr)
-fr Fraction.__neg__(fr)
abs(fr) Fraction.__abs__(fr)
Операторы сравнения
fr < n Fraction.__lt__(fr, n) lt = Less Than
n < fr int.__lt__(n, fr) -> NotImplemented -> Fraction.__gt__(fr, n) n < fr <=> fr > n
fr > n Fraction.__gt__(fr, n) gt = Greater Than
n > fr int.__gt__(n, fr) -> NotImplemented -> Fraction.__lt__(fr, n) n > fr <=> fr < n
fr <= n Fraction.__le__(fr, n) le = Less or Equal
n <= fr int.__le__(n, fr) -> NotImplemented -> Fraction.__ge__(fr, n) n <= fr <=> fr >= n
fr >= n Fraction.__ge__(fr, n) ge = Greater or Equal
n >= fr int.__ge__(n, fr) -> NotImplemented -> Fraction.__le__(fr, n) n >= fr <=> fr <= n
fr == n Fraction.__eq__(fr, n) eq = EQual
n == fr int.__eq__(n, fr) -> NotImplemented -> Fraction.__eq__(fr, n) n == fr <=> fr == n
fr != n Fraction.__ne__(fr, n) ne = Not Equal
n != fr int.__ne__(n, fr) -> NotImplemented -> Fraction.__ne__(fr, n) n != fr <=> fr != n
Преобразование к стандартным типам
Превращение в строку
str(fr), print(fr) Fraction.__str__(fr)
repr(fr) Fraction.__repr__(fr)

Большой и сложный пример для вдумчивого исследования

Очень рекомендуется пройти эту программу по шагам в режиме отладки, чтобы увидеть, как и что происходит.

Код примера
def obj_printer(obj, show_value=True):
    return '<{} object at {} with value {}>'.format(obj.__class__.__name__, hex(id(obj)), repr(obj) if type(obj) != DummyNumbers else obj.value if show_value else '?')

class DummyNumbers():
    """Класс для демонстрации возможностей классов
    """
    print("Эта строка будет выведена раньше всех: здесь Python начал читать, как устроен этот класс")
    def __init__(self, some_value=0):
        print("Запущен конструктор. Переданы парамеры: self={} и some_value={}".format(obj_printer(self, False), obj_printer(some_value)))
        self.value = some_value

    def __repr__(self):
        print("Запущен медот __repr__. Передан self={}".format(obj_printer(self)))
        return 'DummyNumbers({})'.format(self.value)

    def __str__(self):
        print("Запущен медот __str__. Передан self={}".format(obj_printer(self)))
        return 'Дурацкое число: {}'.format(self.value)

    def __sub__(self, other):
        print("Запущен метод __sub__ с параметрами self={}, other={}. "
              "Здесь self гарантированно является экземпляром DummyNumbers, "
              "а other может быть любым объектом.".format(obj_printer(self), obj_printer(other)))
        print("Хотят self - other")
        if type(other) in (int, float):
            return DummyNumbers(self.value - other)
        elif isinstance(other, DummyNumbers):
            return DummyNumbers(self.value - other.value)
        else:
            print("Мы сами не знаем, как вычесть такой объект")
            return NotImplemented

    def __rsub__(self, other):
        print("Запущен метод __rsub__ с параметрами self={}, other={}. "
              "Здесь self гарантированно является экземпляром DummyNumbers, "
              "а other может быть любым объектом. При этом мы из other вычитаем self".format(obj_printer(self), obj_printer(other)))
        print("Хотят other - self, но класс other не знает, как вычесть self")
        if type(other) in (int, float):
            return DummyNumbers(other - self.value)
        else:
            print("Мы сами не знаем, как можно вычитать из такого объекта")
            return NotImplemented

    def __isub__(self, other):
        print("Запущен метод __isub__ с параметрами self={}, other={}. "
              "Здесь self гарантированно является экземпляром DummyNumbers, "
              "а other может быть любым объектом.".format(obj_printer(self), obj_printer(other)))
        print("Хотят из self вычесть other и положить в self")
        if type(other) in (int, float):
            self.value -= other
            return self
        elif isinstance(other, DummyNumbers):
            self.value -= other.value
            return self
        else:
            print("Мы сами не знаем, как из себя вычесть такой объект")
            return NotImplemented

print('\n' * 2, 'a = DummyNumbers()')
a = DummyNumbers()

print('\n' * 2, 'b = DummyNumbers(3)')
b = DummyNumbers(0)

print('\n' * 2, 'c = DummyNumbers(3)')
c = DummyNumbers(3.3)

print('\n' * 2, 'd = a - b')
d = a - b

print('\n' * 2, 'e = b - 5')
e = b - 5

print('\n' * 2, 'f = 3.3 - c')
f = 3.3 - c

print('\n' * 2, 'c -= b')
c -= b

print('\n' * 2, 'd -= 1.79')
d -= 1.79

print('\n' * 2, 'q = 7, q -= b')
q = 7
q -= b

print('\n' * 2, 'print(a, b, c, d, e, f)')
print(a, b, c, d, e, f)

print('\n' * 2, "g = 'a' - a")
try:
    g = 'a' - a
except TypeError as e:
    print(e)

print('\n' * 2, "g = a - 'b'")
try:
    g = a - 'b'
except TypeError as e:
    print(e)

print('\n' * 2, "a -= 'q'")
try:
    a -= 'q'
except TypeError as e:
    print(e)

print('\n' * 2, "q = 'q', q -= a")
try:
    q = 'q'
    q -= a
except TypeError as e:
    print(e)
Интерактивная отладка

Ввод-вывод и тестирование

Во всех задачах на доработку методов класса тестирование производится следующим образом: вашей программе на вход передаётся тест, состоящий из последовательность питоновских команд, который нужно исполнить при помощи функции exec. Сделать это удобнее всего одним из двух способов:

"""Описание класса"""

import sys
exec(sys.stdin.read())

или

"""Описание класса"""

exec(open('input.txt').read())

В первом случае ввод производится с клавиатуры (не забудьте про Ctrl+D для завершения ввода), но плохо работает отладка (debug). Во втором случае ввод производится из файла.

В задачах A-F каждая следующая задача содержит тесты всех предыдущих.

A: Реализация методов __init__ и __str__.

Метод __init__ может принимать на вход пару целых чисел, второе из которых не равно нулю, одно число или вообще не принимать никаких аргументов.
∙ 2 аргумента — числитель и знаменатель;
∙ 1 аргумент — числитель, знаменатель равен 1;
∙ без аргументов — числитель равен 0, знаменатель равен 1.
Метод __str__ должен возвращать строковое представление дроби в виде, показанном в тестах.

print(Fraction()) print(Fraction(179)) print(Fraction(2, 4)) print(Fraction(30, -90)) print(Fraction(8, 2))
0 179 1/2 -1/3 4
IDE

B: Реализация арифметических операций — сложение и унарный плюс (__add__, __radd__, __pos__)

Рациональные числа должны поддерживать сложения друг с другом и с целыми числами.

a = Fraction(2, 4) b = Fraction(30, -90) c = Fraction(3, 7) print(a + b) print(a + b + c + 1) c += (+a + b) print(c) print(1 + a)
1/6 67/42 25/42 3/2
IDE

C: Реализация арифметических операций — вычитание и унарный минус (__sub__, __rsub__, __neg__)

a = Fraction(2, 4) b = Fraction(30, -90) c = Fraction(3, 7) print(a - b) print(-a - b - c - 2) a -= b + c print(a) print(1 - c)
5/6 -109/42 17/42 4/7
IDE

D: Реализация арифметических операций — умножение (__mul__, __rmul__)

a = Fraction(1, 2) b = Fraction(3, -7) c = Fraction(1, 5) print(a * b) c *= a + b print(c) print(3 * a)
-3/14 1/70 3/2
IDE

E: Реализация арифметических операций — деление (__truediv__, __rtruediv__)

a = Fraction(1, 2) b = Fraction(3, -7) c = Fraction(1, 5) print(a / Fraction(2, 3)) b /= c / a print(b) print(4 / Fraction(3, -7))
3/4 -15/14 -28/3
IDE

F: Реализация логических операций — сравнения

(==, !=, <, <=, >, >=, соответственно __eq__, __ne__, __lt__, __le__, __gt__, __ge__)

a = Fraction(2, 5) b = Fraction(5, 12) c = Fraction(3, 7) d = Fraction(1, 35) print(a > b) print(a != c) print(a + d == c) print(b >= c) print(c < a) print(c <= a + b) print(1 < a)
False True True False False True False
IDE

Используем реализацию рациональных чисел для решения задач

В следующих задачах используйте вашу реализацию рациональных чисел.

G: Ближайшая аликвотная дробь

По данной дроби, не превосходящей $1$, найдите максимальную дробь с числителем, равным $1$, не превосходящей данную.
На вход программе подаётся строка вида A/B, где A и B — натуральные числа, десятичная запись которых содержит не более $100$ знаков.
Требуется вывести дробь вида 1/С, где $\dfrac{1}{C}$ — максимальная дробь с числителем, равным $1$, такая что $\dfrac{1}{C}\leqslant\dfrac{A}{B}$.

2/5
1/3
1/4
1/4
23/101
1/5
45/46
1/2
IDE

H: Египетские дроби

Разложением дроби на сумму египетских дробей называется представление данной дроби в ввиде суммы дробей с числителем равным $1$ и разными знаменателями (дроби с числителем равным $1$ ещё называют аликвотными дробями).
Существует несколько алгоритмов разложения данной дроби на сумму египетских дробей. Намёк на один из них — предыдущая задача.
Дана дробь, не превосходящая $1$. Найти разложение данной дроби на сумму египетских дробей и вывести это разложение (см. примеры). Если разложений существует несколько, вывести любое.
На вход программе подаётся строка в формате, описанном в задаче G.
Программа должна вывести выражение — сумму различных аликвотных дробей или одну дробь, если заданная дробь является аликвотной. Значение выведенного выражения должна быть равно данной дроби.

2/5
1/3+1/15
3/7
1/3+1/11+1/231
1/13
1/13
5/121
1/25+1/757+1/763309+1/873960180913+1/1527612795642093418846225
IDE

Цепные дроби

Определение: цепная (или непрерывная) дробь (continued fraction)— это конечное или бесконечное выражение вида: $$[a_0;a_1,a_2,\dots]=a_0+\dfrac{1}{a_1+\dfrac{1}{a_2+\dfrac{1}{a_3+\dots}}}$$ где $a_0$ — целое число, остальные $a_i$ — натуральные числа, причём если дробь конечная, то последнее значение отлично от единицы. Число представляется конечной цепной дробью тогда и только тогда, когда оно рационально.

Декоратор @staticmethod для методов, которым не нужен экземпляр класса

Сейчас мы добавим нашим дробям возможность превращения в цепную или периодическую десятичную дробь и обратно. Начнём с конструирования рационального числа из цепной дроби.

Добавим метод from_continued в класс Fraction, который по массиву или кортежу целых чисел — элементов цепной дроби — будет создавать дробь. Работать это должно так:

class Fraction:
    ...
    def from_continued(terms):
        ...
        return Fraction(p, q)

x = Fraction.from_continued([1, 1, 2])  # 5/3
y = Fraction.from_continued([0, 2, 4, 8, 16])  # 532/1193

Обратите внимание, здесь внутри метода from_continued нам не нужен self — экземпляр класса. При вызове как в примере выше он и не будет передан. Но этот метод можно вызвать и так:

>>> x = Fraction(1, 2)
>>> y = x.from_continued([1, 1, 2])
TypeError: from_continued() takes 1 positional argument but 2 were given

Нужно дать знать питону, что экземпляр класса нам никогда не потребуется. Для этого перед определением метода нужно добавить декоратор @staticmethod. Вот так:

class Fraction:
    ...
    @staticmethod
    def from_continued(terms):
        ...
        return Fraction(p, q)
Декоратор @classmethod

На самом деле в таком сценарии нужно писать так:

class Fraction:
    ...
    @classmethod
    def from_continued(cls, terms):
        ...
        return cls(p, q)

Это всё очень похоже на @staticmethod, только первым параметром вне зависимости от вызова будет передан сам класс. Это важно для правильной работы при использовании наследования. В контексте данного листка это всё неважно.

I: Получение значения дроби по её разложению в цепную дробь

Добавьте метод from_continued в класс Fraction, который по массиву или кортежу целых чисел — элементов цепной дроби — будет создавать дробь.

print(Fraction.from_continued([1, 1, 2]))
5/3
print(Fraction.from_continued([3])) print(Fraction.from_continued([1, 2, 3])) print(Fraction.from_continued([0, 2, 4, 8, 16])) print(Fraction(1, 3) + Fraction.from_continued([1, 1, 2]))
3 10/7 532/1193 2
IDE

J: Разложение данной дроби в цепную дробь

Добавьте метод to_continued в класс Fraction, который возвращает массив, представляющие собой разложение данной дроби в цепную дробь.

print(Fraction(5, 3).to_continued())
[1, 1, 2]
print(Fraction(3).to_continued()) print(Fraction(10, 7).to_continued())
[3] [1, 2, 3]
print(Fraction(532, 1193).to_continued()) print(Fraction(-7, 3).to_continued()) print((1 + 1 / (2 + 1 / (2 + 1 / (2 + 1 / (2 + Fraction(1, 2)))))).to_continued())
[0, 2, 4, 8, 16] [-3, 1, 2] [1, 2, 2, 2, 2, 2]
IDE

K: Ближайшая дробь

По данной дроби $\frac{A}{B}~(A, B < 10^{10})$ найти ближайшую к ней дробь со знаменателем, не превышающим данное число $Q~(Q < 20000)$, где $Q < B$. Если для данной дроби существует несколько дробей, равноудалённых от неё — выведите дробь с наименьшим знаменателем. Реализуйте это в виде метода limit_denominator.

print(Fraction(10, 23).limit_denominator(22))
7/16
print(Fraction(16, 23).limit_denominator(22)) print(Fraction(13, 57).limit_denominator(10)) print(Fraction(123, 1000).limit_denominator(100))
9/13 2/9 8/65
IDE

L: Получение по периодическому представлению дроби её значения

Дана строка, содержащая десятичную запись дроби. Либо дробь — целое число, либо обязательно содержит ровно одну точку. Слева от точки — запись целого числа. Справа от точки последовательность цифр от $0$ до $9$, при этом возможно какой-то непустой суффикс этой последовательности взят в круглые скобки.
Требуется вычислить и вывести значение соответствующей несократимой дроби.

Реализуйте это в виде метода from_repeating (по-английски периодическая дробь — repeating decimal).

print(Fraction.from_repeating('1.41(6)'))
17/12
print(Fraction.from_repeating('0.(0588235294117647)')) print(Fraction.from_repeating('0.(629)')) print(Fraction.from_repeating('0.25'))
1/17 17/27 1/4
IDE

M: Получение по значению дроби её периодического представления

Дана дробь $A/B~(A,B < 10000)$. Требуется вывести её представление в виде конечной десятичной или бесконечной периодической дроби.
Обратите внимание, что дроби допускают различные способы записи. Проверьте, что соблюдены следующие правила:

Реализуйте это в виде метода to_repeating.

print(Fraction(17, 12).to_repeating())
1.41(6)
print(Fraction(1, 17).to_repeating()) print(Fraction(17, 27).to_repeating()) print(Fraction(1, 4).to_repeating())
0.(0588235294117647) 0.(629) 0.25
IDE

N★: Ближайшая дробь (быстрый алгоритм)

По данной дроби $\frac{A}{B}~(A, B < 10^{100})$ найти ближайшую к ней дробь со знаменателем, не превышающим данное число $Q$, где $Q < B$. Если для данной дроби существует несколько дробей, равноудалённых от неё — выведите дробь с наименьшим знаменателем.
Эта задача в точности совпадает с задачей K, отличаясь только ограничениями на величину числителя и знаменателя данной дроби.
Литература по теме:

IDE

O: Игра с калькулятором

У вас есть калькулятор, который умеет выполнять арифетические операции (сложение, вычитание, умножение и деление). Результат деления целых чисел (в случае, когда он не является целым числом) калькулятор умеет показывать вам с точностью до, скажем, первых $50$ десятичных разрядов.
Представим себе следующую игру. Вы можете выбрать один из возможных вариантов действий:


Ваша задача: написать программу, которая в данных условиях сумеет определить по последовательности $50$-разрядных чисел, какой из способов был использован для её получения. Если число получено при помощи деления, выведите RATIONAL, иначе выведите RANDOM.
Тесты в этой задаче закрытые.
Литература по теме приведена в предыдущей задаче, а кроме того можно почитать, например, замечательную книжку «Математический дивертисмент» Сергея Табачникова и Дмитрия Фукса.
81504858796678001868515099327620295960919576801285 04098472842529894827834605964558420976804495029534 65620886155843687447027212233783564459993147350008 79295424616572769488202619879798409853758032416804 71368110823794816367183940729755459969648507089869 07230935482661632464220914285636669261174201759061 43310946362920969724658715119924460828397453981816 90649125634120798608004383559140501051799143861756 79485406521331383752787840945353627149102220745097 05471764553143675247574915667388590104261979378050 24717702242571855712176717634740996700813924196593 21312930451324098918111530306951235568388698964419 57883145363408521303258145363408521303258145363408 01343024319940693740495976222241919349472954757105 48801448623934179631989798866771907139267742835221 74348181090299672014058440458666709321435862483290 67249820645206154629459022682671783225975622784159 64675440512886031049490848581904652357555031463319 10787289277703996105308724182035123346197096394835 09287249830099443835730433697174796466068431783193
RANDOM RATIONAL RATIONAL RATIONAL RATIONAL RANDOM RANDOM RANDOM RANDOM RANDOM RANDOM RATIONAL RATIONAL RATIONAL RANDOM RANDOM RANDOM RANDOM RATIONAL RATIONAL
IDE