Data and the operations on it belong together, and a class binds them under one name.
You start with the class keyword followed by the
class name and a colon. Use def to create methods
inside the indented class body:
# simple_class.py
class Simple:
def __init__(self, text):
print("Inside the Simple constructor")
self.text = text
def show(self, msg=""):
if msg:
print(f"{msg}:", self.text)
else:
print(self.text)
def show_twice(self):
self.show()
self.show()# demo_simple_class.py
from simple_class import Simple
x = Simple("Constructor argument")
#: Inside the Simple constructor
x.show()
#: Constructor argument
x.show("A message")
#: A message: Constructor argument
x.show_twice()
#: Constructor argument
#: Constructor argumentThe first method of
Simple, __init__(), is the
initializer. The double underscores on both ends make
it a dunder (Tour
introduces the term), a method the language calls for you. The
__new__() method is the constructor, which you
rarely use (Singleton shows
a case that needs it). Most programmers call
__init__() the constructor, since it sets
up the new object’s attributes, as a constructor does in other
OOP languages. This book follows that practice.
Python calls the constructor automatically when it creates an
object. In the demo, Simple("Constructor argument")
creates an object with what looks like a call to a function
named after the class.
Ordinary methods require a reference to the current object.
When you define a method, you write the reference as the first
parameter. Python programmers traditionally name the reference
self, but you can use any identifier (though
anything else probably confuses people). To refer to the
object’s attributes or its other methods, you must go through
self. Here show() leaves the parameter
out:
# forgot_self.py
class Oops:
def show(): # Missing the self parameter
print("never runs")
try:
Oops().show() # type: ignore
except TypeError as e:
print(e)
#: Oops.show() takes 0 positional arguments but 1 was givenWhen you call a method for an object, as in
x.show(), Python passes the object reference
automatically. A method defined without self has no
parameter to receive the reference. The “1” in the error message
counts that reference.
A type checker reports Oops().show() before
anything runs, so the call carries a # type: ignore
to say the mistake is deliberate.
In C++ or Java you declare
object-level fields inside the class body but outside the
methods. In Python an object attribute exists once a method
assigns to it through self (typically in the
constructor, though any method can do it). The attributes an
object carries therefore depend on which of its methods have
run. If you assign to a name in the class body, C++/Java style,
that name becomes a class-level attribute (similar to a static
field in C++/Java).
A bare annotation with no value looks most like a C++ or Java field declaration, yet it creates neither kind of attribute. It records the type. Class Attributes and Data Classes as Types use bare annotations.
display_object() is a
small inspection helper built in Metaprogramming.
The helper shows the shape of an object by printing its
attributes and methods:
# display_simple.py
from display import display_object
from simple_class import Simple
x = Simple("Constructor argument")
#: Inside the Simple constructor
display_object(x)
#: [Attributes]
#: • text = 'Constructor argument'
#: [Methods]
#: • show(self, msg='')
#: • show_twice(self)The instance carries one attribute, text, while
the two methods belong to the class. Because the constructor
__init__() is a dunder,
display_object() hides it by default.
With inheritance in C++ or Java,
you often inherit to establish a common interface. Python is
different. Because Python is dynamically typed, it applies an
operation to an object without checking an interface first. You
therefore inherit an implementation, to reuse the code from the
base class. To name an interface, Python uses a Protocol,
which lists the methods and attributes a function needs. A class
matches a protocol by defining them, whatever it inherits.
First import the base class the
same way you import any
name from a module. Then inherit by listing the base class
in parentheses after the name of the inheriting class. Python
supports multiple inheritance, so you can list several classes.
Rethinking
Objects argues against multiple inheritance in favor of
protocols. Here Derived subclasses
Simple, imported from the simple_class
module. Ignore the @override
decorator for now.
# simple_subclass.py
from typing import override
from simple_class import Simple
class Derived(Simple):
def __init__(self, text):
print("Inside Derived constructor")
# Call the base-class constructor with super():
super().__init__(text)
def display(self):
self.show("Called from display()")
@override
def show(self, msg=""):
print("Overridden show() method")
# Call the base-class method from inside
# the overridden method:
super().show(msg)# demo_subclass.py
from simple_subclass import Derived
x = Derived("Derived constructor argument")
#: Inside Derived constructor
#: Inside the Simple constructor
x.display()
#: Overridden show() method
#: Called from display(): Derived constructor argument
x.show_twice() # Inherited from Simple
#: Overridden show() method
#: Derived constructor argument
#: Overridden show() method
#: Derived constructor argumentWhen you override a method but
still want the base-class version, call it through
super(), as Derived’s
show() does.
Derived inherits show_twice() from
Simple unchanged, yet each self.show()
inside it runs Derived’s override. The lookup
starts at the class of the object, not at the class that defines
the calling method, so base-class code reaches a method the
subclass replaced.
f() demonstrates
dynamic typing. The class Different also has a
method named show(), but does not derive from
Simple:
# any_show.py
from simple_subclass import Derived
class Different:
def show(self):
print("Not derived from Simple")
def f(obj): # Works on any obj with a show()
obj.show()
f(Derived("Derived constructor argument"))
#: Inside Derived constructor
#: Inside the Simple constructor
#: Overridden show() method
#: Derived constructor argument
f(Different())
#: Not derived from SimpleAn import inside a class body binds the imported
name like any other assignment, so importing a module-level
function there makes it a method of the class. A call through an
instance then passes self as the function’s first
argument. More than one unrelated class can import the same
function this way, but that import trick is a curiosity more
than a technique. A helper object or a module-level function is
almost always clearer.
super() and ordinary
attribute lookup both follow one list, the class’s method
resolution order (MRO). The MRO lists the classes Python
searches for a name, starting with the class and ending at
object. Derived.__mro__ is
(Derived, Simple, object). super()
searches the same list, starting with the class after the one
whose method calls super(), so
super().show(msg) in Derived reaches
Simple’s show().
With a single base class, as in
Derived, the order is obvious. Python builds the
list with C3 linearization, which also decides the
order when two base classes share an ancestor (Rethinking
Objects returns to that case). When two base classes define
the same name, the MRO decides which one supplies it. Here,
A and B both define
show(), and C inherits from both:
# mro_conflict.py
class A:
def show(self):
print("A.show")
class B:
def show(self):
print("B.show")
class C(A, B):
pass # Defines no show() of its own
print([c.__name__ for c in C.__mro__])
#: ['C', 'A', 'B', 'object']
C().show() # A comes first in the MRO
#: A.showThe bases appear in the order the class
statement lists them. Writing class C(B, A) puts
B first. C().show() then runs
B’s version.
The base-class constructor runs
because Derived’s constructor calls it. C++ and
Java call the base-class constructor automatically, but when a
Python class defines its own constructor, Python runs that
constructor alone. Broken’s constructor leaves out
the super().__init__(text) line, so
show() raises an AttributeError when
it reads self.text:
# missing_super.py
from exceptions import expect
from simple_class import Simple
class Broken(Simple):
def __init__(self, text):
pass # Forgot super().__init__(text)
expect(AttributeError, Broken("ignored").show)
#: [AttributeError] 'Broken' object has no attribute 'text'If a derived class defines no constructor, Python runs the constructor the derived class inherits from the base class.
@overridePython accepts any method name in a subclass. If you misspell the name of a method you mean to override, you define a new method. Python accepts the new method. Renaming or removing the base method later orphans the subclass method. Python accepts the orphaned method too. Either bug is easy to miss.
To let the type checker report
either bug, the typing module provides the
@override decorator. A line starting with
@ above a definition applies a decorator to
it. @override declares that a method replaces one
from a base class:
# override_intro.py
from typing import override
class Base:
def show(self):
print("Base.show")
class Derived(Base):
@override
def show(self):
print("Derived.show")
class Typo(Base):
# @override # "shwo" does not override anything
def shwo(self):
print("Typo.shwo")
Derived().show()
#: Derived.showA type checker now verifies that declaration. The checker
reports an error when a decorated method is an orphan, whether
from a misspelling or from a base method someone has since
renamed or removed. With the decorator above shwo()
uncommented, the type checker (ty) reports:
error[invalid-explicit-override]: Method `shwo` is decorated with
`@override` but does not override anything
Python runs the program either way. Reporting the mistake takes a separate tool. Static Types sets that tool up.
At run time @override
returns the same function object it received. Before returning
the function, the decorator tries to set an
__override__ attribute on it, so that code can find
overrides by introspection. On an object that rejects the new
attribute, such as one with __slots__ or a built-in
class, that assignment raises an AttributeError or
a TypeError, which the decorator catches and
ignores.
Apply @override to
any method that replaces an inherited method. This book makes
two exceptions: constructors, and dunders such as
__repr__() and __str__() that replace
a default inherited from object.
With @property, you
can start with a plain attribute and convert it to a computed
one later. The calling code stays as it was:
# properties.py
class Circle:
def __init__(self, radius):
self.radius = radius # A plain attribute
@property
def area(self): # Used like an attribute, not a call
return 3.14159 * self.radius ** 2
c = Circle(10)
print(c.radius)
#: 10
print(c.area) # Properties don't use parentheses
#: 314.159Writing c.area() reads the property and then
calls the float it produced, which raises a
TypeError.
A @property with a getter alone rejects writes.
Assigning to it raises an AttributeError. A
setter enables writing, and it is the place to validate
the value before storing it:
# property_setter.py
from exceptions import expect
class Circle:
def __init__(self, radius):
# Goes through the setter below
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("radius cannot be negative")
self._radius = value
@property
def area(self): # Unchanged from the version above
return 3.14159 * self.radius ** 2
c = Circle(10)
print(c.radius) # The same two lines as before
#: 10
print(c.area)
#: 314.159
c.radius = 5 # Now the setter validates, then stores
print(c.radius)
#: 5
expect(ValueError, Circle, -1)
#: [ValueError] radius cannot be negativeproperty_setter.py
completes the conversion. radius began as a plain
attribute and is now a validated property. Because code outside
the class reads both versions the same way, you can wait to
convert an attribute into a property until you need the
validation.
The decorators combine the two
methods named radius into one property.
@property turns the getter into a property object
and binds it to the name radius in the class body.
@radius.setter reads that object, and its
setter() method returns a new property holding both
the getter and the setter. Python binds that new property to
radius, the name of the setter’s def.
@radius.setter needs the property the getter
created, so the getter comes first.
The setter’s def must also use the name
radius. Under another name, such as
def set_radius(self, value), the new property binds
to set_radius, while radius keeps the
getter alone. The assignment in the constructor then raises an
AttributeError reporting that the property
radius has no setter.
Because the class binds the name radius to the
property, the value goes into a separate attribute,
_radius. A single leading underscore marks
_radius as internal to the class, a convention
rather than a language rule.
The separate name keeps each
method from calling itself. Inside the getter,
self.radius calls the getter again. Inside the
setter, self.radius = value calls the setter again.
Either loop repeats until the interpreter raises a
RecursionError. Naming both the property and the
backing attribute radius reproduces the
RecursionError:
# property_recursion.py
from exceptions import expect
class Circle:
def __init__(self, radius):
self.radius = radius # Calls the setter
@property
def radius(self):
return self.radius # Calls itself again
@radius.setter
def radius(self, value):
self.radius = value # Calls itself again
expect(RecursionError, Circle, 10)
#: [RecursionError] maximum recursion depth exceededproperty(fset=...) builds a write-only property,
the reverse of a getter-only one, but write-only properties are
rare. Reading one raises an AttributeError
reporting that the property has no getter. A class that needs a
write-only value usually takes it through a method instead. The
write is then a call, and no attribute exists to read back.
cached_propertyA @property reruns
its code on every access. When the computation is slow and its
answer stays the same, functools.cached_property
runs it once, on first access, and stores the result:
# cached_property_demo.py
from functools import cached_property
class Numbers:
def __init__(self, values):
self.values = values
@cached_property
def total(self):
print("summing", len(self.values), "values")
return sum(self.values)
n = Numbers([5, 10, 15])
print(n.total)
#: summing 3 values
#: 30
# Second access: stored value, no recomputation
print(n.total)
#: 30
n.values.append(20)
print(n.total) # Still the old sum: the cache is stale
#: 30
del n.total # Discard the cached value
print(n.total)
#: summing 4 values
#: 50total is lazily
initialized, created on first use, so the method runs at
the first read rather than when the constructor runs.
cached_property stores the value in the
instance’s __dict__,
the dictionary that holds the instance’s attributes. A
@property takes precedence over that dictionary for
its name, but the dictionary takes precedence over a
cached_property. A later n.total
checks the instance first and returns the stored value before
the lookup reaches the method in the class.
When a class and all its base classes declare
__slots__, its instances have no
__dict__ to hold the cached value. The first read
of a cached_property then raises a
TypeError (Performance
shows the failure). Adding a "__dict__" entry to
__slots__ restores the dictionary, so
cached_property works again. That entry also gives
every instance the per-instance dictionary that slots exist to
remove.
Because
cached_property keeps returning the stored value,
total becomes stale when n.values
changes. del n.total removes the stored value from
__dict__. Because a plain @property
recomputes every time, its answer is always current. Cache only
answers whose inputs stay the same.
By default, printing an object
shows its class and its address, as in
<__main__.Point object at 0x7f2dd669cd70>.
That display identifies the object, not the value it holds. Two
dunder methods control how an object displays.
__str__() is the readable form for users, while
__repr__() is the unambiguous form for
developers:
# representation.py
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # Falls back to __repr__
#: Point(3, 4)A __str__() on the same class separates the two
forms:
# representation_str.py
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(3, 4)
print(p) # print() prefers __str__
#: (3, 4)
print(repr(p))
#: Point(3, 4)
print([p])
#: [Point(3, 4)]print() and
str() use __str__() when it exists and
otherwise fall back to __repr__(). The fallback
runs in one direction. repr() calls
__repr__() in every case. A container builds its
display from the __repr__() of its elements, so the
list prints Point(3, 4) rather than
(3, 4). In an f-string, {p!r} selects
__repr__() and {p!s} selects
__str__(). A bare {p} also selects
__str__() for a class that inherits
__format__() from object.
By convention __repr__() returns the text of a
call that rebuilds the object, so it reads
Point(3, 4). Define __repr__() on
classes you debug, and add __str__() only when
users see the output.
For classes that are primarily a bundle of typed data, Data
Classes as Types shows how @dataclass writes
the constructor and __repr__().
A method that doesn’t use
self can be a @staticmethod. A method
that needs the class rather than an instance can be a
@classmethod. A class method receives the class as
its first argument, conventionally named cls:
# class_methods.py
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
# An alternative constructor
def from_fahrenheit(cls, fahrenheit):
return cls((fahrenheit - 32) * 5 / 9)
@staticmethod
def is_freezing(celsius): # Needs no self or cls
return celsius <= 0
class Reading(Temperature):
pass # Adds nothing; inherits from_fahrenheit()
t = Temperature.from_fahrenheit(212)
print(round(t.celsius))
#: 100
print(Temperature.is_freezing(-4))
#: True
r = Reading.from_fahrenheit(212)
print(type(r).__name__)
#: Readingfrom_fahrenheit() builds its result with
cls(...) rather than Temperature(...).
Called on a subclass, from_fahrenheit() receives
that subclass as cls, so the alternative
constructor returns an instance of that subclass. Writing
return Temperature(...) hard-codes the class.
Reading.from_fahrenheit() then returns a
Temperature.
is_freezing() also works as a module-level
function, and that is the usual choice. Its result depends on
its celsius argument alone. Make it a
@staticmethod when it belongs with the class’s
interface, where a reader looks for it, or when a subclass
should be able to replace it like any other method.
Try each exercise before opening its solution.
shrink(self, factor) to
Circle in property_setter.py that
sets self.radius = self.radius / factor, going
through the existing setter. Confirm shrink(2) on a
Circle(10) leaves the radius at 5.0.
Then call shrink(-2) on that same circle, which
would divide the radius down to -2.5, and confirm
the setter raises its ValueError instead of
silently storing a negative radius.class_methods.py, add a
second alternative constructor,
from_kelvin(cls, kelvin), using
celsius = kelvin - 273.15. Add a call that builds a
Temperature both ways for the same physical
temperature and confirms they agree, within rounding.simple_subclass.py, add a
third class, MoreDerived(Derived), that overrides
show() again, printing its own message before
calling super().show(msg). Predict, then confirm,
the full chain of prints from
MoreDerived("x").show_twice().@cached_property called
average to Numbers in cached_property_demo.py
that returns self.total / len(self.values). Access
n.total and then n.average, and
confirm average reads the cached total
instead of recomputing it.Temperature in class_methods.py a
__repr__() that returns
Temperature(21.0) for a temperature of 21 degrees
Celsius. Print a single Temperature and a list of
two of them, and confirm the list shows the same form for each
element. Then add a __str__() returning
21.0C and confirm which of the two
print() uses for each case.override_intro.py, rename
Base.show() to display(), leaving
Derived.show() and its @override as
they are. Run the program and confirm it still prints
Derived.show, then run the type checker (Static
Types sets one up) and read what it reports. Remove
@override and confirm the program runs the same way
while the type checker prints
All checks passed!.