|
24 | 24 | % |
25 | 25 | In \python, we can distinguish two forms of string representations of a given object~\pythonil{o}:% |
26 | 26 | % |
27 | | -\begin{itemize}% |
| 27 | +\begin{enumerate}% |
28 | 28 | % |
29 | 29 | \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.% |
33 | 33 | % |
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}. |
35 | 35 | 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. |
36 | 36 | \pythonil{repr(o)} invokes the \dunder{repr} dunder method, if it has been implemented. |
37 | 37 | Otherwise, it returns the default representation, which is the type name and ID of the object.% |
38 | 38 | % |
39 | | -\end{itemize}% |
| 39 | +\end{enumerate}% |
40 | 40 | % |
41 | 41 | These two functions are compared in program \programUrl{dunder:str_vs_repr} given as \cref{lst:dunder:str_vs_repr}. |
42 | 42 | 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"}. |
44 | 44 | 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. |
45 | 45 |
|
46 | 46 | 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. |
48 | 48 | Printing \pythonil{repr(the_str)}, however, produces~\textil{'123'}. |
49 | 49 | Notice the added single quotation marks on each side? |
50 | 50 | These are necessary. |
|
68 | 68 | We will not discuss this class here in any detail. |
69 | 69 | It suffices to know that instances of this class represent a combination of a date and a time. |
70 | 70 | 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{% |
72 | 72 | 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.} |
73 | 73 |
|
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\}\"}. |
78 | 77 | Doing this with a \pythonilIdx{datetime} object gives us all the information that we need to manually recreate the object. |
79 | 78 | 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}. |
81 | 80 | This would also work with the string representations that we printed for our lists \pythonil{l1} and \pythonil{l2} above. |
82 | 81 |
|
83 | 82 | \gitLoadAndExecPython{dunder:point_user_2}{}{dunder}{point_user_2.py}{}% |
84 | 83 | \listingPythonAndOutput{dunder:point_user_2}{% |
85 | 84 | Investigating string representations and equality for the class~\pythonil{Point}.}{}% |
86 | 85 | % |
87 | 86 | 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}). |
89 | 88 | This class turned out to be quite useful when we went on to implement classes for different two-dimensional geometric shapes. |
90 | 89 | Here, we already implemented one dunder method, the initializer~\dunder{init}. |
91 | 90 | Let us play with this class a bit more. |
|
94 | 93 | \pythonil{p1}~represents the coordinates~$(3,5)$, \pythonil{p2}~stores~$(7, 8)$, and~\pythonil{p3}~has the same coordinates as~\pythonil{p1}. |
95 | 94 | In this program, we first print the \pythonil{str} and \pythonil{repr} results for~\pythonil{p1}. |
96 | 95 | 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. |
98 | 97 | This gives us basically no useful information. |
99 | 98 |
|
100 | 99 | 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 | 109 | 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}. |
111 | 110 |
|
112 | 111 | 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. |
114 | 113 | We could fix this by implementing the \dunder{eq} dunder method. |
115 | 114 | 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}. |
116 | 116 |
|
117 | 117 | 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}. |
118 | 118 | However, this is not necessarily always the case\footnote{% |
119 | 119 | 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. % |
120 | 120 | 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. % |
121 | 121 | Maybe it was true for some \python\ implementations back then, as~\cite{PEP754} indicates.% |
122 | 122 | }. % |
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}. |
124 | 124 |
|
125 | 125 | Finally, we compare whether \pythonil{p1} is the same as the integer number~\pythonil{5}. |
126 | 126 | This, obviously, should return~\pythonil{False}. |
127 | 127 | And it does so. |
128 | 128 | 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). |
131 | 131 | Anything else would be nonsense. |
132 | 132 |
|
133 | 133 | \gitLoadPython{dunder:point_with_dunder}{}{dunder/point_with_dunder.py}{}% |
|
139 | 139 | 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}.}{}% |
140 | 140 |
|
141 | 141 | 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. |
143 | 143 | This offers all the information needed at a glance, but it could be mistaken with a tuple as string. |
144 | 144 | Therefore, the canonical string representation produced by \dunder{repr} will return a string of the shape~\pythonil{"Point(x, y)"}. |
145 | 145 |
|
|
149 | 149 | % |
150 | 150 | \cquotation{PSF:P3D:TPSL:BIC}{% |
151 | 151 | 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). % |
153 | 153 | If all attempts return \pythonilIdx{NotImplemented}, the interpreter will raise an appropriate exception. % |
154 | 154 | Incorrectly returning \pythonilIdx{NotImplemented} will result in a misleading error message or the \pythonilIdx{NotImplemented} value being returned to \python\ code. |
155 | 155 | }% |
156 | 156 | % |
| 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. |
157 | 158 | In other words, our \dunder{eq} method can only compare the current~\pythonil{Point} for equality with another~\pythonil{Point}. |
158 | 159 | If \pythonil{other} is not an instance of~\pythonil{Point}, then no way to compare for equality with it exists. |
159 | 160 | Now, we could return \pythonil{False} in this case, which would be fine as well. |
|
162 | 163 | When we implement the \dunder{eq} method like this, the proper \pgls{typeHint} for the return value is \pythonil{bool | NotImplementedType}\pythonIdx{NotImplementedType}. |
163 | 164 |
|
164 | 165 | 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?% |
167 | 176 | \FloatBarrier% |
168 | 177 | \endhsection% |
169 | 178 | % |
|
0 commit comments