Skip to content

Commit e009cd9

Browse files
committed
improved fraction section
1 parent 329f06e commit e009cd9

File tree

2 files changed

+63
-34
lines changed

2 files changed

+63
-34
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ Die Slides zum Kurs in deutscher Sprache können unter <https://thomasweise.gith
6565
40. [Operationen für Iteratoren](https://thomasweise.github.io/programmingWithPythonSlidesDE/40_operationen_für_iteratoren.pdf)
6666
41. [Klassen: Grundlagen](https://thomasweise.github.io/programmingWithPythonSlidesDE/41_klassen_grundlagen.pdf)
6767
42. [Klassen: Kapselung](https://thomasweise.github.io/programmingWithPythonSlidesDE/42_klassen_kapselung.pdf)
68+
43. [Klassen: Vererbung](https://thomasweise.github.io/programmingWithPythonSlidesDE/43_klassen_vererbung.pdf)
6869
44. [Klassen/Dunder: `__str__`, `__repr__`, und `__eq__`](https://thomasweise.github.io/programmingWithPythonSlidesDE/44_klassen_dunder_str_rep_eq.pdf)
6970
45. [Klassen/Dunder: `__hash__`](https://thomasweise.github.io/programmingWithPythonSlidesDE/45_klassen_dunder_hash.pdf)
71+
46. [Klassen/Dunder: Arithmetische Operatoren und Vergleiche](https://thomasweise.github.io/programmingWithPythonSlidesDE/46_klassen_dunder_math.pdf)
7072

7173

7274
### 2.3. The Slides in English

text/main/classes/dunder/dunder.tex

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -384,16 +384,17 @@
384384
\hsection{Arithmetic Dunder and Ordering}%
385385
\label{sec:arithmeticsAndOrder}%
386386
%
387+
\gitLoadPython{dunder:fraction}{}{dunder/fraction.py}{}%
387388
\gitLoadPython{dunder:fraction:part_1}{}{dunder/fraction.py}{--args format --labels part_1}%
388389
\listingPython{dunder:fraction:part_1}{%
389390
Part~1 of the \pythonil{Fraction} class: The initializer \dunder{init} and global constants.}%
390391
\afterpage{\clearpage}%
391392
%
392-
Much of \python's syntactic behavior can be grounded on dunder methods.
393+
Much of the actual behavior of \python's syntactx is implemented by dunder methods.
393394
Indeed, even the arithmetic operators~\pythonilIdx{+}, \pythonilIdx{-}, \pythonilIdx{*}, and \pythonilIdx{/}.
394395
This allows us to define new numerical types if we want.
395396

396-
Here, we will do exactly that:
397+
And since we did a lot of math-nerdery in this book already {\dots} of course we want that!
397398
We implement the basic arithmetic operations for a class~\pythonil{Fraction} that represents fractions~$q\in\rationalNumbers$, i.e., it holds that~$q=\frac{a}{b}$ with~$a,b\in\integerNumbers$ and~$b\neq0$.\footnote{
398399
\python\ already has such a type built-in. %
399400
Our goal here is to explore dunder methods, so we make our own class instead. %
@@ -402,17 +403,19 @@
402403
In other words, we want to pour primary school mathematics into a new numerical type.
403404
To refresh our memory, $a$~is called the \pgls{numerator} and $b$~is called the \pgls{denominator} of the fraction~$\frac{a}{b}$.
404405

406+
Let's start implementing our class \pythonil{Fraction} in file \programUrl{dunder:fraction}.
405407
The overall code for this class is a bit longer compared to our previous examples.
406408
We therefore split it into several parts, namely~\cref{lst:dunder:fraction:part_1,lst:dunder:fraction:part_2,lst:dunder:fraction:part_3,lst:dunder:fraction:part_4,lst:dunder:fraction:part_5} (and later add \cref{lst:dunder:fraction:part_6}).
407409
At first we need to decide which attributes such a class would need.
408410
We construct the initializer dunder method \dunder{init} in \cref{lst:dunder:fraction:part_1}.
409411

410-
Since the fraction~$\frac{a}{b}$ can be defined by the two integer numbers~$a$ and~$b$, it makes sense to also have two \pythonil{int} attributes~\pythonil{a} and~\pythonil{b} in our~\pythonil{Fraction} class.
411-
We want our numbers to be immutable, because like you cannot change the value of~\pythonil{5}, you should also not be able to change the value of~\pythonil{1/3}.
412+
Since the fraction~$\frac{a}{b}$ can be defined by the two integer numbers~$a$ and~$b$, it makes sense to also have two \pythonil{int} attributes~\pythonil{a} and~\pythonil{b}.
413+
We want our fractions to be immutable.
414+
You cannot change the value of~\pythonil{5}, and you should also not be able to change the value of~\pythonil{1/3}.
412415
The attributes will therefore receive the \pgls{typeHint} \pythonilIdx{Final[int]}~\cite{PEP591}.
413416

414417
Our fractions should be \emph{canonical}.
415-
It is totally possible that two fractions~$\frac{a}{b}=\frac{c}{d}$ with $a\neq c$ or $b\neq c$.
418+
It is totally possible that two fractions~$\frac{a}{b}=\frac{c}{d}$ with $a\neq c$ and $b\neq c$.
416419
This is the case for, let's say, $\frac{-9}{3}$ and $\frac{12}{-4}$.
417420
In such cases we want to ideally store them in objects that have exactly the same attribute values.
418421

@@ -427,7 +430,7 @@
427430
This leaves only the question where the sign should be stored.
428431
Obviously, $\frac{-5}{2}=\frac{5}{-2}$ and $\frac{5}{2}=\frac{-5}{-2}$.
429432
We decide that the sign of the fraction is always stored in the attribute~\pythonil{a}.
430-
In other words, if $\frac{a}{b}<0$, then \pythonil{a} will be negative, otherwise it should be positive.
433+
In other words, if $\frac{a}{b}<0$, then \pythonil{a} will be negative, otherwise it should be positive or~0.
431434
It can only be that $\frac{a}{b}<0$ if exactly one of $a<0$ or $b<0$ is true.
432435
Therefore, the sign of our fraction is determined by \pythonil{-1 if ((a < 0) != (b < 0)) else 1}.
433436

@@ -439,9 +442,10 @@
439442
Then we need to check whether our canonicalization by dividing with the \pythonilIdx{gcd} correctly maps~$\frac{12}{2}$ to~$\frac{6}{1}$.
440443
And we need to verify that~$\frac{2}{-12}$ and~$\frac{-2}{12}$ correctly become~$\frac{-1}{6}$ while $\frac{-2}{-12}$~becomes~$\frac{1}{6}$.
441444
The special case of the number zero also needs to be checked:
442-
We know that \pythonil{gcd(0, -9) = -9}, so it should work, but it is better to verify that~$\frac{0}{-9}$ is indeed mapped to~$\frac{0}{1}$.
445+
We know that \pythonil{gcd(0, -9) = -9}, so $\frac{0}{-9}$ should become $\frac{0}{1}$.
446+
Still, it is better to verify that.
443447
Finally, we need to verify that the \pythonilIdx{ZeroDivisionError} is indeed raised when we try to instantiate \pythonil{Fraction} with a zero \pgls{denominator}.
444-
Without needing to read the actual code of \pythonil{\_\_init\_\_}, a user can therefore already learn a lot about how our class \pythonil{Fraction} represents rational numbers just from the \pglspl{doctest}.
448+
Without needing to read the actual code of \dunder{init}, a user can therefore already learn a lot about how our class \pythonil{Fraction} represents rational numbers just from the \pglspl{doctest}.
445449

446450
Several special fractions will occur very often in computations.
447451
Instead of creating them again and again, we can define them as constants.
@@ -470,17 +474,24 @@
470474
This was because we did not yet define the \dunder{str} and \dunder{repr} methods for our class \pythonil{Fraction}.
471475
We do this in \cref{lst:dunder:fraction:part_2}.
472476
The method \dunder{str} is supposed to return a compact representation of the fractions.
473-
We implement such that it returns \pythonil{self.a} as string if the \pgls{denominator} is one, i.e., if~\pythonil{self.b == 1}.
477+
We implement it such that it returns \pythonil{self.a} as string if the \pgls{denominator} is one, i.e., if~\pythonil{self.b == 1}.
478+
Because then the fraction is actual an integer number.
474479
Otherwise, it should return \pythonil{f"\{self.a\}/\{self.b\}"}.
480+
475481
This is easy and clear enough for each user to immediately recognize the value of the fraction.
476-
It is also ambiguous, though, because one cannot distinguish \pythonil{str(Fraction(12, 1))} from \pythonil{str(12)}, i.e., fractions that represent integer numbers will produce the same strings as integer numbers.
482+
It is also ambiguous, though, because one cannot distinguish \pythonil{str(Fraction(12, 1))} from \pythonil{str(12)}.
483+
Fractions that represent integer numbers will produce the same strings as these integer numbers.%
484+
%
485+
\begin{sloppypar}%
477486
The \dunder{repr} method exists to produce unambiguous output.
478-
We implement it to return~\pythonil{f\"Fraction(\{self.a\}, \{self.b\})"}.
479-
487+
We implement it to return~\pythonil{f\"Fraction(\{self.a\}, \{self.b\})\"}.%
488+
\end{sloppypar}%
489+
%
480490
In the \pglspl{docstring} of both methods, we include \pglspl{doctest}.
481-
Notice that \dunder{str} is used if pass an object to~\pythonilIdx{print}.
491+
Notice that \dunder{str} is used automatically if pass an object to~\pythonilIdx{print}.
482492
This means that we can compare the expected output of \pythonil{f.\_\_str\_\_()} for a fraction~\pythonil{f} to the result of~\pythonil{print(f)}.
483-
Otherwise, \pglspl{doctest} always convert objects to string using~\pythonilIdx{repr}, meaning that the line~\pythonil{Fraction(-5, 12)} in the \pgls{doctest} of \dunder{repr} actually calls \pythonil{repr(Fraction(-5, 12)}.
493+
Otherwise, \pglspl{doctest} always convert objects to string using~\pythonilIdx{repr}.
494+
This means that the line~\pythonil{Fraction(-5, 12)} in the \pgls{doctest} of \dunder{repr} actually calls \pythonil{repr(Fraction(-5, 12)}.
484495
Anyway, with the string conversion out of the way, we can begin to implement mathematical operators.
485496

486497
\gitLoadPython{dunder:fraction:part_3}{}{dunder/fraction.py}{--args format --labels part_3}%
@@ -495,36 +506,41 @@
495506

496507
In \cref{lst:dunder:fraction:part_3}, we want to enable our \pythonil{Fraction} class to be used with the \pythonil{+} and \pythonil{-} operators.
497508
In \python, doing something like \pythonil{x + y} will invoke \pythonil{x.\_\_add\_\_(y)}, if the class of~\pythonil{x} defines the \dunder{add} method.
498-
From primary school, we remember that $\frac{a}{b}+{c}{d} = \frac{a*d+c*b}{b*d}$.
499-
Therefore, if \pythonil{other} is also an instance\pythonIdx{isinstance} of \pythonil{Fraction}, \pythonil{\_\_add\_\_(other)} computes the result like that and creates a new \pythonil{Fraction}.
509+
From primary school, we remember that $\frac{a}{b}+\frac{c}{d} = \frac{a*d+c*b}{b*d}$, for~$b,d\neq0$.
510+
Therefore, if \pythonil{other} is also an instance\pythonIdx{isinstance} of \pythonil{Fraction}, then \pythonil{\_\_add\_\_(other)} computes the result like that and creates a new \pythonil{Fraction}.
500511
Notice that the initializer of that new fraction will automatically normalize the fraction by using~\pythonilIdx{gcd}.
501-
If \pythonil{other} is not an instance of \pythonil{Fraction}, we return \pythonilIdx{NotImplemented}, because this would enable \python\ to look for other routes to perform addition with our objects.\footnote{%
502-
\python\ would then look whether \pythonil{other} provides an \pythonilIdx{\_\_radd\_\_}\pythonIdx{dunder!\_\_radd\_\_} method that does not return~\pythonilIdx{NotImplemented} {\dots} but we will not implement all possible arithmetic dunder methods here so we skip this one.%
503-
}%
504-
The behavior of this method is again be tested with \pglspl{doctest}.
505-
These check that $\frac{1}{3} + \frac{1}{2}$ actually yields~$\frac{5}{6}$ and that $\frac{1}{2}+\frac{1}{2}$ really returns~$\frac{1}{1}$.
506-
They also check correct normalization by trying~$\frac{21}{-12}+\frac{-33}{42}=\frac{882+396}{-504}=\frac{1278}{-504}=\frac{18*1278}{18*-28}=\frac{-71}{28}$.
512+
513+
If \pythonil{other} is not an instance of \pythonil{Fraction}, we return \pythonilIdx{NotImplemented}.
514+
We already know this from our implementation of \dunder{eq} for points.
515+
This result enables \python\ to look for other routes to perform addition with our objects.
516+
Here, \python\ would then look whether \pythonil{other} provides a \dunder{radd} method that does not return~\pythonilIdx{NotImplemented} {\dots} but we will not implement all possible arithmetic dunder methods here so we skip this one.
517+
518+
Either way, the behavior of this method is again be tested with \pglspl{doctest}.
519+
These tests check that $\frac{1}{3} + \frac{1}{2}$ actually yields~$\frac{5}{6}$.
520+
We test whether $\frac{1}{2}+\frac{1}{2}$ really returns~$\frac{1}{1}$.
521+
Then we also check correct normalization by trying whether~$\frac{21}{-12}+\frac{-33}{42}=\frac{882+396}{-504}=\frac{1278}{-504}=\frac{18*1278}{18*-28}=\frac{-71}{28}$.
507522

508523
After confirming that these tests succeed, we continue by implementing the \dunder{sub} method in exactly the same way.
509524
This enables subtraction by using~\pythonilIdx{-}, because \pythonil{x - y} will invoke \pythonil{x.\_\_sub\_\_(y)}, if the class of~\pythonil{x} defines the \dunder{sub} method.
510-
Clearly, $\frac{a}{b}-{c}{d} = \frac{a*d-c*b}{b*d}$.
511-
As \pglspl{doctest}, the same three cases as used for \pythonil{\_\_add\_\_} will do.
525+
Clearly, $\frac{a}{b}-\frac{c}{d} = \frac{a*d-c*b}{b*d}$ for~$b,d\neq0$.
526+
As \pglspl{doctest}, the same three cases as used for \dunder{add} will do.
512527

513528
In \cref{lst:dunder:fraction:part_4}, we now focus on multiplication and division.
514-
The \pythonil{*}~operation will utilize a \dunder{mul}, if implemented, and that \pythonil{/}~operation uses \dunder{truediv}.
515-
Multiplying the fractions~$\frac{a}{b}$ and~$\frac{c}{d}$ yields~$\frac{a*c}{b*d}$.
516-
Dividing~$\frac{a}{b}$ by~$\frac{c}{d}$ yields~$\frac{a*d}{b*c}$.
529+
The \pythonil{*}~operation will utilize the method \dunder{mul}, if implemented.
530+
The \pythonil{/}~operation uses the method \dunder{truediv}, if implemented.
531+
Multiplying the fractions~$\frac{a}{b}$ and~$\frac{c}{d}$ yields~$\frac{a*c}{b*d}$ for $b,d\neq0$.
532+
Dividing~$\frac{a}{b}$ by~$\frac{c}{d}$ yields~$\frac{a*d}{b*c}$ for $b,c,d\neq0$.
517533
The dunder methods can be implemented according to the same schematic as before.
518534
We test multiplication by confirming that $\frac{6}{19}*\frac{3}{-7}=\frac{6 * 3}{19*-7}=\frac{18}{-133}=\frac{-18}{133}$.
519535
The division is tested by computing whether $\frac{6}{19}*\frac{3}{-7}=\frac{6 * -7}{19*3}=\frac{-42}{57}=\frac{3*-14}{3*19}$ indeed gives us~$\frac{-14}{19}$.
520536

521537
Now we also implement support for the \pythonilIdx{abs} function.
522538
\pythonilIdx{abs} returns the absolute value of a number.
523539
Therefore, $\pythonil{abs(5)}=\pythonil{abs(-5)}=\pythonil{5}$.
524-
If present, \pythonil{abs(x)} will invoke~\pythonil{x.\_\_abs\_\_()}\pythonIdx{\_\_abs\_\_}\pythonIdx{dunder!\_\_abs\_\_}.
540+
\pythonil{abs(x)} will invoke~\pythonil{x.\_\_abs\_\_()}\pythonIdx{\_\_abs\_\_}\pythonIdx{dunder!\_\_abs\_\_}, if present.
525541
We can implement this method as follows:
526542
If our fraction is positive, then it can be returned as-is.
527-
Otherwise, we return a new, positive variant of our fraction by simply flipping the sign of it.
543+
Otherwise, we return a new, positive variant of our fraction.
528544

529545
\gitLoadPython{dunder:fraction:part_5}{}{dunder/fraction.py}{--args format --labels part_5}%
530546
\listingPython{dunder:fraction:part_5}{%
@@ -551,28 +567,39 @@
551567
\end{itemize}%
552568
%
553569
Implementing equality and inequality is rather easy, since our fractions are all normalized.
554-
For two fractions~\pythonil{x} and~\pythonil{y}, it holds only that~\pythonil{x == y} if \pythonil{x.a == y.a} and \pythonil{x.b == y.b}.
570+
For two fractions~\pythonil{x} and~\pythonil{y}, it holds that~\pythonil{x == y} if and only if \pythonil{x.a == y.a} and \pythonil{x.b == y.b}.
555571
\dunder{eq} is thus quickly implemented.
556572
\dunder{ne} is its complement for the \pythonil{!=}\pythonIdx{"!=}~operator.
557573
\pythonil{x != y} is \pythonil{True} if either \pythonil{x.a != y.a} or \pythonil{x.b != y.b}.
558574

559575
The other four comparison methods can be implemented by remembering how we used the common \pgls{denominator} for addition and subtraction.
560576
We did addition like this:~$\frac{a}{b}+\frac{c}{d}=\frac{a*d}{b*d}+\frac{c*b}{b*d}=\frac{a*d+c*b}{b*d}$.
561-
Looking at this again, we realize that $\frac{a}{b}<\frac{c}{d}$ is the same as~$\frac{a*d}{b*d}<\frac{c*b}{b*d}$, which must be the same as~$a*d<c*b$.
577+
Looking at this again, we realize that $\frac{a}{b}<\frac{c}{d}$ is the same as~$\frac{a*d}{b*d}<\frac{c*b}{b*d}$.
578+
This, in turn, must be the same as~$a*d<c*b$.
562579
Thus, $\frac{a}{b}\leq\frac{c}{d}$ is the same as~$a*d\leq c*b$.
563580
The greater and greater-or-equal operations can be defined the other way around.
564581

565582
All six comparison operations are defined accordingly in \cref{lst:dunder:fraction:part_5}.
566583
This time, I omitted \pglspl{doctest} for the sake of space.
567584
Matter of fact, I have shortened the code and tests in all of the above code snippets.
568-
For example, we do not check whether the parameters of the initializer \dunder{init} are actually integers (and raise a \pythonilIdx{TypeError} otherwise).
569-
Such checks should then be covered by \pglspl{doctest}.
570-
You should never omit such checks and tests, because in program code, you \emph{do} have space.
585+
For example, we do not check whether the parameters of the initializer \dunder{init} are actually integers~(and raise a \pythonilIdx{TypeError} otherwise).
586+
587+
Such checks should then be covered by additional \pglspl{unitTest}.
588+
You should never omit such checks and tests.
589+
In your program code, you \emph{do} have space.
590+
You can also pack them into additional modules.
571591
Your code does not need to fit on book pages\dots
572592

573593
Anyway, in \cref{exec:dunder:fraction:doctest} we present the output of \pytest\ running the \pglspl{doctest} of all the methods we implemented.
574594
All of them succeed.
575595
This means that we can be fairly confident that using our \pythonil{Fraction} class in real computations would provide us correct results.%
596+
597+
There are quite a few more dunder methods for implementing arithmetic operations.
598+
And this truly is a fun thing to do.
599+
We can have our classes for fractions and complex numbers.
600+
Well, \python\ already has those.
601+
But we could have complex numbers based on fractions.
602+
The sky is the limit!%
576603
\FloatBarrier%
577604
\endhsection%
578605
%

0 commit comments

Comments
 (0)