Skip to content

Commit f4a3ef8

Browse files
committed
first steps into dunder improvement
1 parent c9e8b7d commit f4a3ef8

File tree

4 files changed

+36
-26
lines changed

4 files changed

+36
-26
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ 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+
44. [Klassen/Dunder: `__str__`, `__repr__`, und `__eq__`](https://thomasweise.github.io/programmingWithPythonSlidesDE/44_klassen_dunder_str_rep_eq.pdf)
6869

6970

7071
### 2.3. The Slides in English

bookbase

text/main/basics/collections/dictionaries/dictionaries.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
You may notice that all of these sequences have the same order of elements that was used when we created the dictionary.
3737
They key-value pair \pythonil{2: "two"} comes before \pythonil{1: "one"}.
3838
This is because dictionaries, different from sets, are ordered datastructures.
39-
Their elements appear in all sequenced versions always in the same order in which they were originally inserted into the dictionary~\cite{PSF:P3D:TPLR:D}.%
39+
Their elements appear in all sequenced versions always in the same order in which they were originally inserted into the dictionary~\cite{PSF:P3D:TPLR:DM:D}.%
4040
\end{sloppypar}%
4141
%
4242
\begin{sloppypar}%

text/main/classes/dunder/dunder.tex

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,27 @@
2424
%
2525
In \python, we can distinguish two forms of string representations of a given object~\pythonil{o}:%
2626
%
27-
\begin{itemize}%
27+
\begin{enumerate}%
2828
%
2929
\item \pythonil{str(o)}\pythonIdx{str} should return a concise and brief representation of the object~\pythonil{o}.
30-
This representation is mainly for end users.
31-
\pythonil{str(o)} invokes the \dunder{str} dunder method, if it has been implemented.
32-
Otherwise, \dunder{repr} is used instead.%
30+
This representation is mainly for end users~\cite{PSF:P3D:TPLR:DM:S}.
31+
\pythonil{str(o)} invokes the \dunder{str} dunder method of~\pythonil{o}, if it has been implemented.
32+
Otherwise, \pythonil{o.__repr__()} is used instead.%
3333
%
34-
\item \pythonil{repr(o)}\pythonIdx{repr} should ideally return a string representation that contains all the information that is needed to re-create the object.
34+
\item \pythonil{repr(o)}\pythonIdx{repr} should ideally return a string representation that contains all the information that is needed to re-create the object~\cite{PSF:P3D:TPLR:DM:R}.
3535
The target audience here are programmers who are working on the code, who may need to write precise information into log files, or who are searching for errors.
3636
\pythonil{repr(o)} invokes the \dunder{repr} dunder method, if it has been implemented.
3737
Otherwise, it returns the default representation, which is the type name and ID of the object.%
3838
%
39-
\end{itemize}%
39+
\end{enumerate}%
4040
%
4141
These two functions are compared in program \programUrl{dunder:str_vs_repr} given as \cref{lst:dunder:str_vs_repr}.
4242
Here, we first create an integer variable \pythonil{the_int} with value \pythonil{123}.
43-
Both \pythonil{str(the_int)} and \pythonil{repr(the_int)} are \pythonil{"123"}.
43+
Both \pythonil{str(the_int)} and \pythonil{repr(the_int)}\pythonil{repr} are \pythonil{"123"}.
4444
This is to be expected, since this is all the information that is needed to completely recreate this value and, at the same time, it is also the most concise way to present the value.
4545

4646
We then create another variable \pythonil{the_str} with value \pythonil{"123"}.
47-
Printing \pythonil{the_str} to the \pgls{stdout}, which is equivalent to \pythonil{print(str(the_str))}, will make the text \textil{123} appear on the console.
47+
Printing \pythonil{the_str} to the \pgls{stdout}, which means doing \pythonil{print(str(the_str))}, will make the text \textil{123} appear on the console.
4848
Printing \pythonil{repr(the_str)}, however, produces~\textil{'123'}.
4949
Notice the added single quotation marks on each side?
5050
These are necessary.
@@ -68,24 +68,23 @@
6868
We will not discuss this class here in any detail.
6969
It suffices to know that instances of this class represent a combination of a date and a time.
7070
In the program, we first import the class \pythonilIdx{datetime} from the module of the same name.
71-
We create a variable \pythonil{right\_now} and assign to it the result of the function \pythonilIdx{now}\pythonIdx{datetime!datetime!now}, which returns an object representing, well, today and the current time.\footnote{%
71+
We create a variable \pythonil{right\_now} and assign to it the result of the function \pythonil{datetime.now}\pythonIdx{now}\pythonIdx{datetime!datetime!now}, which returns an object representing, well, today and the current time.\footnote{%
7272
In the output of our program given in \cref{exec:dunder:str_vs_repr}, you cannot see the time of your reading, but the time when this book was compiled.}
7373

74-
If we want to print the result of the \pythonilIdx{str} function applied to an object~\pythonil{o} in an \pgls{fstring}, then we can either do this using the format specifier~\pythonil{!s}\pythonIdx{"!s} or by printing the result of \pythonil{str(o)}.
75-
The former variant is usually preferred.
76-
Anyway, we find that the simple string representation of a \pythonilIdx{datetime} object is, well, a simple human readable date and time string.
77-
The result of the function \pythonilIdx{repr} for an object~\pythonil{o} can be obtained using the format specifier~\pythonil{!r}\pythonIdx{"!r} or by printing the result of \pythonil{repr(o)}.
74+
If we want to print the result of the \pythonilIdx{str} function applied to an object~\pythonil{o} in an \pgls{fstring}, then we can do this using the format specifier~\pythonil{!s}\pythonIdx{"!s}, i.e., by writing \pythonil{f\"\{o!s\}\"}
75+
We find that the simple string representation of a \pythonilIdx{datetime} object is, well, a simple human readable date and time string.
76+
The result of the function \pythonilIdx{repr} for an object~\pythonil{o} can be obtained in an \pgls{fstring} by using the format specifier~\pythonil{!r}\pythonIdx{"!r}, i.e., by writing \pythonil{f\"\{o!1\}\"}.
7877
Doing this with a \pythonilIdx{datetime} object gives us all the information that we need to manually recreate the object.
7978
We could copy the output of \pythonilIdx{repr} from \cref{exec:dunder:str_vs_repr} into the \python\ console!
80-
This would re-create the \pythonil{right\_now} object with the same data.
79+
This would create a \pythonilIdx{datetime} object with exactly the same data as \pythonil{right\_now}.
8180
This would also work with the string representations that we printed for our lists \pythonil{l1} and \pythonil{l2} above.
8281

8382
\gitLoadAndExecPython{dunder:point_user_2}{}{dunder}{point_user_2.py}{}%
8483
\listingPythonAndOutput{dunder:point_user_2}{%
8584
Investigating string representations and equality for the class~\pythonil{Point}.}{}%
8685
%
8786
Let us now move a bit backwards and revisit a previous example we created by ourselves.
88-
In \cref{sec:immutableClassPoints2D} presenting file \programUrl{immutableClassPoints2D}, we created the class \pythonil{Point} for representing points in the two-dimensional Euclidean plane~(see \cref{lst:classes:point}).
87+
In \cref{sec:immutableClassPoints2D} presenting file \programUrl{classes:point}, we created the class \pythonil{Point} for representing points in the two-dimensional Euclidean plane~(see \cref{lst:classes:point}).
8988
This class turned out to be quite useful when we went on to implement classes for different two-dimensional geometric shapes.
9089
Here, we already implemented one dunder method, the initializer~\dunder{init}.
9190
Let us play with this class a bit more.
@@ -94,7 +93,7 @@
9493
\pythonil{p1}~represents the coordinates~$(3,5)$, \pythonil{p2}~stores~$(7, 8)$, and~\pythonil{p3}~has the same coordinates as~\pythonil{p1}.
9594
In this program, we first print the \pythonil{str} and \pythonil{repr} results for~\pythonil{p1}.
9695
We immediately find them very unsatisfying.
97-
Since we implemented neither \dunder{str} nor \dunder{repr}, the default result for \pythonil{str} falls back to the result of \pythonil{repr} which then falls back to just the type name and object~ID.
96+
Since we implemented neither \dunder{str} nor \dunder{repr}, the default result for \pythonil{str} falls back to the result of \dunder{repr} which then falls back to just the type name and object~ID.
9897
This gives us basically no useful information.
9998

10099
While we are on the subject of \inQuotes{not useful,} there is another aspect of our \pythonil{Point} class that does not show useful behavior.
@@ -110,24 +109,25 @@
110109
Vice versa, \pythonil{p1 != p2}\pythonIdx{"!=} should be (and is) \pythonil{True}, but \pythonil{p1 != p3}\pythonIdx{"!=} should be \pythonil{False} but turns out to be \pythonil{True}.
111110

112111
The reason for this is that \python\ cannot know when and why instances of our own class should be equal.
113-
So it simply assumes that equality~$=$~identity, i.e., only identical instances are equal.
112+
So it simply assumes that equality~$=$~identity, i.e., an object is only equal to itself.
114113
We could fix this by implementing the \dunder{eq} dunder method.
115114
This method would receive an arbitrary object~\pythonil{other} as input and should return \pythonil{True} if that is equal to the object whose method was invoked.
115+
Similar to the \pythonil{str(o)} function, which invokes \pythonil{o.__str__()} , the \pythonil{a == b} operator invokes \pythonil{a.__eq__(b)}, if \pythonil{a} implements \dunder{eq}.
116116

117117
If you implement \dunder{eq}, \python\ will make the reasonable assumption that \pythonil{(a != b) == not (a == b)}\pythonIdx{"!=}\pythonIdx{==}, i.e., assume that two objects are unequal if and only if they are not equal~\cite{PEP207}.
118118
However, this is not necessarily always the case\footnote{%
119119
In~\cite{PEP207}, it is stated that IEEE~754 floating point numbers do not satisfy that \pythonilIdx{==} and \pythonil{!=}\pythonIdx{"!=} are each other's complements. %
120120
However, I could not find for an example where this was true in the standard~\cite{IEEE2019ISFFPA}, maybe with the exception of signaling~\pythonilsIdx{nan}, which does not matter in \python. %
121121
Maybe it was true for some \python\ implementations back then, as~\cite{PEP754} indicates.%
122122
}. %
123-
Therefore, \python\ also allows us to implement an \dunder{ne} dunder method to realize inequality differently or, potentially, more efficiently, instead~\cite{PEP207}.
123+
Therefore, \python\ also allows us to implement an \dunder{ne} dunder method which is called by \pythonil{a != b} as \pythonil{a.__ne__(b)} if it is implemented\cite{PEP207}.
124124

125125
Finally, we compare whether \pythonil{p1} is the same as the integer number~\pythonil{5}.
126126
This, obviously, should return~\pythonil{False}.
127127
And it does so.
128128
This is because the two objects~\pythonil{p1} and~\pythonil{5} are not identical.
129-
The default equality comparison only checks for identity.
130-
If implement \dunder{eq} by ourselves, this method should clearly return a value that makes \pythonil{p1 == 5} become~\pythonil{False} as well.
129+
As said, the default equality comparison only checks for identity.
130+
If implement \dunder{eq} by ourselves, this method should also return a \pythonil{False} if it receives \pythonil{5} as argument~(and not crash or raise an exception\dots).
131131
Anything else would be nonsense.
132132

133133
\gitLoadPython{dunder:point_with_dunder}{}{dunder/point_with_dunder.py}{}%
@@ -139,7 +139,7 @@
139139
The same program exploring string representations and equality as shown in \cref{lst:dunder:point_user_2}, but this time using our new \pythonil{Point} class from \cref{lst:dunder:point_with_dunder}.}{}%
140140

141141
In order to fix all of the problems discussed above, we implement the three dunder methods \dunder{str}, \dunder{repr}, and \dunder{eq} for our \pythonil{Point} class in file \programUrl{dunder:point_with_dunder} shown as \cref{lst:dunder:point_with_dunder}.
142-
The concise string representation returned by \dunder{str} will just be the point coordinates in parentheses.
142+
The concise string representation returned by \dunder{str} will just be the point coordinates separated by commas and in parentheses.
143143
This offers all the information needed at a glance, but it could be mistaken with a tuple as string.
144144
Therefore, the canonical string representation produced by \dunder{repr} will return a string of the shape~\pythonil{"Point(x, y)"}.
145145

@@ -149,11 +149,12 @@
149149
%
150150
\cquotation{PSF:P3D:TPSL:BIC}{%
151151
A special value which should be returned by the binary special methods [\dots] to indicate that the operation is not implemented with respect to the other type\dots\medskip\\\strut\hspace{1cm}\strut%
152-
\emph{Note:}~When a binary (or in-place) method returns \pythonilIdx{NotImplemented} the interpreter will try the reflected operation on the other type (or some other fallback, depending on the operator). %
152+
\emph{Note:}~When a binary~(or in-place) method returns \pythonilIdx{NotImplemented} the interpreter will try the reflected operation on the other type~(or some other fallback, depending on the operator). %
153153
If all attempts return \pythonilIdx{NotImplemented}, the interpreter will raise an appropriate exception. %
154154
Incorrectly returning \pythonilIdx{NotImplemented} will result in a misleading error message or the \pythonilIdx{NotImplemented} value being returned to \python\ code.
155155
}%
156156
%
157+
By returning \pythonilIdx{NotImplemented} for \pythonil{other} objects that are not instances of class \pythonil{Point}, we simply defer to the default behavior of the \pythonilIdx{==}~operator.
157158
In other words, our \dunder{eq} method can only compare the current~\pythonil{Point} for equality with another~\pythonil{Point}.
158159
If \pythonil{other} is not an instance of~\pythonil{Point}, then no way to compare for equality with it exists.
159160
Now, we could return \pythonil{False} in this case, which would be fine as well.
@@ -162,8 +163,16 @@
162163
When we implement the \dunder{eq} method like this, the proper \pgls{typeHint} for the return value is \pythonil{bool | NotImplementedType}\pythonIdx{NotImplementedType}.
163164

164165
Program \programUrl{dunder:point_with_dunder_user} given as \cref{lst:dunder:point_with_dunder_user} is the same as \cref{lst:dunder:point_user_2}, but now uses this new variant of our class \pythonil{Point}.
165-
As you can see in \cref{exec:dunder:point_with_dunder_user}, its output now matches much better to what one would expect.%
166-
%
166+
As you can see in \cref{exec:dunder:point_with_dunder_user}, its output now matches much better to what one would expect.
167+
The function \pythonil{str} now gives us concise and informative output when applied to an instance of \pythonil{Point}.
168+
The \pythonil{repr} operator gives us a text that we could copy into the console and that would then re-create the point.
169+
The equality and inequality operations now also behave reasonable and detect whether two points have the same coordinates.
170+
They also work in the presence of non-\pythonil{Point} inputs.
171+
172+
We now have begun our venture into the realm of so-called \emph{dunder} methods.
173+
These methods control much of the behavior of the operators and constructs of the \python\ language.
174+
With \dunder{str}, \dunder{repr}, \dunder{eq}, and \dunder{ne} we already touched several methods that we used quite often, albeit indirectly.
175+
What other adventures may be waiting for us in these deep bowels of the \python\ machine?%
167176
\FloatBarrier%
168177
\endhsection%
169178
%

0 commit comments

Comments
 (0)