|
384 | 384 | \hsection{Arithmetic Dunder and Ordering}% |
385 | 385 | \label{sec:arithmeticsAndOrder}% |
386 | 386 | % |
| 387 | +\gitLoadPython{dunder:fraction}{}{dunder/fraction.py}{}% |
387 | 388 | \gitLoadPython{dunder:fraction:part_1}{}{dunder/fraction.py}{--args format --labels part_1}% |
388 | 389 | \listingPython{dunder:fraction:part_1}{% |
389 | 390 | Part~1 of the \pythonil{Fraction} class: The initializer \dunder{init} and global constants.}% |
390 | 391 | \afterpage{\clearpage}% |
391 | 392 | % |
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. |
393 | 394 | Indeed, even the arithmetic operators~\pythonilIdx{+}, \pythonilIdx{-}, \pythonilIdx{*}, and \pythonilIdx{/}. |
394 | 395 | This allows us to define new numerical types if we want. |
395 | 396 |
|
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! |
397 | 398 | 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{ |
398 | 399 | \python\ already has such a type built-in. % |
399 | 400 | Our goal here is to explore dunder methods, so we make our own class instead. % |
|
402 | 403 | In other words, we want to pour primary school mathematics into a new numerical type. |
403 | 404 | To refresh our memory, $a$~is called the \pgls{numerator} and $b$~is called the \pgls{denominator} of the fraction~$\frac{a}{b}$. |
404 | 405 |
|
| 406 | +Let's start implementing our class \pythonil{Fraction} in file \programUrl{dunder:fraction}. |
405 | 407 | The overall code for this class is a bit longer compared to our previous examples. |
406 | 408 | 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}). |
407 | 409 | At first we need to decide which attributes such a class would need. |
408 | 410 | We construct the initializer dunder method \dunder{init} in \cref{lst:dunder:fraction:part_1}. |
409 | 411 |
|
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}. |
412 | 415 | The attributes will therefore receive the \pgls{typeHint} \pythonilIdx{Final[int]}~\cite{PEP591}. |
413 | 416 |
|
414 | 417 | 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$. |
416 | 419 | This is the case for, let's say, $\frac{-9}{3}$ and $\frac{12}{-4}$. |
417 | 420 | In such cases we want to ideally store them in objects that have exactly the same attribute values. |
418 | 421 |
|
|
427 | 430 | This leaves only the question where the sign should be stored. |
428 | 431 | Obviously, $\frac{-5}{2}=\frac{5}{-2}$ and $\frac{5}{2}=\frac{-5}{-2}$. |
429 | 432 | 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. |
431 | 434 | It can only be that $\frac{a}{b}<0$ if exactly one of $a<0$ or $b<0$ is true. |
432 | 435 | Therefore, the sign of our fraction is determined by \pythonil{-1 if ((a < 0) != (b < 0)) else 1}. |
433 | 436 |
|
|
439 | 442 | Then we need to check whether our canonicalization by dividing with the \pythonilIdx{gcd} correctly maps~$\frac{12}{2}$ to~$\frac{6}{1}$. |
440 | 443 | 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}$. |
441 | 444 | 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. |
443 | 447 | 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}. |
445 | 449 |
|
446 | 450 | Several special fractions will occur very often in computations. |
447 | 451 | Instead of creating them again and again, we can define them as constants. |
|
470 | 474 | This was because we did not yet define the \dunder{str} and \dunder{repr} methods for our class \pythonil{Fraction}. |
471 | 475 | We do this in \cref{lst:dunder:fraction:part_2}. |
472 | 476 | 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. |
474 | 479 | Otherwise, it should return \pythonil{f"\{self.a\}/\{self.b\}"}. |
| 480 | + |
475 | 481 | 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}% |
477 | 486 | 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 | +% |
480 | 490 | 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}. |
482 | 492 | 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)}. |
484 | 495 | Anyway, with the string conversion out of the way, we can begin to implement mathematical operators. |
485 | 496 |
|
486 | 497 | \gitLoadPython{dunder:fraction:part_3}{}{dunder/fraction.py}{--args format --labels part_3}% |
|
495 | 506 |
|
496 | 507 | In \cref{lst:dunder:fraction:part_3}, we want to enable our \pythonil{Fraction} class to be used with the \pythonil{+} and \pythonil{-} operators. |
497 | 508 | 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}. |
500 | 511 | 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}$. |
507 | 522 |
|
508 | 523 | After confirming that these tests succeed, we continue by implementing the \dunder{sub} method in exactly the same way. |
509 | 524 | 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. |
512 | 527 |
|
513 | 528 | 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$. |
517 | 533 | The dunder methods can be implemented according to the same schematic as before. |
518 | 534 | We test multiplication by confirming that $\frac{6}{19}*\frac{3}{-7}=\frac{6 * 3}{19*-7}=\frac{18}{-133}=\frac{-18}{133}$. |
519 | 535 | 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}$. |
520 | 536 |
|
521 | 537 | Now we also implement support for the \pythonilIdx{abs} function. |
522 | 538 | \pythonilIdx{abs} returns the absolute value of a number. |
523 | 539 | 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. |
525 | 541 | We can implement this method as follows: |
526 | 542 | 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. |
528 | 544 |
|
529 | 545 | \gitLoadPython{dunder:fraction:part_5}{}{dunder/fraction.py}{--args format --labels part_5}% |
530 | 546 | \listingPython{dunder:fraction:part_5}{% |
|
551 | 567 | \end{itemize}% |
552 | 568 | % |
553 | 569 | 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}. |
555 | 571 | \dunder{eq} is thus quickly implemented. |
556 | 572 | \dunder{ne} is its complement for the \pythonil{!=}\pythonIdx{"!=}~operator. |
557 | 573 | \pythonil{x != y} is \pythonil{True} if either \pythonil{x.a != y.a} or \pythonil{x.b != y.b}. |
558 | 574 |
|
559 | 575 | The other four comparison methods can be implemented by remembering how we used the common \pgls{denominator} for addition and subtraction. |
560 | 576 | 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$. |
562 | 579 | Thus, $\frac{a}{b}\leq\frac{c}{d}$ is the same as~$a*d\leq c*b$. |
563 | 580 | The greater and greater-or-equal operations can be defined the other way around. |
564 | 581 |
|
565 | 582 | All six comparison operations are defined accordingly in \cref{lst:dunder:fraction:part_5}. |
566 | 583 | This time, I omitted \pglspl{doctest} for the sake of space. |
567 | 584 | 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. |
571 | 591 | Your code does not need to fit on book pages\dots |
572 | 592 |
|
573 | 593 | Anyway, in \cref{exec:dunder:fraction:doctest} we present the output of \pytest\ running the \pglspl{doctest} of all the methods we implemented. |
574 | 594 | All of them succeed. |
575 | 595 | 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!% |
576 | 603 | \FloatBarrier% |
577 | 604 | \endhsection% |
578 | 605 | % |
|
0 commit comments