You Need To Understand Classes
When I started learning to code in Python, I was fairly quickly introduced to the concept of classes. Classes aren't as pervasive in Python as they are in Java or C++, but they do come up a lot. And they weren't very well explained.
The most common explanations I've come across for classes fall into two camps. One tries to explain them in terms of Platonic forms. But even if you've read a bit of Plato or learned some philosophy in university, this is an obviously terrible way to explain classes. Classes are real, at least in code, whereas Platonic forms are, by definition, an imaginary concept. This explanation gets confusing and distracting really quickly.
Now, what these explanations are trying to get at is that a class is a kind of template for behaviour, that can be modified by the objects that instantiate, and inherited by child classes.
It's my opinion that this is still a terrible way to explain classes, even if you use a more practical example, because it still doesn't get at what a class is in terms of code. It doesn't explain why a class might be considered more suitable than some other alternative, when you would choose to create a class instead of writing lots of procedural code, or what the computer does with a class. Believe it or not, that last part can be really crucial for learners to understand, in order to fit classes into a coherent mental model.
So after years of working with code and learning to understand what classes or for, I've developed a way of explaining classes that I find to be much more effective. Are you ready? Here it is: a class is a data structure with functions attached.
That's it. If you understand what a data structure is, a structured container with data in it, and you understand what a function is, a procedure that takes inputs and returns outputs, (see my article on those for details), then you can understand what a class is.
Of course, classes in OOP languages, with a lot of other features attached, do have more complexity than this. Inheritance is a complex feature, which requires the compiler to evaluate which parts of the base class and the child classes to compose. Constructors often have features in higher level languages that handle memory management and deallocation. And don't even try to understand the way Python classes work, with their free access to modify them after definition.
But at it's core, a class is a data structure with functions attached. Now, if you are struggling to understand classes, it's possible that this explanation isn't enough to understand the concept on its own. For the rest of this essay I will show some examples that illustrate this point. But if you hold on to this one concept, it will give you a good mental model for starting to understand classes.
Classes in C
It's true that classes don't exist in C. Like functions, they are a construct imposed on the underlying machine code. There are no primitives for classes in Assembly. Nevertheless, class-like behaviour can be simulated to some extent in C. It can be helpful to see at least a hint of how this is done, in order to better understand what a class is.
Structs
You might be familiar with the sorts of data structures which contain a lot of homogenous members. Lists are like this, where you just pack more values in and they automatically get new indices. Even hashmaps or dictionaries are like this, as key-value pairs can be added indefinitely without any differentiation.
A struct is different. A struct in C first has to be defined, with each member named, and the type associated with it declared. The members can then be retrieved, using those names. Here's an example:
typedef struct {
int count;
char** words;
} Sentence;
That's the type definition. We can then access those members using the names, rather than using indices or pointers, as we would on other data structures, like this:
#include <stdio.h>
#include <stdlib.h>
int main() {
Sentence sentence;
sentence.count = 0;
sentence.words = malloc(10 * sizeof(char *));
printf("Count: %d\n", sentence.count);
return 0;
}
As you can see, dot notation now allows us to access members of the struct. The compiler knows the memory layout of the struct in advance, so it is able to calculate the distances using those names. You might recognise the dot notation from higher level languages like Python or JavaScript. This is where it comes from.
Attributes
It should be clear by now that these struct members are called attributes in OOP languages. Let's have a look at some examples of members in classes to compare.
Python
Here's an example in Python:
>>> class Sentence:
... def __init__(self):
... self.count = 0
... self.words = []
The term self refers to the instance of a class. In order to explain what this is, I need to show you some quirks about the way Python behaves. Before I do that, however, I just want you to notice the similarities between this structure and the struct in C.
In C, we created an instance of a struct with the line Sentence sentence;. Then we initialised the members, and we were able to access them with dot notation, as in sentence.count. The constructor shown above in Python, the __init__ method, does this for us when we initialise the class, and we can use the same syntax to access the attributes, like this:
>>> s = Sentence()
>>> s.count
0
The dot notation works exactly the same way in Python as in C. The constructor does the work of giving initial values to the members, saving us from needing to do it manually, as we did in the C example.
But Python also has some interesting design choices, that are worth observing before we look at a more typical object-oriented language. Python does not restrict access to its members. This is similar to the way C behaves. If you have a reference to a struct in C, you can access its members and change them. This is often considered bad practice in OOP languages, where access should usually take place through special methods. The argument is that this allows the class to specify when an attribute should be updated, allows additional behaviour and checking to accompany updates, and potentially allows for restricting who can update them.
But Python does something else a bit strange. Because it is a dynamically typed language, you can add attributes after a class is defined. This leads to some fun REPL sessions like this one:
>>> s = Sentence()
>>> s.count
0
>>> s.count = 5
>>> s.count
5
>>> s.new_attr = "random string"
>>> s.new_attr
'random string'
As you can see, not only can we update members without using a function to access them, we can also add entirely new attributes.
Now, back to the self keyword. This distinguishes instance from class. What does that mean?
When we define a class, we are essentially defining a template. The template says, "whenever you create a Sentence, it should look like this". In our case, the Sentence will always have a constructor method, that gets called when we create it, along with any other functions attached to the method, and it will have any class attributes that we define as well.
When we actually create a Sentence, we get an "instance". And an instance has a specific count of words, and it has particular words added to the list.
We can add class attributes when we define a class. That would look like this:
>>> class Sentence:
... sentence_complete = False
... def __init__(self):
... self.count = 0
... self.words = []
...
Notice that sentence_complete is defined outside of the __init__ constructor, and it doesn't attach to self. (sentence_complete is not a good example of a static variable, as it relates to the contents of the instance and should really belong to it, but this at least illustrates how the code works.) This means that it is available on the class itself. It's what is known as a static variable in OOP languages. We can access it outside of an instance, but each instance will also have its own copy of the variable.
>>> Sentence.sentence_complete
False
>>> s = Sentence()
>>> s.sentence_complete
False
>>> s.sentence_complete = True
>>> s.sentence_complete
True
>>> Sentence.sentence_complete
False
We can access the attribute without creating an instance. We can also access it on an instance and modify it, without modifying the value of the class version. The reverse is not true, however. We can't access instance attributes on a class.
>>> Sentence.count
Traceback (most recent call last):
File "<python-input-22>", line 1, in <module>
Sentence.count
AttributeError: type object 'Sentence' has no attribute 'count'
When to use static variables and when to use instance variables is a matter of judgement. When in doubt, you should usually use instance variables.
Now that you understand the difference, you should be able to understand what self is doing. The self keyword refers to the instance. It is passed in to any instance-specific functions, so that those functions have access to the memory where your instance has been created, and can access and update the attributes in there.
We'll see how this applies to other functions in a class soon, but first let's briefly compare this Python example with an example in Java, to show another way that classes can be handled.
Java
I now have the opportunity to present to you a delightfully evil little piece of software called JShell. That's right, you might have thought a clunky enterprise language like Java, where literally everything is a class. wouldn't have a REPL, but it turns out it does! (You should feel concerned for my sanity.) Here's an example of a basic class in it:
jshell> public class Sentence {
...> private int count;
...> private List <String> words;
...> }
We've now defined a class with two attributes. The private keyword tells us that the value of those variables is not allowed to be updated from outside of the class. You have to use special methods to do that.
Note that this is not the same as the distinction between class, or static, variables, and instance variables. To make a static variable in Java, that we can access the same way as the one in Python, we need the static keyword, and we need to make it public.
jshell> public class Sentence {
...> public static boolean sentenceComplete;
...> private int count;
...> private List<String> words;
...> }
jshell> Sentence.sentenceComplete
$12 ==> false
jshell> Sentence.sentenceComplete = true
$13 ==> true
jshell> Sentence.sentenceComplete
$14 ==> true
Java developers usually frown very heavily upon programmers who create public static variables. They are usually a sign of poor design. The word "encapsulation" describes the practice of discreetly tucking variables away into classes and only allowing them to be accessed through the appropriate methods. Whether or not to use variables in this way is a design discussion, but it's worth understanding the different mechanisms.
To make this Java example complete, let's add a constructor.
jshell> public class Sentence {
...> public static boolean sentenceComplete;
...> private int count;
...> private List<String> words;
...>
...> public Sentence() {
...> count = 0;
...> words = new ArrayList<String>();
...> }
...> }
I won't go into depth on what's going on here. If you really want to learn Java, there are plenty of good resources online for that. But this constructor is doing the same thing that the one in Python is, which is giving an initial value to the variables. Notice again, that this is what we had to do manually after creating a struct in C. And, just like in C and Python, we can access these members via dot notation, although in the case of Java we can only do that with public members.
One final note on the differences between Java and Python before we move on: Python uses the word self to refer to the instance when accessing members. That word originally comes from SmallTalk, an old programming language which contributed a lot of ideas to later object-oriented languages. I've been told that it makes some very interesting design decisions that are worth studying. Java, following, as it typically does, in the footsteps of C++, uses the keyword this, which is also used in JavaScript. In Java, however, it can often be omitted, as you see in the above example. Java blurs the lines when using this, as it can be used to access static or instance variables, and can be omitted. The point remains, that the instance still needs a way to access its own members. This is handled automatically by the compiler, rather than explicitly passing around self as Python does. The difference will become even clearer when we get to methods.
To recap, a class can have variables set on it which retain their values for every instance of a class that is created, and an instance can have its own specific variables which are different for every instance. It's worth understanding how to access each of these, and when they will exist. Which ones to use in each particular case is a question of design.
JavaScript
If you're a JavaScript programmer, you might be wondering how that language handles classes. The real answer is, it doesn't. JavaScript has what are called prototypes. If you new to a lot of this, it's ok to think of prototypes as similar to classes for now. You can still use the class keyword to create one, and you can use dot notation to access the members of an object. As you learn to understand JavaScript better, you will find ways of programming that don't involve classes, and are arguably a better design for that language. But for now, you can still apply some of the same principles. A class is a data structure that holds attributes and functions that can be accessed with dot notation outside of the class itself. (As a side note, the dot notation is only a convention, that has been inherited from C. Other languages do have other ways of handling this, but C has made dot notation popular and widely used.)
Function Pointers
When I started this article I promised that I could explain classes as data structures with functions attached. It's time to examine that.
Function Pointers in C
Pointers to functions in C are possible, but not fun to work with. The language was built as a human readable wrapper around Assembly, where, as I explained here, functions are just collections of procedural code with arguments loaded into registers for access.
Still, function pointers do exist. Let's take a quick look at one.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int count;
char** words;
int (*inc_func_ptr)(int);
} Sentence;
int increment(int n) {
return n + 1;
}
int main() {
Sentence sentence;
sentence.count = 0;
sentence.words = malloc(10 * sizeof(char *));
sentence.inc_func_ptr = &increment;
printf("Count: %d\n", sentence.count);
sentence.count = sentence.inc_func_ptr(sentence.count);
printf("Count: %d\n", sentence.count);
free(sentence.words);
return 0;
}
If you run this, you will see a count of 0 in the first line, and a count of 1 in the second line. Obviously this is a bit of a pointless example, but it illustrates how to attach a function to a struct.
As you can see, we create a function pointer as one of the members of the struct. To create a function pointer in C, the formula is [return type] (*member name)(argument types). The name of the function, which is the name that we will be referring to to access it, has to go into parentheses for a function pointer. The arguments go in parentheses as types only, since we don't know parameter names when this is a generic template for a function. (This is actually also what a function declaration looks like in some of the older versions of C. Go and read K&R if you want to know more, it's a great book.)
Note that this function pointer does not have a body. There's no definition, just a pointer, which has to be filled in later with a function body. Note also, that there is no self or this keyword, referring to the struct. There is no way for this function to access the struct members. That's why I had to pass the count variable in as a parameter.
It is possible to get around this. You will find some good discussion and sources on the topic here. But I couldn't get it working, and it's honestly a pointless and annoying exercise. The main thing to understand from this is that it is possible to add a function to a struct.
Python
Let's instead have a look at how to do the same thing in Python. We've already seen how to add a constructor to a class in Python, now we'll see how to add regular functions. (Side note, functions attached to classes are traditionally referred to as methods, that's why you will see both names come up.)
>>> class Sentence:
... def __init__(self):
... self.count = 0
... self.words = []
... def add_word(self, word):
... self.words.append(word)
... self.count += 1
... def get_count(self):
... return self.count
... def join(self, delimiter=' '):
... return str(delimiter).join(self.words)
I'm doing this in the REPL, which is why there are no newlines between methods. If you were writing this in a file, you could format it according to PEP-8 guidelines. It's a small example, so this shouldn't be too hard to read.
Notice the difference between this and the function pointer in the C struct. If we look at the simplest method, get_count, (which we don't need as Python attributes are openly accessible, but we'll ignore that for the sake of an example), self gets passed in as a parameter, and the method is able to access a value from that object. But we don't need to pass in self when we call it.
>>> s = Sentence()
>>> s.get_count()
0
As you can see, we don't pass any arguments to the method. The Python runtime handles the task of passing it for us. This is one of the selling points of object-oriented programming. You get a template that automatically bundles the same data types with functions that will only operate on them, without the user needing to worry about the internal details.
As you can see from a couple of the other methods on this class, we can combine behaviours. So instead of directly accessing count and updating it, (which we can because ... Python), we ensure that it gets updated when a word is added. The join method also allows us to specify a delimiter, or use the default one, with a keyword argument. These examples are basic, but they illustrate the power of combining functions and data. Again, whether you think this is a good thing, or you prefer functional programming, is a question of design. At least now you can see how the mechanisms work.
Java
Let's have a quick look at the equivalent example in Java, to round out this picture.
jshell> public class Sentence {
...> private int count;
...> private List<String> words;
...>
...> public Sentence() {
...> count = 0;
...> words = new ArrayList<String>();
...> }
...>
...> public void addWord(String word) {
...> words.add(word);
...> count++;
...> }
...>
...> public int getCount() {
...> return count;
...> }
...>
...> public String join() {
...> return String.join(" ", words);
...> }
...> }
| created class Sentence
(I actually wrote this whole class the first time barely looking at the docs, after not touching Java for a couple of years. You should definitely feel concerned for my sanity. It's not healthy to keep that much boilerplate in your brain!)
The class works the same way as the Python code, although of course access is actually properly restricted this time, and the getter really is necessary:
jshell> Sentence sentence = new Sentence()
sentence ==> Sentence@439f5b3d
jshell> sentence.count
| Error:
| count has private access in Sentence
| sentence.count
| ^------------^
jshell> sentence.getCount()
$3 ==> 0
jshell> sentence.addWord("Hello")
jshell> sentence.addWord("world")
jshell> sentence.getCount()
$6 ==> 2
jshell> sentence.join()
$7 ==> "Hello world"
As you can see, the class encapsulates the data, in this case the count and the list of words, and the methods automatically operate on those data without the need for you to know about them or control how they are updated.
Again, a lot of people argue about whether this is good design or not these days, but you will almost certainly come across object-oriented code in your career in software, so it's worth understanding the concept.
Functions Without Classes
As one final illustration, here is a comparison of a class method in Python with an ordinary function. Let's take the string method for splitting as an example.
>>> s = "This is a sentence"
>>> s.split()
['This', 'is', 'a', 'sentence']
You might not know this, but a string is an object in Python. In C, a string is an array of characters. In Python the concept is similar, but, since it's an object, it also has a number of methods attached to it.
Pay attention to this. Strings are a built in type of object in Python, with their own methods. You access the methods with dot notation, and you don't have to pass in the string, because the runtime handles passing it into the method for you with the self keyword in the background. You can even call the method directly on a string:
>>> "This is a sentence".split()
['This', 'is', 'a', 'sentence']
Now, let's look at a function that splits a string without using a class:
>>> def split(sentence: str) -> list[str]:
... words = []
... word = ""
... for char in sentence:
... if char == " ":
... words.append(word)
... word = ""
... else:
... word += char
... words.append(word)
... return words
...
>>> split("This is a sentence")
['This', 'is', 'a', 'sentence']
This time, we do have to pass in the string in order to split it. (I could also have specified an optional delimiter, which is what the original method does, but I didn't bother for the sake of this demonstration.) The data is not connected to the function. This makes the function more reusable, but also slightly less convenient to use, and arguably easier to abuse.
Again, there are lengthy discussions about classes vs functions. The point of this is just to show you how they work.
Conclusion
If you've read my articles on environments and functions, you may notice a connection between all these concepts. What a class is doing, is providing a specific environment for a function to operate in. When you call a class method, you tell the runtime to execute code at the location where that function exists. The runtime knows where to find it because a class provides a specific memory layout for it to follow. When it executes the function, it ensures that the instance being called is within the scope of that function, allowing it to access and update the instance members.
This has been a lengthy article, with a lot of detail about the specifics of four different programming languages. I hope you've learned something from it. (You have no idea how much effort it took to escape all that code to be HTML-safe, so I hope it was worth it!) If you kept up, well done. If you didn't, that's ok. For now, remember, a class is a data structure with functions attached which operate on that data. Go and write some classes and functions and come back when you're feeling a bit more confident, to revisit this information.