DATA ANALYSIS  

Classes: Introduction

📥
You can download the original Jupyter Notebook for this lesson: classes_introduction.ipynb

This article provides a guide to understanding and using classes in Python. Classes are a fundamental part of Python's Object-Oriented Programming (OOP) paradigm, allowing you to bundle data and functionality together.

1. What is a Class?

A class is a blueprint for creating objects. It defines:

  • data (attributes) and
  • behavior (methods).

Think of it like a template: Class -> blueprint, Object -> actual instance.

# Example Class class Person: def __init__(self, name, year, citizenship): self.name = name self.year = year self.citizenship = citizenship
# Creating Objects (Instances) person1 = Person("Alicja", 1990, "Polish") person2 = Person("James", 1985, "American") print(person1.name) # Alicja print(person2.name) # James

When we call Person("Alicja", 1990, "Polish"), Python creates a new object and automatically calls the __init__ method. The person1 and person2 instances are initialized this way.

The self parameter is a reference to the current instance of the class. A reference is essentially the memory address—the specific location in your computer's RAM—where that object's data is stored. It allows the object to access its own attributes and methods. We don't need to pass it manually; Python handles it for us automatically.

2. Adding Methods (Behavior)

class Person: def __init__(self, name, year, citizenship): self.name = name self.year = year self.citizenship = citizenship def greet(self): return f"Hello, my name is {self.name}." def age(self, current_year): return current_year - self.year p1 = Person("Alicja", 1990, "Polish") print(p1.greet()) print(p1.age(2025))

Tasks:

  1. Create a Book class with attributes title, author, and pages. Implement an __init__ method that prints "New book: [title]", and an is_long() method that returns True if pages > 300. Finally, create two instances and print their details.
  2. Add a get_detailed_info() method that returns a string formatted as "'Title' by Author, [X] pages".
  3. Use the is_long() method to print a message like "This is a long read!" for books with more than 300 pages.
  4. Add a method update_pages(new_pages) that allows you to change the page count of the book instance.

3. Default Values

class Person: def __init__(self, name, year, citizenship="Unknown"): self.name = name self.year = year self.citizenship = citizenship def greet(self): return f"Hi, I'm {self.name}." p1 = Person("John", 2005) p2 = Person("Luke", 1990, 'American') print(p1.citizenship) # Unknown print(p2.citizenship) # American

You can provide default values for parameters in the __init__ method. If a value is not provided when creating an instance, the default value will be used. This makes your classes more flexible and easier to use, as you only need to provide essential information.

Tasks:

  1. Create a Product class where the price attribute has a default value of 0.0. Instantiate one product with a price and another without, then print both.

4. Instance Attributes vs Class Attributes

A class attribute is a variable that is shared by all instances of a class. Unlike instance attributes (which are unique to each object), class attributes are defined outside of any methods (usually right at the top of the class). They are accessed using the class name itself or through any instance.

class Person: # Class Attributes: Shared by all instances of the class number_of_people = 0 planet = "Earth" def __init__(self, name, year, citizenship): self.name = name self.year = year self.citizenship = citizenship # Increment class attribute whenever a new instance is created Person.number_of_people += 1 p1 = Person("Luke", 2001, "Polish") p2 = Person("Bob", 1985, "German") print(Person.number_of_people)

Tasks:

  1. Create a Car class with a class attribute wheels = 4. Instantiate two cars, verify they show 4 wheels, then change Car.wheels to 6 and check both instances to see the shared update.
  2. Add instance attributes brand and model to the Car class. Create an instance for a "Toyota Corolla".
  3. Add a class attribute fuel_type = 'Petrol' and demonstrate that all instances access the same value.
  4. Add a method drive() that prints "The [brand] [model] is now driving on [wheels] wheels."

5. Understanding Decorators

A decorator is a design pattern in Python that allows you to modify the behavior of a function or class without permanently changing its source code. Essentially, a decorator is a function that takes another function as an argument and returns a new function that "wraps" the original one.

This is possible because in Python, functions are first-class objects, meaning they can be passed around as arguments, returned from other functions, and assigned to variables.

