Class Helpers
Named Tuples
Whenever you need to wrap some values together, think of NamedTuple:
from typing import NamedTuple
class Location(NamedTuple):
name: str
lat: float
long: float
The class this creates has an initializer which sets attributes on the Location object:
>>> location = Location('San Diego', 32.733999, -117.147777)
>>> location.name
'San Diego'
And it has a nice default string representation:
>>> location
Location(name='San Diego', lat=32.733999, long=-117.147777)
And like a tuple, you can iterate over (and therefore unpack) Location objects:
>>> name, lat, long = Location('San Diego', 32.733999, -117.147777)
>>> name
'San Diego'
Also like tuples, we cannot modify namedtuple objects (they’re immutable):
>>> location.name = 'Los Angeles'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
Also Location objects can be compared to each other using equality (if the values they contain are the same, they will be seen as “equal”):
>>> location2 = Location('San Diego', 32.733999, -117.147777)
>>> location == location2
True
>>> location3 = Location('Los Angeles', 32.733999, -117.147777)
>>> location == location3
False
Note
There’s also a function version of NamedTuple, in the collections module:
from collections import namedtuple
Location = namedtuple('Location', 'name lat long')
This is a function which returns a class.
I find that a bit confusing and non-obvious, so I recommend the class version of NamedTuple in the typing module instead.
Data Classes
If you like the idea of named tuples, but you want more flexibility (something that helps you generate a class but doesn’t make an immutable tuple), definitely look into dataclasses.
The dataclasses module was added in Python 3.7.
Data classes are basically a factory for making friendly classes without having to write your own boilerplate code for many common methods.
It is (superficially) a bit similar to the way NamedTuple works, but is more powerful and very customizable.
Here’s a data class that is similar to the Location named tuple we made earlier:
from dataclasses import dataclass
@dataclass
class Location:
name: str
lat: float
long: float
>>> location = Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> location
Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> location.name
'San Diego'
This class has a default initializer, string representation, and support for equality.
It isn’t iterable by default, though we could add iterability by adding a __iter__ method:
from dataclasses import astuple, dataclass
@dataclass
class Location:
name: str
lat: float
long: float
def __iter__(self):
yield from astuple(self)
>>> location = Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> name, lat, long = location
>>> name
'San Diego'
And unlike NamedTuple, data classes are mutable by default:
>>> location.name = 'Los Angeles'
>>> location
Location(name='Los Angeles', lat=32.733999, long=-117.147777)
If you want to make your Location objects immutable, you can pass a frozen attribute to the dataclass decorator:
from dataclasses import astuple, dataclass
@dataclass(frozen=True)
class Location:
name: str
lat: float
long: float
def __iter__(self):
yield from astuple(self)
Now our location objects are immutable:
>>> location = Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> location.name = 'Los Angeles'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'name'
The new dataclasses library is essentially a more stripped-down version of the third-party attrs library.
If you want more functionality, look into attrs as well.
When would you use these?
If you find yourself passing around tuples where each element represents a certain value, you could consider using a NamedTuple instead.
For example this function:
def split_name(name):
first, last = name.rsplit(" ", 1)
return first, last
Always returns a tuple of two values:
>>> split_name("Trey Hunner")
('Trey', 'Hunner')
We could instead use a NamedTuple instead:
from typing import NamedTuple
class Name(NamedTuple):
"""Class to store first and last names."""
first: str
last: str
def split_name(name):
first, last = name.rsplit(" ", 1)
return Name(first, last)
This will make our code more descriptive but it’ll also mean that if someone inspects the tuple returned from this function, they’ll more easily understand what it represents:
>>> split_name("Trey Hunner")
Name(first='Trey', last='Hunner')
I’d recommend dataclasses whenever you’d like to make a class with a few attributes to store data, but you don’t necessarily need a tuple.
Practically, in almost all cases where you could use a named tuple, I’d recommend using a dataclass instead. Dataclasses are friendly classes to work with, but they aren’t tuple-like, so they don’t have as many weird quirks as named tuples.
For example, here are some of the tuple-inherited quirks of the the Name named tuple above:
>>> name = Name("Trey", "Hunner")
>>> len(name)
2
>>> name + name
('Trey', 'Hunner', 'Trey', 'Hunner')
>>> name * 3
('Trey', 'Hunner', 'Trey', 'Hunner', 'Trey', 'Hunner')
>>> name[1:]
('Hunner',)
>>> "Trey" in name
True
Some of those might be nice-to-have features, but you could always implement the few nice-to-have features yourself using dunder methods.
For more on dataclasses, NamedTuple, and the third-part attrs library, see my talk, Easier Classes: Python Classes Without All The Cruft.
Dataclass Exercises
Ice Cream DataClasses
Rewrite the Size, Flavor, and IceCream classes in classes.py using data classes.
You can test each of these classes by running tests for Size, Flavor, and IceCream.