OOP in Python, part 20: Highlights, and beyond this series
MP 67: What were the most important takeaways? What would come next?
Note: This is the final post in a series about OOP in Python.
This series has covered a lot, and there’s a bunch more that could be covered. In this final post we’ll look back on the most important points that have been discussed. I’ll also share some topics you might want to learn more about in order to build on what was covered here.
In case you haven’t read through the whole series, or if you’ve skimmed it but want to review the most important parts, I thought it would be beneficial to highlight the posts and topics that are most significant. These are topics that people are often unclear about, and concepts that tend to be relevant to the kinds of projects that people work on.
MP 37: What’s so special about
self in almost every class you ever look at. A clear understanding of what
self represents will help you understand the structure of other people’s classes better. It will also allow you to build out more complex class relationships, that serve to make your projects simpler overall.
MP 39: The
Almost every class you write will have an
__init__() method. The
__init__() method is called automatically when you make an instance of an object, but how does that happen? If you have a clear understanding of the mechanics behind this, you’ll be able to troubleshoot issues with
__init__() more easily. You’ll also have a better sense of how you can use
__init__() to build the kind of instances you want to from your classes.
MP 47: Comparing objects
While not a critical topic, I think this post is really interesting. It gets into ways we can solve problems we care about by considering the tools we already have at hand.
Many of us have seen code like this:
projects_root = Path("/Users/eric/projects")
dsd_dir = projects_root / "django-simple-deploy"
The forward slash in the second line combines two paths to create one longer path, which is then assigned to
dsd_dir. But this behavior is implemented by overriding a built-in method that was originally used for implementing division. If you understand this approach, you can use symbols to implement just about any behavior you want with objects of your own classes.
MP 49: Helper methods
If you’ve written classes that do work with any degree of complexity, it’s likely you’ve written some methods that are so long they became harder to reason about. Moving some of that work to helper methods increases the readability of your code, and makes it easier to maintain your codebase over time.
MP 52: Inheritance
There are a number of posts in this series about inheritance, the first of which covers
super(). People tend to use
super() without really understanding it very well. That’s fine when you’re writing your first few classes involving inheritance. But if you’re continuing to write or maintain code that uses inheritance, a clear understanding of what
super() is and how it works is beneficial.
MP 64: Composition and inheritance
MP 63 introduces composition, but MP 64 shows how composition and inheritance can be used together to model real-world things and concepts in a clear and maintainable way. Many people use composition without realizing it. But recognizing when you’re using it, and knowing why it’s a good thing to use, will help you justify some of your decisions about how to structure your code.
Other posts in the series
You can see the full listing of posts in this series here. Some of the posts I didn’t mention cover static and class methods, other dunder methods such as
__new__(). A group of posts later in the series looks at class hierarchies in real-world projects such as marshmallow, Python’s exceptions, the pathlib module, and Matplotlib.
Beyond this series
There’s a world of OOP topics beyond what was covered in this series. I’ll cover some of these topics in individual posts later on, and in the context of other discussions. I may write another series that picks up where this one leaves off, but that won’t be for some time.
If you’re interested in learning more about OOP right now, here are a few of the topics I’d discuss if I was taking the series a little further:
When you create an instance of a class, Python creates a dictionary to keep track of the attributes that have been defined for that instance. This is a straightforward way to keep track of existing attribute names and values, and it also allows you to define new attributes for an instance whenever you need to.
Dictionaries are appropriate data structures to use if you need to keep adding key-value pairs, but they’re not the most efficient way of storing a set number of key-value pairs. If you know ahead of time exactly what attributes will be needed for every instance of a class, you can specify those attributes using
__slots__. You won’t be able to define new attributes after an object has been created, but Python will use a more efficient data structure to store attribute names and values. This is most beneficial when a large number of instances are being created from a class.
A common pattern when using OOP in Python involves listing a series of arguments in the
__init__() method, and then immediately casting those arguments to attributes:
def __init__(self, route, rating, fa_date):
self.route = route
self.rating = rating
self.fa_date = fa_date
A dataclass automates this structure:
This code will automatically create an
__init__() method that accepts these arguments, and defines them as attributes.
To learn more about dataclasses, see the main reference page in the Python docs.
One strength of OOP is the tremendous flexibility it offers in how you model the real world in classes, and the flexibility in how you use instances of those classes. One weakness of this approach is that instances can be modified after they’ve been created. When you receive an object from another section of code, you don’t really know what you’ll find there. Unless you trust the code that generated the object, you’ll have to inspect the object before using it.
Functional programming is a different way of thinking about programming. One of the core tenets of functional programming is that the elements you work with should be immutable. If you need to change something such as a set of values, you usually create a new set of values based on the original set, rather than modifying the original set. In this approach, once something has been created there’s no way for any other code to change that element.
It’s generally considered harder to learn functional programming than a paradigm like OOP. But when you can write code in a functional way, it’s easier to verify the behavior of the code you’ve written. You can be more confident that your code is “free from side effects”, meaning you won’t have surprising and unintended changes made to elements of your code.
This has been a long series, but OOP is one of the most important things to understand in Python. Even if you don’t write many classes yourself, everything in Python is an object. That means most of the code you work with is based on classes that other people have written. Ultimately, understanding the fundamentals of OOP will help you make more sense of the code you work with.
If you have questions or comments about the series as a whole, please post them below. If you’d like to see a specific aspect of OOP covered in more depth, please feel free to reply to this email. And if you know of someone who might benefit from what’s been covered in this series, please consider sharing this post.