We can use built-in decorators like @classmethod or @staticmethod (explained in the next section), or create custom decorators for various purposes such as logging, timing, or authorization.

To modify the behavior of a function using a built-in decorator, it is enough to place the decorator name above the function definition, preceded by @.

In the case of custom decorators, we follow the same approach, as shown in the example below, where we place @log_execution above def greet(name): . However, the decorator must be defined beforehand.

*args and **kwargs require further explanation, but for the purpose of below example it is enough to know that this construction allows all arguments passed to the greet() function to be forwarded to the wrapper() function, and then passed again—in the same form—to the original greet() function. Thanks to this, we can execute greet() from within wrapper() using exactly the same arguments as if greet() were called directly, without a decorator.

def log_execution(func): def wrapper(*args, **kwargs): print(f"Executing {func.__name__}...") result = func(*args, **kwargs) print(f"{func.__name__} finished.") return result return wrapper @log_execution def greet(name): return f"Hello, {name}!" print(greet("Alice"))

The greet() function is called with the argument "Alice". If it were executed without the decorator, only "Hello, Alice" would be printed. However, the @log_execution decorator extends its behavior by printing two additional lines: "Executing greet..." and "greet finished.".

Tasks:

  1. Create a decorator bold_decorator that wraps a function returning a string and adds <b> and </b> tags around it.
  2. Apply it to a function get_text() that returns "Hello World".

6. Class Methods vs Static Methods

class Person: # Class Attributes: Shared by all instances of the class number_of_people = 0 planet = "Earth" def __init__(self, name, year, citizenship): self.name = name self.year = year self.citizenship = citizenship # Increment class attribute whenever a new instance is created Person.number_of_people += 1 def greet(self): return f"Hello, my name is {self.name}." @classmethod def get_population(cls): return f"Current registered population: {cls.number_of_people}" @staticmethod def is_adult(age): return age >= 18 p1 = Person("Luke", 2001, "Polish") p2 = Person("Bob", 1985, "German") p3 = Person("Kathrine", 1985, "American") print(Person.get_population()) print(p3.get_population()) # Example of using is_adult static method print(f"Is Luke an adult? {Person.is_adult(24)}") print(f"Is age 15 an adult? {Person.is_adult(15)}")
  • @classmethod: Receives the class as its first argument (cls). Use it when you need to access class-level attributes or create alternative constructors (factory methods).
  • @staticmethod: Does not receive self or cls. It behaves like a regular function but lives inside the class namespace. Use it for utility functions that relate to the class logic but don't need to access its state.

Key Difference: A class method can modify class state that applies across all instances; a static method is self-contained and doesn't depend on the class or instance state at all.

Tasks:

  1. In the Person class, add a @classmethod called from_string(cls, person_str) that parses a string like "Alice,1990,Polish" and returns a new Person instance.
  2. Add a @staticmethod called is_valid_citizenship(citizenship) that returns True if the citizenship is in a predefined list of allowed countries.

7. Scope: Global vs Local Variables in Classes

To use global variables inside a class method, you can simply refer to them by name. However, if you need to modify a global variable, you must use the global keyword within the method.

Variables created inside a function or method (local variables) can only be accessed within that scope. In contrast, attributes assigned to self are instance attributes and are accessible by all instance methods within the class.

counter = 0 # Global variable class Tracker: def __init__(self, name): self.name = name # Instance attribute def increment_global(self): global counter counter += 1 def local_demo(self): temp_val = "I only exist here" # Local variable print(temp_val) t = Tracker("Alpha") t.increment_global() print(f"Global counter: {counter}") t.local_demo()

Tasks:

  1. Create a global variable APP_MODE set to "development". Then implement a class Application with a method show_mode() that prints the current mode using the global variable. ( Do not pass the mode as a parameter. Access the global variable directly inside the method)
  2. You are building a visitor counter. Create a global variable visits = 0 and a class Website. Each time the method visit() is called, it should increment the global counter. Add a method show_visits() that prints the total visits.

8. Method Overriding

