
Python OOP Cheat Sheet
Welcome, Python enthusiast! Whether you're just starting your journey into object-oriented programming (OOP) with Python or you're looking for a quick reference guide, you're in the right place. OOP is a programming paradigm that uses objects and classes to structure software, and Python makes it both powerful and intuitive. Let’s dive into the essentials with clear explanations and practical examples.
Classes and Objects
At the heart of OOP are classes and objects. A class is like a blueprint for creating objects. It defines the attributes (data) and methods (functions) that the objects created from the class will have. An object is an instance of a class—a concrete entity built from that blueprint.
To define a class in Python, use the class
keyword. Here's a simple example:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
return "Woof!"
In this example, Dog
is a class. The __init__
method is a special method called a constructor. It initializes the object's attributes. self
refers to the instance of the class. To create an object (an instance of the class), you call the class as if it were a function:
my_dog = Dog("Rex", 3)
print(my_dog.name) # Output: Rex
print(my_dog.bark()) # Output: Woof!
Remember: self
is always the first parameter in instance methods and refers to the instance calling the method. You don't pass it explicitly when calling the method.
Class Component | Purpose | Example |
---|---|---|
Class Definition | Blueprint for objects | class Dog: |
Constructor (__init__ ) |
Initializes object attributes | def __init__(self, name, age): |
Instance Method | Function defined in a class | def bark(self): |
Object Creation | Creating an instance | my_dog = Dog("Rex", 3) |
Here’s a quick list of key points to keep in mind when working with classes and objects:
- Classes are defined using the class
keyword.
- The __init__
method is called automatically when a new object is created.
- Attributes are variables that belong to an object.
- Methods are functions that belong to an object.
- You can create as many objects (instances) as you need from a single class.
Inheritance
Inheritance allows you to define a class that inherits all the methods and properties from another class. The class being inherited from is called the parent or base class, and the class that inherits is called the child or derived class. This promotes code reusability.
Let’s extend our Dog
class:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
Here, Animal
is the base class, and Dog
and Cat
are derived classes. They inherit the __init__
method from Animal
but override the speak
method.
my_dog = Dog("Rex", 3)
my_cat = Cat("Whiskers", 2)
print(my_dog.speak()) # Output: Woof!
print(my_cat.speak()) # Output: Meow!
Inheritance enables you to create a general class first and then more specialized classes. This avoids repetition and keeps your code DRY (Don’t Repeat Yourself).
Inheritance Type | Description | Syntax |
---|---|---|
Single Inheritance | Child class inherits from one parent | class Child(Parent): |
Multiple Inheritance | Child inherits from multiple parents | class Child(Parent1, Parent2): |
Multilevel Inheritance | Chain of inheritance | class Child(Parent): , class GrandChild(Child): |
Hierarchical Inheritance | Multiple children from one parent | class Child1(Parent): , class Child2(Parent): |
Key aspects of inheritance in Python:
- A child class can override methods of the parent class.
- You can use the super()
function to call methods from the parent class.
- Multiple inheritance is supported, but it should be used carefully to avoid complexity.
- Inheritance represents an "is-a" relationship (e.g., a Dog is an Animal).
Encapsulation
Encapsulation is the concept of restricting direct access to some of an object's components. This is achieved by using private and protected attributes and methods. It helps in preventing accidental modification of data and in bundling the data with the methods that operate on that data.
In Python, we use naming conventions to denote the access level:
- Public: Accessible from anywhere. No underscores.
- Protected: Accessible within the class and its subclasses. Prefix with a single underscore _
.
- Private: Accessible only within the class. Prefix with double underscores __
.
Here’s an example:
class BankAccount:
def __init__(self, balance):
self._balance = balance # Protected attribute
def deposit(self, amount):
self._balance += amount
def _display_balance(self): # Protected method
return f"Balance: {self._balance}"
def get_balance(self):
return self._display_balance()
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance()) # Output: Balance: 1500
# print(account._balance) # Possible but not recommended
Although Python doesn’t enforce strict encapsulation (it’s more about convention), it’s good practice to respect these conventions to write maintainable code.
Access Modifier | Prefix | Accessibility |
---|---|---|
Public | None | Anywhere |
Protected | _ |
Within class and subclasses |
Private | __ |
Only within the class |
Important notes on encapsulation:
- Use protected members when you intend for them to be used only within the class and its subclasses.
- Use private members to prevent accidental access from outside the class.
- Name mangling is applied to private members: __attribute
becomes _Classname__attribute
.
- Encapsulation helps in achieving data hiding and abstraction.
Polymorphism
Polymorphism allows methods to do different things based on the object it is acting upon. It means "many forms". In Python, polymorphism is achieved through method overriding and duck typing.
Method overriding occurs when a child class provides a specific implementation of a method that is already defined in its parent class. We saw this in the inheritance example with the speak
method.
Duck typing is a concept where the type or class of an object is less important than the methods it defines. If an object has the method you're calling, it can be used, regardless of its class.
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
def animal_sound(animal):
return animal.speak()
my_dog = Dog()
my_cat = Cat()
print(animal_sound(my_dog)) # Output: Woof!
print(animal_sound(my_cat)) # Output: Meow!
Here, animal_sound
function doesn't care about the class of animal
as long as it has a speak
method.
Polymorphism Type | Description | Example |
---|---|---|
Method Overriding | Child class provides specific implementation | def speak(self): in child class |
Duck Typing | Object's methods matter more than its class | animal.speak() in function |
Key points about polymorphism: - Method overriding is a way to achieve runtime polymorphism. - Duck typing is a philosophy in Python: "If it walks like a duck and quacks like a duck, it must be a duck." - Polymorphism allows for writing more generic and reusable code. - It often goes hand-in-hand with inheritance.
Special Methods
Special methods in Python are predefined methods that you can override to add special functionality to your classes. They are always surrounded by double underscores, like __init__
. These methods are called automatically in response to specific operations.
For example, the __str__
method is called when you use the print()
function on an object, and __len__
is called when you use the len()
function.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
return f"{self.title} by {self.author}"
def __len__(self):
return self.pages
my_book = Book("Python Basics", "John Doe", 200)
print(my_book) # Output: Python Basics by John Doe
print(len(my_book)) # Output: 200
Here’s a table of some commonly used special methods:
Special Method | Purpose | Trigger |
---|---|---|
__init__ |
Constructor | Object creation |
__str__ |
Informal string representation | print(obj) , str(obj) |
__repr__ |
Formal string representation | repr(obj) |
__len__ |
Length of object | len(obj) |
__add__ |
Addition | obj1 + obj2 |
__eq__ |
Equality check | obj1 == obj2 |
By implementing these methods, you can make your objects behave more like built-in types.
Important special methods to know:
- __init__
: Initialize new objects.
- __str__
: Return a human-readable string representation.
- __repr__
: Return an unambiguous string representation, often used for debugging.
- __add__
, __sub__
, etc.: Define behavior for arithmetic operations.
- Overloading these methods allows for intuitive object interactions.
Properties and Decorators
Properties allow you to define methods that can be accessed like attributes. This is useful when you want to add logic to attribute access without changing the interface. Decorators like @property
, @<attribute>.setter
, and @<attribute>.deleter
are used to define properties.
Consider a class where we want to ensure that an attribute is always non-negative:
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.14 * self._radius ** 2
my_circle = Circle(5)
print(my_circle.radius) # Output: 5
print(my_circle.area) # Output: 78.5
my_circle.radius = 10
print(my_circle.area) # Output: 314.0
# my_circle.radius = -1 # Raises ValueError
Here, radius
is a property with a getter and setter. The area
is a read-only property.
Decorator | Purpose | Example |
---|---|---|
@property |
Defines a getter method | def radius(self): |
@<property>.setter |
Defines a setter method | def radius(self, value): |
@<property>.deleter |
Defines a deleter method | def radius(self): |
Benefits of using properties:
- You can add validation logic when setting an attribute.
- You can compute values on the fly (like area
).
- The interface remains clean—users access it like an attribute.
- Encapsulation is maintained without sacrificing convenience.
Class and Static Methods
So far, we've dealt with instance methods, which operate on instances. Python also supports class methods and static methods.
- Class methods are bound to the class and not the instance. They can modify class state that applies across all instances. They are defined using the
@classmethod
decorator and takecls
as the first parameter. - Static methods are also bound to the class, but they don't modify class or instance state. They are defined using the
@staticmethod
decorator and don't takeself
orcls
.
class MyClass:
class_attribute = 0
def __init__(self, value):
self.instance_attribute = value
@classmethod
def class_method(cls):
cls.class_attribute += 1
return cls.class_attribute
@staticmethod
def static_method():
return "This is a static method."
obj = MyClass(10)
print(MyClass.class_method()) # Output: 1
print(obj.class_method()) # Output: 2
print(MyClass.static_method()) # Output: This is a static method.
Method Type | Decorator | First Parameter | Can Modify |
---|---|---|---|
Instance Method | None | self |
Instance state |
Class Method | @classmethod |
cls |
Class state |
Static Method | @staticmethod |
None | Nothing |
When to use each: - Use instance methods when you need to access or modify instance attributes. - Use class methods for factory methods or when you need to work with class-level data. - Use static methods for utility functions that are related to the class but don't need access to instance or class data.
Composition Over Inheritance
While inheritance is powerful, it's not always the best solution. Composition is an alternative where you build complex objects by combining simpler ones. This often leads to more flexible and maintainable code.
Inheritance represents an "is-a" relationship, while composition represents a "has-a" relationship.
For example, instead of making Engine
a subclass of Car
, you can compose a Car
with an Engine
:
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
my_car = Car()
print(my_car.start()) # Output: Engine started
Here, Car
has an Engine
, rather than being an Engine
.
Approach | Relationship | Flexibility |
---|---|---|
Inheritance | Is-a | Less flexible |
Composition | Has-a | More flexible |
Advantages of composition: - Reduces coupling between classes. - Easier to change behavior at runtime by replacing components. - Avoids the diamond problem in multiple inheritance. - Promotes code reuse without the constraints of inheritance hierarchies.
Abstract Base Classes
Abstract Base Classes (ABCs) are classes that cannot be instantiated and are meant to be subclassed. They define a set of methods that must be implemented by their subclasses. This is useful for defining interfaces.
To define an ABC, use the abc
module:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
# animal = Animal() # This would raise an error
my_dog = Dog()
print(my_dog.speak()) # Output: Woof!
Here, Animal
is an abstract class with an abstract method speak
. Any concrete subclass must implement speak
.
ABC Component | Purpose | Example |
---|---|---|
ABC class |
Base class for ABCs | class Animal(ABC): |
@abstractmethod |
Decorator for abstract methods | @abstractmethod def speak(self): |
Key points about ABCs: - Cannot instantiate an abstract class. - Subclasses must implement all abstract methods. - Useful for enforcing a consistent interface across multiple classes. - Helps in large projects to ensure that certain methods are always present.
Magic Methods for Comparisons
Python allows you to define how objects behave with comparison operators by overriding magic methods like __eq__
, __lt__
, etc. This is useful for comparing custom objects.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
person1 = Person("Alice", 30)
person2 = Person("Bob", 25)
print(person1 == person2) # Output: False
print(person1 < person2) # Output: False
print(person1 > person2) # Output: True (uses __lt__ in reverse)
Here, we've defined __eq__
for equality and __lt__
for less-than. Python can infer other comparisons from these.
Magic Method | Operator | Purpose |
---|---|---|
__eq__ |
== |
Equality |
__ne__ |
!= |
Inequality |
__lt__ |
< |
Less than |
__le__ |
<= |
Less than or equal |
__gt__ |
> |
Greater than |
__ge__ |
>= |
Greater than or equal |
By implementing these, you can use comparison operators directly on your objects.
Slots for Memory Optimization
By default, Python objects store their attributes in a dictionary, which allows dynamic addition of attributes but consumes more memory. If you have a class with a fixed set of attributes, you can use __slots__
to save memory by preventing the creation of __dict__
.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # This would raise an AttributeError
With __slots__
, you cannot add new attributes dynamically, but memory usage is reduced.
Aspect | With __slots__ |
Without __slots__ |
---|---|---|
Memory Usage | Lower | Higher |
Dynamic Attributes | Not allowed | Allowed |
Access Speed | Slightly faster | Slightly slower |
Use __slots__
when:
- You have a large number of instances.
- The attributes are fixed and known in advance.
- Memory efficiency is a concern.
Metaclasses
Metaclasses are advanced topic often referred to as "classes of classes". They define how classes behave. The default metaclass is type
. You can create custom metaclasses by subclassing type
.
This is powerful but complex, so use it sparingly. Here’s a simple example:
class Meta(type):
def __new__(cls, name, bases, dct):
dct['class_id'] = 123
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=Meta):
pass
print(MyClass.class_id) # Output: 123
This metaclass adds a class_id
attribute to any class that uses it.
Metaclass Component | Purpose | Example |
---|---|---|
__new__ |
Controls class creation | def __new__(cls, name, bases, dct): |
type |
Default metaclass | class MyClass(metaclass=Meta): |
Metaclasses are used for: - API design where you want to enforce patterns. - Registration of classes. - Adding class-level attributes or methods automatically. - They are deep magic—use only if necessary.
Best Practices
To write clean and maintainable OOP code in Python, follow these best practices:
- Use meaningful names for classes, attributes, and methods.
- Prefer composition over inheritance when possible.
- Keep classes focused on a single responsibility.
- Use properties to encapsulate attribute access.
- Document your classes and methods with docstrings.
- Write unit tests for your classes.
class Employee:
"""A class to represent an employee."""
def __init__(self, name, salary):
self.name = name
self._salary = salary
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, value):
if value < 0:
raise ValueError("Salary cannot be negative")
self._salary = value
def __str__(self):
return f"Employee: {self.name}, Salary: {self.salary}"
This class follows good practices: it has a docstring, uses properties for validation, and provides a clear string representation.
Practice | Benefit | Example |
---|---|---|
Single Responsibility | Easier to maintain | One class, one purpose |
Encapsulation | Prevents accidental misuse | Use properties with validation |
Documentation | Better understanding | Docstrings |
Testing | Reliable code | Unit tests for methods |
By adhering to these, you'll write robust and professional OOP code.
I hope this cheat sheet serves as a valuable resource in your Python OOP journey. Practice these concepts with small projects, and soon they'll become second nature. Happy coding!