Advanced Python Concepts - From Functions to Classes¶
We've built a strong foundation with our developer tools; now it's time to level up our Python skills. Today, we're transitioning from writing simple scripts to building more structured and organized applications. We'll explore advanced functional programming and then dive into Object-Oriented Programming (OOP), a powerful paradigm for managing complexity and modeling real-world entities in your code.
Advanced Functional Programming¶
While we've used functions to organize code, Python also has powerful features that let us treat functions as "first-class citizens." This means we can pass them as arguments, return them from other functions, and assign them to variables.
List/Set Comprehensions¶
List/set comprehensions provide a concise and elegant way to create lists or sets. They are often more readable and efficient than using a for loop. Sometimes this is simpler than defining a custom function/lambda and loops.
*args and **kwargs¶
These special syntax features allow functions to accept a variable number of arguments.
*args (non-keyword arguments): Gathers any number of positional arguments into a tuple.
**kwargs (keyword arguments): Gathers any number of keyword arguments into a dictionary.
They are incredibly useful for writing flexible functions that can handle many different inputs.
Lambda Functions¶
Lambda functions are small, anonymous functions defined with the lambda keyword. They are perfect for one-off operations where a full def statement would be overkill.
Higher-Order Functions and functools.partial¶
A higher-order function is one that takes another function as an argument. The built-in map() and filter() functions are great examples. We can use them with lambda functions to write very concise, readable code.
Sometimes, you need to use a function but want to "pre-fill" some of its arguments. That's what functools.partial is for. It lets you create a new, simpler version of a function with some of its arguments already provided.
Function Decorators¶
A decorator is a special kind of higher-order function that adds new functionality to an existing function without changing its structure. They are commonly used for tasks like logging, timing, and access control. We apply a decorator using the @ syntax.
graph TD
A[Original Function] --> B{Decorator}
B -- wraps --> C[New, Decorated Function]
Here is a simple example of a decorator that prints a message before and after a function runs.
Object-Oriented Programming (OOP)¶
The two fundamental concepts in OOP are classes and objects.
- A class is a blueprint or a template for creating objects. Think of a class as a cookie cutter.
- An object is an instance of a class. It's the actual thing you create from the blueprint. Following our analogy, an object is the actual cookie.
Defining Classes, Methods, and Dunder Methods¶
You define a class using the class keyword. A method is a function defined inside a class that describes an object's behavior.
A dunder method (named for "double underscore") is a special method used by Python to implement core functionality. The most common is __init__, the initializer, which runs when you create a new object. Another useful one is __str__, which provides a user-friendly string representation of an object.
Dun Dun Dun
There are numerous powerful dunder methods that can super power your classes. See here for other examples.. For example, __add__ can be used to define how you add two custom objects together.
Inheritance (The "Is A" Relationship)¶
Inheritance is a core concept that allows a class to inherit attributes and methods from another class. The inheriting class is called the child or subclass, and the class it inherits from is the parent or superclass. This is powerful for modeling relationships like "is a." A Student is a User, so the Student class can inherit from the User class.
classDiagram
class User {
+name
+email
+login()
}
class Student {
+major
+enroll()
}
User <|-- Student
To __init__ or not to __init__
When inheriting from a parent class which contains an __init__ method, you don't have to supply an __init__ in the child class. However, if you need to extend the initializer to include additional properties, you can use the super().__init__(...).
Composition (The "Has A" Relationship)¶
Composition is when a class contains an instance of another class as an attribute. It models a "has a" relationship, and it is often preferred over inheritance because it's more flexible.
-
Let's define a simple
Engineclass incore/components.py. This class has the behavior of starting and stopping.
What is if __name__ == "__main__"?
This a convinent way to distinguish functionality between when you import the module and when you run it directly - and it is completely optional. It is incredibly useful for debugging purposes or documeting example usage. In other words, when you import from core/components.py, everything above the conditional is executed/defined, however the conditional itself only evaluates if you execute core/components.py directly. Now your file is accessible as both a stand-alone script as well as an importable module.
-
Now, in
transport/cars.py, we'll create aCarclass that has anEngine. We do this by creating an instance of theEngineclass within theCar's__init__method.transport/cars.py
Inheritance vs. Composition¶
| Concept | Relationship | Analogy | When to Use |
|---|---|---|---|
| Inheritance | "is a" | A Car is a Vehicle. | When a class is a specialized version of another. |
| Composition | "has a" | A Car has a Engine. | When a class is composed of other objects. |
Using both of these correctly is key to building robust and scalable applications.
Static and Class Methods¶
These are methods that belong to the class rather than an individual object.
- A static method doesn't require access to the instance (
self) or the class (cls). It's just a regular function logically grouped within the class. We use the@staticmethoddecorator. - A class method receives the class itself as the first argument (
cls). This is often used as a "factory method" to create an instance of the class from a different format. We use the@classmethoddecorator.
Recommended Exercises & Homework¶
For homework, you'll be tasked with applying these new functional and OOP concepts.
Development Environment¶
Recall, what you know about Git, Docker, and DevContainers to set yourself up for success. Set up a development environment which you can use for the rest of this advanced python section - and don't reinvent the wheel.
Create a Class Hierarchy (Inheritance - "is a")¶
- Create a parent class named
Shapewith an__init__method that takes acoloras an argument. - Create two child classes,
CircleandSquare, that inherit fromShape. - The
Circleclass should have an instance attribute forradius. - The
Squareclass should have an instance attribute forside_length. - Both
CircleandSquareshould have a method calledarea()that calculates and returns their respective area.
More Advanced Abstractions
There are simple ways to implement the above exercise. However, you can look into another advanced technique, Abstract Base Class (abc), to do this better.
Add Dunder Methods and Inheritance¶
- To your
Shape,Circle, andSquareclasses, add a__str__method that returns a user-friendly string representation of the object (e.g.,"A red square with a side length of 5 and area 25."). - Implement a class method on the
Shapeclass calledcreate_red_shape(cls). This method should return a new instance of the class (cls) with the color already set to'red'. - Change the
areacalculation to a private method, which is automatically called sets aself.areaproperty upon initialization. - Implement
__add__and__sub__methods, which defines you to add/subtract shapes together. As an example, make it so when you add two shapes, you return the sum of each area.
Modeling a System with Composition ("has a")¶
This exercise focuses on the "has a" relationship, where one object contains other objects.
-
Part A: Create Component Classes
- Create a class named
Enginewith an__init__method that acceptsfuel_type(e.g., 'gasoline', 'electric'). - The
Engineclass should have a methodstart()that prints a message indicating the engine is starting with its fuel type (e.g.,"Gasoline engine roaring to life!"). - Create a class named
Wheelwith an__init__method that acceptsdiameter(an integer). - The
Wheelclass should have a methodrotate()that prints a message like"Wheel (20 inches) is rotating.".
- Create a class named
-
Part B: Build the Main Class using Composition
- Create a class named
Carwith an__init__method that takesmodel(string),fuel_type(string), andwheel_size(integer) as arguments. - Inside the
Car's__init__method, instantiate oneEngineobject and fourWheelobjects using the provided arguments. - The
Carobject should not inherit fromEngineorWheel.
- Create a class named
-
Part C: Delegate Behavior
- The Car class should have a method
drive(). This method should:- Call the
start()method of its internalEngineobject. - Iterate over its list of
Wheelobjects and call therotate()method on each one. - Print a final message:
"The [model] is ready to go!"
- Call the
- The Car class should have a method
Goal: This exercise demonstrates how the Car class composes its behavior by delegating tasks to its internal Engine and Wheel objects.
Decorators Challenge¶
- Create a new Python file and write a decorator called
@timer. This decorator should print the executions time of the wrapped function. - Apply this decorator to the
area()method on yourCircleandSquareclasses.