class Student(Person): def __init__(self, name, year, citizenship, university): super().__init__(name, year, citizenship) self.university = university def greet(self): return f"Hi, I'm {self.name} and I'm a student." s = Student("Alice", 2000, "Polish", "University of Warsaw") print(s.greet()) # Calls overridden method in Student print(Person.greet(s)) # Explicitly calls method from Parent (Person) class

Method overriding occurs when a child class provides a specific implementation of a method that is already defined in its parent class. This is helpful when you want to change or extend the behavior of a parent method to suit the needs of the child class, allowing for polymorphism.
In this section, this concept is only introduced. For a better understanding, refer to the article Classes: Object-Oriented Programming Paradigms.

Tasks:

  1. Create a Shape class with a method area() that returns 0.
  2. Create a Square class that inherits from Shape, takes side in its __init__, and overrides area() to return side * side.
  3. Create a Circle class that inherits from Shape, takes radius, and overrides area() using math.pi.
  4. Use super() in a ColoredSquare class (inheriting from Square) to set both side and color during initialization.

Note on super(): We use super() because both the parent and child classes define an __init__ method, and the child's version overrides the parent's. However, if the parent has a method that the child does not redefine, the child inherits it automatically and can call it without super().

9. Documentation

A docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. Following PEP 257, docstrings should be enclosed in triple double quotes """. They are essential for documentation and are used by tools like Sphinx, pdoc, and Doxygen to generate external manuals.

  • __str__: Returns a user-friendly string representation of an object (intended for end-users).
  • __repr__: Returns an unambiguous string representation of an object, ideally matching the command used to create it (intended for developers/debugging).
from datetime import datetime class Person: """ A class to represent a person. Attributes: name (str): The name of the person. year_of_birth (int): The year the person was born. citizenship (str): The citizenship of the person (default is 'Polish'). """ # Class Attribute: Shared by all instances of the class number_of_people = 0 def __init__(self, name: str, year: int, citizenship: str = 'Polish'): """ The constructor method. Initializes the attributes for each new instance. """ self.name = name self.year_of_birth = year self.citizenship = citizenship # Increment class attribute whenever a new instance is created Person.number_of_people += 1 print(f"Creating profile for {self.name}...") # Instance Method: Operates on an instance of the class def calculate_age(self): """ Calculates and returns the age of the person based on the current year. """ current_year = datetime.now().year return current_year - self.year_of_birth # Static Method: Does not require access to instance or class (utility function) @staticmethod def is_adult(age): """ A utility function to check if a given age is adult. """ return age >= 18 # Class Method: Operates on the class itself, not on an instance @classmethod def get_population(cls): """ Returns the total number of Person instances created. """ return f"Current registered population: {cls.number_of_people}" # Special Magic Method: Changes how the object is represented as a string def __str__(self): return f"Person(name={self.name}, age={self.calculate_age()})" def __repr__(self): return f"Person('{self.name}', {self.year_of_birth}, '{self.citizenship}')" # Usage of the Person class print("--- 1. Creating Instances ---") person1 = Person("Andrew", 1980) person2 = Person("James", 2000) print(f"Name: {person1.name}, Age: {person1.calculate_age()}") print(f"Name: {person2.name}, Age: {person2.calculate_age()}") print(" --- 2. Class vs Instance Attributes ---") print(f"Total people: {Person.number_of_people}") print(Person.get_population()) print(" --- 3. Static Methods ---") age = person2.calculate_age() print(f"Is {person2.name} an adult? {'Yes' if Person.is_adult(age) else 'No'} ") print("--- 4. String Representations ---") print(str(person1)) # Uses __str__ print(repr(person1)) # Uses __repr__

Tasks:

  1. Add comprehensive docstrings to your previously created Book class.
  2. Implement the __str__ method to show a friendly message like "Book: Quo Vadis by Henryk Sienkiewicz - 600 pages".
  3. Implement the __repr__ method to return a string like "Book(title='Quo Vadis', author='Henryk Sienkiewicz', pages=600)".

Contact details:

+48 790-430-860

analysislessons@gmail.com