Objects and Classes
The problem classes solve — data and the behaviour that belongs to it in one place — how to write one, and the many cases where a function and a dictionary are the better answer.
The problem classes solve — data and the behaviour that belongs to it in one place — how to write one, and the many cases where a function and a dictionary are the better answer.
You are passing the same four values into every function:
def make_caption(name, city, taken_at, size): ...
def is_large(name, city, taken_at, size): ...
def archive(name, city, taken_at, size): ...They travel together because they are one thing — a photo — and your code keeps taking it apart and putting it back together. Then someone adds a fifth field and you edit eleven function signatures.
A class lets you say "these belong together, and here is what
they can do". By the end of this lesson you will write one, know
what self is, and — the part most tutorials skip — know the
several common cases where a class is the wrong answer.
class Photo:
def __init__(self, name, city, size):
self.name = name
self.city = city
self.size = size
def caption(self):
words = self.name.replace("_", " ").replace(".jpg", "")
return f"{words.title()}, {self.city}"
def is_large(self):
return self.size > 1000Using it:
photo = Photo("sunrise_over_lisbon.jpg", "Lisbon", 1450)
print(photo.name) # sunrise_over_lisbon.jpg
print(photo.caption()) # Sunrise Over Lisbon, Lisbon
print(photo.is_large()) # TrueA class is a description of a kind of thing. An object
(or instance) is one actual thing made from that
description. Photo is the class; photo is an object.
The functions defined inside a class are called methods, and
you have been calling methods since the strings lesson —
name.upper() is a method on a string object. This is what was
underneath all along.
self is the one piece of syntax that confuses everyone, and it
is simpler than it looks.
When you write photo.caption(), Python calls the caption
method and passes the object itself as the first argument. So
inside the method, self is this particular photo.
photo.caption() # what you write
Photo.caption(photo) # what actually happensThat is why every method's first parameter is self, and why
you never pass it yourself.
self.name means "the name belonging to this object". Without
the self., you are talking about a local variable inside the
method, which is a genuine and common bug:
def rename(self, new_name):
name = new_name # a local variable. Does nothing.
self.name = new_name # this is what you meant__init__ is the method that runs when you create an object.
Its job is to attach the starting values. The double underscores
mark it as one of Python's special methods rather than something
you call directly — you never write photo.__init__(), you
write Photo(...) and Python calls it for you.
Make one and print it:
That is useless, and the fix is one method:
__repr__ is what Python shows when it needs a text version for
a developer — at the prompt, in a list, in a debugger, in a log
line. The !r inside the f-string asks for the repr of each
value, which is what puts the quotes around the strings.
Write one for every class you define. It costs a line and it is the difference between a debugging session where you can see your data and one where you are looking at memory addresses.
Two other special methods earn their place early:
__eq__ decides what == means for your objects — without it,
two photos with identical fields are not equal, because the
default compares identity rather than contents.
Tutorials teach classes and then imply everything should be one. Most Python is not written that way, and reaching for a class too early produces code that is harder to read than the version it replaced.
Bad — a class that is a function wearing a costume.
Good — a function.
The class holds no state that lives between calls — separator
is an argument that has been moved somewhere less visible. So
the object exists only to be created and immediately used, which
is two steps and one extra name to do what one function call
does. The test that catches this: if a class has an __init__
and one other method, it is a function.
Two more cases where something simpler wins:
Just carrying data. If there is no behaviour, a
dataclass gives you the same thing without the boilerplate:
That generates __init__, __repr__ and __eq__ for you. It
is covered properly in the intermediate course; know it exists
so you do not hand-write those three methods forever.
Grouping related functions. That is what a module is for.
A class whose methods never touch self is a module with extra
punctuation.
Two questions settle it, and only one of the four answers is a class.
Values that travel together and do nothing on their own.
The one case that earns it.
Pass the values. There is nothing to hold on to.
__init__ plus one method is a function with extra steps.
A class can build on another:
RawPhoto gets everything Photo has. super() refers to the
parent, so super().__init__(...) runs the parent's setup
rather than repeating it, and super().caption() extends rather
than replaces.
Inheritance is genuinely useful and routinely overused. The
question to ask is whether the child is a kind of the
parent, and whether it can be used anywhere the parent can. A
RawPhoto is a Photo, so this is fine.
The failure looks like this: class PhotoDatabase(Photo),
because the database has photos in it.
Inheritance fits.
It can be used anywhere a Photo can. Everything the parent
offers still makes sense on the child.
Not a true sentence.
A database is not a kind of photo — it holds photos. That is composition.
Inheriting anyway gives the database every photo method,
including caption() and is_large(), which mean nothing on
it.
When unsure, prefer holding an object to inheriting from it.
You can now define a type of your own, give it data and behaviour, make it print usefully, and extend one class from another. Just as valuable, you have three tests for when not to — because knowing that most Python is functions and modules, with classes where they earn their place, is what stops you writing Java in Python.
Next is The Standard Library You Will Actually Use, a tour
of what already ships with Python. You have met pathlib,
json, csv and collections in passing; that lesson covers
them properly along with the others worth knowing, so you stop
writing code that already exists.
Before you move on, take a group of values your code passes
around together and turn it into a class with one real method
and a __repr__. Then look honestly at whether it improved
anything. Sometimes the answer is no, and noticing that is the
skill this lesson is actually teaching.
DEFINING
class Photo:
def __init__(self, name, size):
self.name = name per-object values go here
self.size = size
def caption(self): a method; self is this object
return self.name.title()
photo = Photo("dawn.jpg", 1450) __init__ runs
photo.caption() -> Photo.caption(photo)
SELF
self = this particular object
self.name = the attribute on it
name = a local variable <- the common bug
never pass self yourself
SPECIAL METHODS WORTH KNOWING EARLY
__init__ set up a new object
__repr__ how it prints for a developer <- always write one
__eq__ what == means
__len__ what len() means
INHERITANCE
class RawPhoto(Photo):
super().__init__(...) run the parent's setup
super().caption() extend rather than replace
ask: is the child really a KIND of the parent?
prefer holding an object to inheriting from it
WHEN NOT TO USE A CLASS
__init__ plus one method -> that is a function
data with no behaviour -> a dataclass, or a dict
methods ignoring self -> that is a module
use one when data and behaviour belong together
AND the data outlives one call
TRAP
values in the class body are shared by every instance
per-object state goes in __init__print(photo)
# <__main__.Photo object at 0x104f3a2d0>class Photo:
def __init__(self, name, city, size):
self.name = name
self.city = city
self.size = size
def __repr__(self):
return f"Photo({self.name!r}, {self.city!r}, {self.size})"print(photo) # Photo('sunrise_over_lisbon.jpg', 'Lisbon', 1450)
print([photo]) # [Photo('sunrise_over_lisbon.jpg', ...)] def __eq__(self, other):
return (self.name, self.city) == (other.name, other.city)
def __len__(self):
return self.sizeclass CaptionMaker:
def __init__(self, separator="_"):
self.separator = separator
def make(self, filename):
return filename.replace(self.separator, " ").title()
maker = CaptionMaker()
caption = maker.make("sunrise_over_lisbon.jpg")def make_caption(filename, separator="_"):
return filename.replace(separator, " ").title()
caption = make_caption("sunrise_over_lisbon.jpg")from dataclasses import dataclass
@dataclass
class Photo:
name: str
city: str
size: intclass RawPhoto(Photo):
def __init__(self, name, city, size, camera):
super().__init__(name, city, size)
self.camera = camera
def caption(self):
return f"{super().caption()} ({self.camera})"class Photo:
tags = [] # ONE list, shared by all photos
a, b = Photo(), Photo()
a.tags.append("sunrise")
print(b.tags) # ['sunrise']