Classes
Classes are Python’s way of creating your own custom data types, used to bundle related data and functions together. Python classes provide all the standard features of Object Oriented Programming.
Basic Class Definition
Here’s a simple class example:
class Person:
def __init__(self, name):
self.name = name
# Create an instance
person = Person("Marcus")
person.name
# Output: Marcus
The __init__
function is the constructor for the class. It is called automatically when you create a new instance of the class. The self
parameter refers to the instance itself and is used to initialize the instance variables.
Class with Default Values
Here’s an example of a class with default values:
class Person:
def __init__(self, name="Unknown"):
self.name = name
So when creating instances, if you do not provide a name it will set the default value.
alice = Person("Alice")
billie = Person()
print(alice.name) # Output: Alice
print(billie.name) # Output: Unknown
Class vs Instance Variables
There are two types of variables in classes: class variables (shared by all instances) and instance variables (unique to each instance).
class Dog:
# Class variable - shared by all dogs
species = "Canis lupus"
def __init__(self, name, breed):
# Instance variables - unique to each dog
self.name = name
self.breed = breed
buddy = Dog("Buddy", "Golden Retriever")
max_dog = Dog("Max", "German Shepherd")
print(buddy.species) # Output: Canis lupus
print(max_dog.species) # Output: Canis lupus
print(buddy.name) # Output: Buddy
print(max_dog.name) # Output: Max
# Changing class variable affects all instances
Dog.species = "Domestic Dog"
print(buddy.species) # Output: Domestic Dog
print(max_dog.species) # Output: Domestic Dog
Methods
Methods are functions that belong to a class. The first parameter is always self
, which refers to the instance:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
return self.balance
else:
return "Insufficient funds"
def get_balance(self):
return self.balance
# Using the class
account = BankAccount("Alice", 100)
print(account.get_balance()) # Output: 100
account.deposit(50)
print(account.get_balance()) # Output: 150
account.withdraw(30)
print(account.get_balance()) # Output: 120
print(account.withdraw(200)) # Output: Insufficient funds
String Representation
If you try to print an instance of a class, by default it won’t output anything meaningful. For example,
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
p = Book("Cujo", "Stephen King")
print(p)
# Output: <__main__.Book object at 0x10063b200>
To show a human-readable representation, you need to define the __str__
method.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
p = Book("Cujo", "Stephen King")
print(p)
# Output: Cujo by Stephen King
There is also __repr__
for developers and debugging, typically __repr__
is used to provide a string representation of an object that can be used to recreate the object. If __str__
is not defined, __repr__
is used as a fallback.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
def __repr__(self):
return f"Book('{self.title}', '{self.author}')"
b = Book("Cujo", "Stephen King")
print(b) # Output: Cujo by Stephen King
print(str(b)) # Output: Cujo by Stephen King
print(repr(b)) # Output: Book('Cujo', 'Stephen King')
r = repr(b)
nb = eval(r) # Create new book object from output
print(nb) # Output: Cujo by Stephen King
Summary: __str__
is for human-readable output, __repr__
is for developers and debugging.
Property Decorators
Properties let you access methods like attributes. use them for computed values or to add validation:
class Circle:
def __init__(self, radius):
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):
return 3.14159 * self._radius ** 2
@property
def circumference(self):
return 2 * 3.14159 * self._radius
circle = Circle(5)
print(circle.radius) # Output: 5
print(circle.area) # Output: 78.53975
print(circle.circumference) # Output: 31.4159
circle.radius = 10
print(circle.area) # Output: 314.159
# circle.radius = -5 # Would raise ValueError
Class Methods and Static Methods
At times you may want to create a class to create methods that work with the class itself, not instances:
class MathUtils:
pi = 3.14159
@classmethod
def circle_area(cls, radius):
return cls.pi * radius ** 2
@staticmethod
def add(a, b):
return a + b
@staticmethod
def is_even(number):
return number % 2 == 0
# Class methods can be called on the class
area = MathUtils.circle_area(5)
print(area) # Output: 78.53975
# Static methods are just regular functions grouped with the class
result = MathUtils.add(10, 20)
print(result) # Output: 30
print(MathUtils.is_even(4)) # Output: True
print(MathUtils.is_even(7)) # Output: False
Inheritance
Inheritance lets you create new classes based on existing ones:
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def speak(self):
return f"{self.name} makes a sound"
def info(self):
return f"{self.name} is a {self.species}"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Dog") # Call parent constructor
self.breed = breed
def speak(self): # Override parent method
return f"{self.name} barks!"
def fetch(self): # New method specific to dogs
return f"{self.name} fetches the ball"
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name, "Cat")
self.color = color
def speak(self):
return f"{self.name} meows!"
def climb(self):
return f"{self.name} climbs the tree"
# Using inheritance
buddy = Dog("Buddy", "Golden Retriever")
whiskers = Cat("Whiskers", "Orange")
print(buddy.info()) # Output: Buddy is a Dog
print(buddy.speak()) # Output: Buddy barks!
print(buddy.fetch()) # Output: Buddy fetches the ball
print(whiskers.info()) # Output: Whiskers is a Cat
print(whiskers.speak()) # Output: Whiskers meows!
print(whiskers.climb()) # Output: Whiskers climbs the tree
Dunder Methods
The __str__
and __repr__
earlier are dunder methods (double underscore) that are special methods that are automatically called by Python in various contexts. There are several other dunder methods that you can define in your classes to customize their behavior.
Operators
The following operator examples will use the Point
class to demonstrate.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
Equality: Define __eq__
method to compare two objects using the ==
operator:
def __eq__(self, other):
if isinstance(other, Point):
# return True only if both x and y are equal
return self.x == other.x and self.y == other.y
return False
p1 = Point(2,4)
p2 = Point(3,5)
p3 = Point(2,4)
print(p1 == p2) # Output: False
print(p1 == p3) # Output: True
Math Operators: Define __add__
method on a class to define what it means to add two objects together using the +
operator:
def __add__(self, other):
if isinstance(other, Point):
return Point(self.x + other.x, self.y + other.y)
raise TypeError("Can only add Point objects")
p1 = Point(2,4)
p2 = Point(3,5)
print(p1 + p2) # Output: Point(5, 9)
You can also add methods for the following math operators and symbols:
Method | Symbol | Description |
---|---|---|
__sub__ | - | Subtraction |
__mul__ | * | Multiplication |
__truediv__ | / | Division |
__floordiv__ | // | Floor Division |
__mod__ | % | Modulus |
__pow__ | ** | Exponentiation |
Logic Operators: Additional logic operators are defined in the same way:
Method | Symbol | Description |
---|---|---|
__lt__ | < | Less than |
__le__ | <= | Less than or equal to |
__gt__ | > | Greater than |
__ge__ | >= | Greater than or equal to |
__ne__ | != | Not equal to |
__and__ | & | Logical AND |
__or__ | | | Logical OR |
__invert__ | ~ | Logical NOT |
I don’t recommend overloading operators too much, it can be fun to do so, but it can also lead to confusion and errors if not used carefully.
Iterator
You can build your own iterator to use in a loop. To do so you must implement the __iter__
and __next__
methods.
class Doubler:
def __init__(self, start, maxim=100):
self.num= start / 2
self.maxim = maxim
def __iter__(self):
return self
def __next__(self):
self.num = self.num * 2
if self.num > self.maxim:
raise StopIteration
return int(self.num)
print("Doubling from 2 to 100")
for i in Doubler(2):
print(i)
print("Doubling from 3 to 200")
for i in Doubler(3, 200):
print(i)
Generator
For simple iterators it is often easier to use a generator function to achieve the same result. A generator is a regular functions that use the yield
keyword to return a value. Each time next()
is called on it, the generator will resume execution from where it left off. What makes generators easier to use is the __iter__()
and __next__()
methods are created automatically.
def doubler(num, maxim=100):
while num <= maxim:
yield int(num)
num *= 2
print("Doubling from 2 to 100")
for i in doubler(2):
print(i)
print("Doubling from 3 to 200")
for i in doubler(3, 200):
print(i)
Class Summary
Classes are incredibly powerful for organizing your code and modeling real-world concepts. Start with simple classes and gradually add complexity as you need it. The key is to think about what data and behaviors naturally belong together.