|
1 | 1 | \hsection{Basics of Classes}%
|
2 | 2 | %
|
3 |
| -Classes are, basically, a semantic unit of data and code. |
4 | 3 | We learned about simple datatypes in \cref{sec:simplyDataTypesAndOperations} and about collections in \cref{sec:collections}.
|
5 | 4 | However, in many situations, we deal with data that cannot satisfyingly represented by either of them alone.
|
6 |
| -Moreover, often, datatypes and the operations on them form a semantic unit. |
7 |
| -For this purpose, \emph{classes} exist in \python\pythonIdx{class}:% |
| 5 | +Often, datatypes and the operations on them form a semantic unit. |
| 6 | + |
| 7 | +Imagine, for example, you would like to implement mathematical operations for complex numbers.\footnote{% |
| 8 | +\python\ already offers the datatype \pythonilIdx{complex}, but for the sake of the example, assume it does not.} |
| 9 | +Of course, you could represent complex numbers as \pythonil{tuple[float, float]}, but this comes with several drawbacks. |
| 10 | +On one hand, not \emph{every} tuple of two \pythonils{float} is to be understood as a complex number. |
| 11 | +So just from the \pgls{signature} of your functions, i.e., just based on its parameters and return types, it is not immediately clear that your functions are for complex numbers. |
| 12 | +All we can directly see is that they are for tuples of two \pythonils{float}. |
| 13 | +On the other hand, the two parts of a complex numbers, the real part and the imaginary part, have two distinct and different meanings. |
| 14 | +It would not immediately be clear whether the first number of the tuple is the real or the imaginary part. |
| 15 | +Indeed, we could also represent complex numbers in polar form, in which case the two tuple elements would have yet different meanings. |
| 16 | +Also, the standard textual representation of tuples of two \pythonils{float} would be something like~\pythonil{"(3.0, 4.0)"}, whereas we would probably prefer something like~\pythonil{"3+4i"}. |
| 17 | + |
| 18 | +The first important use-cases of \pythoniles{class} in \python\ is that they offer us a way to define a data structure together with the operations for it as one semantic unit. |
| 19 | +This allows us to define a \pythonil{class} for complex numbers which has attributes with the name \pythonil{real_part} and \pythonil{imaginary_part}. |
| 20 | +We can define operators that work with instances of this \pythonil{class}, making it immediately clear how and when they are to be used- |
| 21 | +And this \pythonil{class} can then have default textual representations of our liking. |
| 22 | + |
| 23 | +A second situation where ability to define functions and we have learned so far hits a limit arises with \pglspl{API}. |
| 24 | +Let us be ambitious and imagine that you wanted to create a versatile system that can produce documents. |
| 25 | +On the output side, you want to support different formats, say LibreOffice~\cite{DF2024LTDF,GL2012LTSOOSSCBAFACSOL}, Microsoft~Word~\cite{MS2024MW,DR2019STFAWAUMW}, and Adobe~PDF~\cite{A2024WDPM,A2008P3DMPDFP1P1}. |
| 26 | +On the input side, you would like to provide the user with a uniform way to create documents, to add text paragraphs, to insert graphics, to set the font of texts, and so on. |
| 27 | +This input side \pgls{API} should be the same for all output formats. |
| 28 | +It would not just consist of a single function, but several groups of functions. |
| 29 | +There could even be nested hierarchies of operations that you would like to support, e.g., the ability to divide a text into chapters which can be divided into paragraphs which can be divided into runs of text with different fonts. |
| 30 | +Obviously, these operations will be implemented differently for the different output formats. |
| 31 | +We could try to solve this by making different modules with functions of the same \pglspl{signature} for the different output formats. |
| 32 | +But this would be a huge hassle, in particular since there would be no way to define a central blueprint of \inQuotes{how the \pgls{API} looks like.} |
| 33 | +It can easily lead to inconsistencies during the software life cycle. |
| 34 | +If we slightly change the \pgls{signature} of one function, we need to implement this in all of the modules. |
| 35 | +There also is no way that a tool like \ruff\ could tell is if some module is no longer synchronized due to the lack of a central blueprint. |
| 36 | + |
| 37 | +Classes offer us the necessary abstraction: |
| 38 | +We could specify a base class \pythonil{Document} for document objects that provides methods for the necessary operations, from adding text to aligning figures. |
| 39 | +These operations could just \pythonilIdx{raise} a \pythonilIdx{NotImplementedError}. |
| 40 | +Then, for each output format, we could derive a new subclass from this base class with the actual implementation of the methods. |
| 41 | +The code on the user side could treat all these different document types the same, because all of them would be instances of \pythonil{Document} with the same operations. |
| 42 | +The implementation-specific stuff would all be invisible to the user, exactly as it should be. |
| 43 | +So the second important use case for classes are that they offer us a very nice abstraction for defining and implementing \pglspl{API}. |
| 44 | + |
| 45 | +Classes therefore can solve two important problems where the basic datatypes and plain functions we learned about are not sufficient: |
| 46 | +First, they allow us to semantically and clearly group data and operations together. |
| 47 | +Second, they offer us a simple way to group several operators under one \pgls{API}, which then -- transparently to the user -- can be implemented in different ways. |
| 48 | +In this chapter, we discuss \pythoniles{class} in \python. |
| 49 | +Syntactly, \pythoniles{classes} follow the general blueprint below:% |
8 | 50 | %
|
9 | 51 | \begin{pythonSyntax}
|
10 |
| -class: |
| 52 | +class MyClass: # or `class MyClass(MyBaseClass)` |
11 | 53 | """Docstring of the class."""
|
12 | 54 |
|
13 | 55 | def __init__(self) -> None:
|
|
22 | 64 | # compute something using the attributes
|
23 | 65 | \end{pythonSyntax}
|
24 | 66 | %
|
| 67 | +\bestPractice{className}{\sloppy% |
| 68 | +Class names should follow the CapWords convention (often also called camel case), i.e., look like \pythonil{MyClass} or \pythonil{UniversityDepartment} (not \pythonil{my_class} or \pythonil{university_department})~\cite{PEP8}.}% |
| 69 | +% |
25 | 70 | \hsection{A Simple Immutable Class for Points in the Two-Dimensional Euclidean Plane}%
|
26 | 71 | \label{sec:immutableClassPoints2D}%
|
27 | 72 | %
|
|
95 | 140 | }
|
96 | 141 | These lines create the attributes \pythonil{self.x} and \pythonil{self.y} of the object which was passed in via the parameter~\pythonil{self}.
|
97 | 142 | Additionally, the \pgls{typeHint} \pythonilIdx{Final} from the \pythonilIdx{typing} module annotates a variable as immutable~\cite{PEP591}.
|
98 |
| -In other words, we do not allow the coordinates of our \pythonils{Point} to be change after creation.% |
| 143 | +In other words, we do not allow the coordinates of our \pythonils{Point} to change after object creation.% |
99 | 144 | %
|
100 | 145 | \bestPractice{attributeTypeHint}{%
|
101 |
| -Every attribute of an object must be annotated with a \pgls{typeHint} and a documentation comment when created in the initializer~\pythonilIdx{\_\_init\_\_}\pythonIdx{dunder!\_\_init\_\_}. % |
102 |
| -\pglspl{typeHint} work as with normal variables, but the documentation is slightly different: % |
103 |
| -In the line \emph{above} the attribute initialization, a \emph{comment} starting with \pythonilIdx{\#: } must be written which explains the meaning of the attribute.% |
| 146 | +Every attribute of an object must be annotated with a \pgls{typeHint} and a documentation comment when created in the initializer~\pythonilIdx{\_\_init\_\_}\pythonIdx{dunder!\_\_init\_\_}~\cite{LM2024WTMD}. % |
| 147 | +\pglspl{typeHint} work as with normal variables.}% |
| 148 | +% |
| 149 | +\bestPractice{attributeDocstring}{% |
| 150 | +An attribute is documented in the line \emph{above} the attribute initialization by writing a \emph{comment} starting with \pythonilIdx{\#: }, which explains the meaning of the attribute~\cite{SD2024DCAD}. % |
| 151 | +(Sometimes, the documentation is given as string directly below the attribute definition~\cite{PEP287}, but we stick to the former method, because it has proper tool support, e.g., by~\sphinx.)% |
104 | 152 | }%
|
105 | 153 | %
|
106 |
| -We can now do something like \pythonil{p = Point(1, 2)} and it will create the object~\pythonil{p} which is an instance of the \pythonilIdx{class} \pythonil{Point}. |
| 154 | +After properly defining our initializer, we can now do something like \pythonil{p = Point(1, 2)} and it will create the object~\pythonil{p} which is an instance of the \pythonilIdx{class} \pythonil{Point}. |
107 | 155 | This will allocate the memory for~\pythonil{p} and automatically invoke \pythonil{\_\_init\_\_(p, 1, 2)}.
|
108 | 156 | As a result, \pythonil{p.x} will have the value~\pythonil{1} and \pythonil{p.y} will have value~\pythonil{2}.
|
109 | 157 | Notice that we can immediately see from the knowledge that \pythonil{p} is an instance of \pythonil{Point} that \pythonil{p.x} and \pythonil{p.y} are its x\nobreakdashes-\ and y\nobreakdashes-coordinate, respectively.
|
|
122 | 170 | If you compute \pythonil{Point(1, 1).distance(Point(4, 4))}, then the expected result is something like~4.243.%
|
123 | 171 | %
|
124 | 172 | \bestPractice{methodDocstring}{%
|
125 |
| -All methods of \pythoniles{class} must be annotated with \pglspl{docstring}.% |
| 173 | +All methods of \pythoniles{class} must be annotated with \pglspl{docstring} and \pglspl{typeHint}.% |
126 | 174 | }%
|
127 | 175 | %
|
128 | 176 | We could now go on and add more methods that do reasonable computations with instances of~\pythonil{Point}.
|
|
0 commit comments