Classes
Inheritance
Parent class (Base class) & Child Class (Derived class) example:
class Car:
def __init__(self, brand: str, year: int):
self.brand = brand
self.year = year
def print_brand(self):
print(self.brand)
def print_infos(self):
print("{} year {}".format(self.brand, self.year))
class Ferrari(Car):
def __init__(self, model: str, year: int):
super().__init__("Ferrari", year)
self.model = model
def print_infos(self):
print("{} model: {} - year {}".format(self.brand, self.model, self.year))
You can create abstract class / method using the abc module:
from abc import ABC, abstractmethod
class Car(ABC):
def __init__(self, brand: str, year: int):
self.brand = brand
self.year = year
def print_brand(self):
print(self.brand)
@abstractmethod
def print_infos(self):
pass
Multiple Inheritance
class A:
def __init__(self):
pass
class B:
def __init__(self):
pass
class C:
def __init__(self):
pass
class D(A, B, C):
def __init__(self):
# With Super
super().__init__() # call A init method
super(A, self).__init__() # call B init method (call C if B does not have a __init__ method)
super(B, self).__init__() # call C init method
# Without Super
A.__init__(self) # call A init method
B.__init__(self) # call B init method
C.__init__(self) # call C init method
Magic Class Methods
Overload comparison operators
Comparison operators:
<
with__lt__(self, other)
<=
with__le__(self, other)
>
with__gt__(self, other)
>=
with__ge__(self, other)
==
with__eq__(self, other)
!=
with__ne__(self, other)
If you don’t want to implement all the six rich comparison methods, you can use the decorator total_ordering from the functools
library.
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, grade: float):
self.grade: float = grade
def __eq__(self, other: "Student"):
return (self.grade == other.grade)
def __lt__(self, other: "Student"):
return (self.grade < other.grade)
# Thanks to the total_ordering decorator, the methods:
# - __le__(self, other)
# - __gt__(self, other)
# - __ge__(self, other)
# - __ne__(self, other)
# are also automatically supplied
You can also overload all of the others operators:
+
with__add__(self, other)
-
with__sub__(self, other)
^
with__xor__(self, other)
&
with__and__(self, other)
etc … (Get the full list here)
Make a class Hashable
From the python documentation, hashable:
An object is hashable if it has a hash value which never changes during its lifetime (it needs a
__hash__()
method), and can be compared to other objects (it needs an__eq__()
method). Hashable objects which compare equal must have the same hash value. Hashability makes an object usable as a dictionary key and a set member, because these data structures use the hash value internally. […]
class MyClass():
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __eq__(self, other: "MyClass"):
return (self.a == other.a) and (self.b == other.b)
def __hash__(self):
# Attributes used for hash must never changes during the object lifetime
return hash((self.a, self.b))
inst1 = MyClass(1, 2, 3)
inst2 = MyClass(1, 3, 3)
inst3 = MyClass(2, 3, 3)
inst4 = MyClass(1, 2, 4)
dico = {inst1: 1, inst2: 2, inst3: 3, inst4: 4} # Here inst4 key override inst1 item
# So: dico[inst1] = dico[inst4] = 4
Make a class Iterable
class IterableClass:
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
# Need to return an object with the __next__ method defined
# You could also directly yield the next values here
self.index = 0
return self
def __next__(self):
if self.index > len(self.data) - 1:
raise StopIteration
output = self.data[self.index]
self.index += 1
return output
iterable_class = IterableClass([1, 2, 3, 4, 5])
for k in iterable_class: print(k)
# Ok
for k in iterable_class: print(k)
# Ok too: To check that the index is correctly reset to 0
Exemple with an iterable attribute:
class IterableClass:
def __init__(self, iterable_attr):
self.iterable_attr = iterable_attr
def __iter__(self):
yield from self.iterable_attr
Make a class Subscriptable
Making a class subscriptable is done with the defining the __getitem__
magic method:
class SubscriptableClass:
def __init__(self, data):
self.data = data
def __getitem__(self, item):
return self.data[item]
inst = SubscriptableClass({"a": 1, "b": 2, "c": 3})
# inst["a"] = 1, inst["b"] = 2, etc...
# Doesn't work because inst.data is not a sequence with integers keys values
for k in inst: print(k)
# Works !
inst = SubscriptableClass((4, 5, 6, 7, 8))
for k in inst: print(k)
Note
Making a class subsriptable by using the method __getitem__
automatically makes the class also iterable if the attribute is a sequence with integers keys values. (For sequence types, the accepted keys should be integers and slice objects […])
Class String representation
Using the methods __str__
and __repr__
, see the example below.
Note
The __str__
is intended to be as human-readable as possible, whereas the __repr__
should aim to be something that could be used to recreate the object.
(source)
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
# Prefered method as it's called by __str__ when not defined
def __repr__(self):
return f"Person(name={self.name}, age={self.age})"
def __str__(self):
return f"{self.name}: {self.age} years old"
jp = Person("Jean Paul", 89)
print(jp) # => "Jean Paul: 89 years old"
jp # => "Person(name=Jean Paul, age=89)"
str(jp) # => "Jean Paul: 89 years old"
Note
If you want to define only one method, define only the __repr__
method as __str__
calls it automatically when it’s not defined.
Decorators
Dataclasses
Very succinctly :
- Class that contains mainly data, not much method (although there is no restriction)
- Automatically generates the __init__()
and __repr__()
methods = shorter definition
More info in the official documentation
from dataclasses import dataclass, field
from typing import List
@dataclass
class Person():
name: str
age: int
# Argument with default value
childrens: List[str] = field(default_factory=list)
# Attributes not defined now
is_adult: bool = field(init=False)
def __post_init__(self):
self.is_adult = self.age >= 18
tati = Person("Tatiana", 32)
nico = Person("Nicolas", 32, ["Robert", "Simone"])
Alternatives to dataclasses:
Pydantic
Attrs
Property
@property
decorator: https://docs.python.org/3/library/functions.html?highlight=property#property
class Student():
def __init__(self, name, id, grade):
self.name = name
self.id = id
self._grade = grade
def get_name(self):
return self.name
# Read only attribute
# Generated only when required
@property
def full_id(self):
return self.name + " - " + str(self.id)
# Defining a setter ang getter method for an attribute
@property
def grade(self):
return self._grade
# Allow to add a check on the value for example
@grade.setter
def grade(self, grade):
if grade < 0:
print("I know this guy is bad but less than 0 is mean")
return
self._grade = grade
pollo = Student("Poulet", "AC2474", 12)
(source: https://www.askpython.com/python/built-in-methods/python-property-decorator)
Sources:
Multiple Inheritance: https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way
Mixins Class: https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-is-it-useful
Operators overloading: https://www.geeksforgeeks.org/operator-overloading-in-python/
Purpose of
__repr__
and__str__
: https://stackoverflow.com/questions/3691101/what-is-the-purpose-of-str-and-reprDifference between
__repr__
and__str__
: https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-reprfunctools.total_ordering: https://docs.python.org/3/library/functools.html?highlight=total_ordering#functools.total_ordering
dataclasses: https://docs.python.org/3/library/dataclasses.html
property decorator: https://www.askpython.com/python/built-in-methods/python-property-decorator