feat (curriculum): add oop and encapsulation lectures (#62121)

Co-authored-by: Ilenia <26656284+ilenia-magoni@users.noreply.github.com>
Co-authored-by: Sem Bauke <sem@freecodecamp.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kolade Chris
2025-09-25 21:00:31 +01:00
committed by GitHub
parent 35d05d2c05
commit 1f8acc6df5
7 changed files with 1511 additions and 36 deletions

View File

@@ -1,135 +1,283 @@
---
id: 68420be9af9d89620af7944a
# title needs to be updated to correct title when lectures are finalized
title: Understanding Object Oriented Programming and Encapsulation
title: What is Object-Oriented Programming, and How Does Encapsulation Work?
challengeType: 19
# dashedName needs to be updated to correct title when lectures are finalized
dashedName: lecture-understanding-object-oriented-programming-and-encapsulation
dashedName: what-is-object-oriented-programming-and-how-does-encapsulation-work
---
# --description--
Watch the video or read the transcript and answer the questions below.
Object-oriented programming, also known as OOP, is a programming style in which developers treat everything in their code like a real-world object.
A class is like a blueprint for creating objects. Every single object created from a class has attributes that define data and methods that define the behaviors of the objects.
In a previous lecture, you learned how to create classes. Here's a reminder of the syntax:
```py
class ClassName:
def __init__(self, parameters):
attribute = value
def method_name(self):
# method logic
```
Here's an example of a class that uses the `__init__` special method to initialize the brand and color attributes whenever an object is created using the class:
```py
class Car:
def __init__(self, brand, color):
self.brand = brand
self.color = color
# create two objects from the Car class
car1 = Car('Toyota', 'red')
car2 = Car('Lambo', 'green')
print('Car 1 Brand:', car1.brand) # Car 1 Brand: Toyota
print('Car 1 Color:', car1.color) # Car 1 Color: red
print('Car 2 Brand:', car2.brand) # Car 2 Brand: Lambo
print('Car 2 Color:', car2.color) # Car 2 Color: green
```
Object-oriented programming has four key principles that help you organize and manage code effectively. They are encapsulation, inheritance, polymorphism, and abstraction.
The rest of this lecture will focus on how encapsulation works.
Encapsulation is the bundling of the attributes and methods of an object into a single unit, the class.
With encapsulation, you can hide the internal state of the object behind a simple set of public methods and attributes that act like doors. Behind those doors are private attributes and methods that control how the data changes and who can see it.
Let's say you want to track a wallet balance. You want to allow people to deposit or withdraw money from the wallet, but no one should be able to tamper with the balance directly.
In that case, you can make `deposit()` and `withdraw()` public methods, and you hide the balance under the `_balance` attribute:
```py
class Wallet:
def __init__(self, balance):
self._balance = balance # For internal use by convention
def deposit(self, amount):
if amount > 0:
self._balance += amount # Add to the balance safely
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount # Remove from the balance safely
```
By convention, prefixing attribute and methods with a single underscore means they are meant for internal use. No one should directly access them from outside the class since it defies the principles of encapsulation, which can lead to bugs.
While a single underscore prefix is just a convention, prefixing attributes and methods with a double underscore effectively prevents them to be accessed from the outside of their class, making those attributes and methods private.
```py
class Wallet:
def __init__(self, balance):
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount # Add to the balance safely
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount # Remove from the balance safely
account = Wallet(500)
print(account.__balance) # AttributeError: 'Wallet' object has no attribute '__balance'
```
To get the current value of `__balance`, you can define a get_balance method. For example:
```py
class Wallet:
def __init__(self, balance):
self.__balance = balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
def get_balance(self):
return self.__balance
acct_one = Wallet(100)
acct_one.deposit(50)
print(acct_one.get_balance()) # 150
acct_two = Wallet(450)
acct_two.withdraw(28)
print(acct_two.get_balance()) # 422
acct_two.deposit(150)
print(acct_two.get_balance()) # 572
```
You can also define a private `__validate` method to check if every deposit or withdrawal amount is a positive number:
```py
class Wallet:
def __init__(self):
self.__balance = 0
def __validate(self, amount):
if amount < 0:
raise ValueError('Amount must be positive')
def deposit(self, amount):
self.__validate(amount)
self.__balance += amount
def withdraw(self, amount):
self.__validate(amount)
if amount > self.__balance:
raise ValueError('Insufficient funds')
self.__balance -= amount
def get_balance(self):
return self.__balance
acct_one = Wallet()
acct_one.deposit(4) # ValueError('Amount must be positive')
print(acct_one.get_balance()) # 0
acct_one.deposit(50)
print(acct_one.get_balance()) # 50
acct_one.withdraw(-8) # ValueError('Amount must be positive')
acct_one.withdraw(58) # ValueError('Insufficient funds')
```
As you can see, the `__validate` method is private, and runs behind the scenes in the `deposit()` and `withdraw()` public methods to make sure the amount is always valid.
In a coming lecture, you will learn more about how attributes prefixed with a double underscore works.
In summary, encapsulation locks down internal data behind clear public methods. That's how you keep your classes safe from tampering and centralize validation in one place. You can update or extend your code freely, knowing that outside code only touches the interfaces you expose.
# --questions--
## --text--
Question 1
What are the four key principles of Object-Oriented Programming (OOP)?
## --answers--
Answer 1.1
Encapsulation, Inheritance, Polymorphism, Abstraction
### --feedback--
Feedback 1
Look out for the principles that define how data and behavior are organized in OOP.
---
Answer 1.2
Variables, Functions, Loops, Conditionals
### --feedback--
Feedback 1
Look out for the principles that define how data and behavior are organized in OOP.
---
Answer 1.3
Classes, Objects, Methods, Attributes
### --feedback--
Feedback 1
Look out for the principles that define how data and behavior are organized in OOP.
---
Answer 1.4
Public, Private, Protected, Static
### --feedback--
Feedback 1
Look out for the principles that define how data and behavior are organized in OOP.
## --video-solution--
5
1
## --text--
Question 2
Why is it a bad practice to directly access attribute/methods prefixed with a single underscore?
## --answers--
Answer 2.1
Because Python will raise a syntax error if you try.
### --feedback--
Feedback 2
Think about how encapsulation exists to protect an object's internal state.
---
Answer 2.2
Because it violates encapsulation, allowing uncontrolled data access.
### --feedback--
Feedback 2
Think about how encapsulation exists to protect an object's internal state.
---
Answer 2.3
Because private properties are slower to access than public ones.
### --feedback--
Feedback 2
Think about how encapsulation exists to protect an object's internal state.
---
Answer 2.4
Because they can only be accessed from child classes.
### --feedback--
Feedback 2
Because they can only be accessed from child classes.
## --video-solution--
5
2
## --text--
Question 3
What is the way for indicating private attributes or methods?
## --answers--
Answer 3.1
Prefix with a single underscore
### --feedback--
Feedback 3
Look out for the symbols used to prefix the name of a private attribute or method.
---
Answer 3.2
Use leading and trailing double underscores
### --feedback--
Feedback 3
Look out for the symbols used to prefix the name of a private attribute or method.
---
Answer 3.3
Prefix with a double underscore
### --feedback--
Feedback 3
Look out for the symbols used to prefix the name of a private attribute or method.
---
Answer 3.4
Use the private keyword
### --feedback--
Feedback 3
Look out for the symbols used to prefix the name of a private attribute or method.
## --video-solution--
5
3

View File

@@ -0,0 +1,260 @@
---
id: 68c128cbd77e4ba9ed671937
title: What are Getters and Setters
challengeType: 19
dashedName: what-are-getters-and-setters
---
# --description--
Getters and setters are methods that let you control how the attributes of a class are accessed and modified. With getters you retrieve a value, and with setters you set a value.
These actions are done through what's known as properties. They are what connect getters and setters, and allow access to data.
Properties act like attributes but behave like methods under the hood. Think of them as data you define like methods, but work like attributes. This means you can access properties with dot notation instead of parentheses or round brackets.
The main thing properties do is that they run extra logic behind the scenes when you get, set, or delete values with them. This makes them the perfect choice when you want to access or manipulate data within objects.
So why use properties for that instead of methods? It's mostly about readability and convention. They make your code cleaner and easier to read.
When you use a method, you always have to call it with parentheses. But with a property, you can access it just like a normal attribute using dot notation. That makes your code look simple even when it is doing extra work behind the scenes.
For example, you might want to calculate a value or check that a new value is valid before saving it. Instead of calling a method for that, you can use an attribute-like way to do that.
To create a property, you define a method and place the @property decorator above it. This tells Python to treat the method as a property.
That takes us to getters. Here's how to create one with the @property decorator:
```py
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self): # A getter to get the radius
return self._radius
@property
def area(self): # A getter to calculate area
return 3.14 * (self._radius ** 2)
my_circle = Circle(3)
print(my_circle.radius) # 3
print(my_circle.area) # 28.26
```
This example gets a radius and the area of a circle.
Notice how we used `_radius` instead of radius inside the class. The underscore is a common Python convention to show that an attribute is meant to be private. In other words, it signifies that it's for internal use and should not be accessed directly from outside the class.
To make a setter to create the radius, for example, you have to define another method with the same name and use `@<property_name>.setter` above it:
```py
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self): # A getter to get the radius
return self._radius
@radius.setter
def radius(self, value): # A setter to set the radius
if value <= 0:
raise ValueError('Radius must be positive')
self._radius = value
my_circle = Circle(3)
print('Initial radius:', my_circle.radius) # Initial radius: 3
my_circle.radius = 8
print('After modifying the radius:', my_circle.radius) # After modifying the radius: 8
```
In this example, the radius setter is not just setting the radius for the circle, it's also running a validation that makes sure the radius is not a negative number.
Once you define getters and setters, Python automatically calls them under the hood whenever you use normal attribute syntax:
```py
my_circle.radius # This will call the getter
my_circle.radius = 4 # This will call the setter
```
Note that inside the setter, you cannot use same name of the property when assigning a new value. That's because `self.radius = value` will call the setter within the setter method itself, leading to infinite recursion and a `RecursionError`. So you must always use the underscore-prefixed form `self._radius = value`.
Just like you can control how an attribute is accessed through getter and how it is modified with setter, you can also control how it is deleted using a deleter.
A deleter runs custom logic when you use the del statement on a property. To create one, you use the `@<property_name>.deleter` decorator:
```py
class Circle:
def __init__(self, radius):
self._radius = radius
# Getter
@property
def radius(self):
return self._radius
# Setter
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
# Deleter
@radius.deleter
def radius(self):
print("Deleting radius...")
del self._radius
```
Here's how the deleter can be put to use:
```py
# Create circle object with a radius
my_circle = Circle(33)
print("Initial radius:", my_circle.radius) # 33
# Delete the radius
# This calls the deleter
del my_circle.radius # Deleting radius...
print("Radius deleted!") # Radius deleted!
# Try to access radius after deletion
try:
print(my_circle.radius)
except AttributeError as e:
print("Error:", e) # Error: 'Circle' object has no attribute '_radius'
```
The takeaway from this is that:
- Getters let you retrieve a value or even compute a value on the fly.
- Setters let you modify the values safely by running checks before assignment.
- Properties are what tie these getters and setters together so you can write logic while still using dot notation.
- Deleters let you define what happens when an attribute is deleted.
# --questions--
## --text--
What lets you run logic behind the scenes while getting or setting an attribute's value?
## --answers--
Importing external modules.
### --feedback--
Think about methods that allow validation or computation when reading and writing data.
---
Class inheritance.
### --feedback--
Think about methods that allow validation or computation when reading and writing data.
---
Properties
### --feedback--
Think about methods that allow validation or computation when reading and writing data.
---
Direct attribute access.
### --feedback--
Think about methods that allow validation or computation when reading and writing data.
## --video-solution--
3
## --text--
What ties getters and setters together so you can execute logic while maintaining dot notation access?
## --answers--
Properties
### --feedback--
Think about a feature that lets you use methods like attributes with simple dot syntax.
---
Decorators
### --feedback--
Think about a feature that lets you use methods like attributes with simple dot syntax.
---
Class inheritance
### --feedback--
Think about a feature that lets you use methods like attributes with simple dot syntax.
---
Direct method calls
### --feedback--
Think about a feature that lets you use methods like attributes with simple dot syntax.
## --video-solution--
1
## --text--
What two decorators are used to create getters and setters for a property?
## --answers--
`@getter` and `@setter`
### --feedback--
Think about decorators that allow method calls to use simple dot notation without parentheses.
---
`@attr.get` and `@attr.set`
### --feedback--
Think about decorators that allow method calls to use simple dot notation without parentheses.
---
`@compute` and `@assign`
### --feedback--
Think about decorators that allow method calls to use simple dot notation without parentheses.
---
`@property` and `@<property_name>.setter`
### --feedback--
Think about decorators that allow method calls to use simple dot notation without parentheses.
## --video-solution--
4

View File

@@ -0,0 +1,255 @@
---
id: 68c3ba8940e4df8afa83a723
title: What Is Inheritance and How Does It Promote Code Reuse?
challengeType: 19
dashedName: what-is-inheritance-and-how-does-it-promote-code-reuse
---
# --description--
Inheritance is the next key concept of object-oriented programming (OOP) we'll cover.
Let's take a deeper look at this concept and how it lets you write reusable code.
With inheritance, a subclass (or child class) can use the attributes and methods of a base class (or parent class). This allows you to reuse code, create clear class hierarchies, and customize behavior without rewriting everything. You can customize by extending existing methods or overriding them in the child class.
Here's the basic syntax for inheritance:
```py
class Parent:
# Parent attributes and methods
class Child(Parent):
# Child inherits, extends, and/or overrides where necessary
```
For the `Child` class to inherit from the `Parent` class, you have to pass the Parent to the Child.
This style is called single inheritance, since a child class inherits from exactly one parent class.
Here's an example:
```py
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
return f'{self.name} makes a sound'
class Dog(Animal):
bark = 'woof! woof!! woof!!!'
jack = Dog('Jack')
print(jack.sound()) # Jack makes a sound
print(jack.bark) # woof! woof!! woof!!!
```
You can see that we're able to reuse the `self.name` attribute and the `sound()` method from the parent `Animal` class in the child `Dog` class.
Let's override the `sound()` method from the parent `Animal` class in the child `Dog` class so we can have `sound()` use the `bark` class variable:
```py
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
return f'{self.name} makes a sound.'
class Dog(Animal):
bark = 'woof! woof!! woof!!!'
# Override sound() to use bark class variable
def sound(self):
return f'{self.name} barks {self.bark}'
jack = Dog('Jack')
print(jack.sound()) # Jack barks woof! woof!! woof!!!
```
If you want to keep the return value of `sound()` and add the bark class variable later, you can extend `sound()` by using the `super()` function:
```py
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
return f'{self.name} makes a sound'
class Dog(Animal):
bark = 'woof! woof!! woof!!!'
# Call Animal.sound(), then append bark
def sound(self):
base = super().sound()
return f'{base}, then {self.name} barks {self.bark}'
jack = Dog('Jack')
print(jack.sound()) # Jack makes a sound, then Jack barks woof! woof!! woof!!!
```
In this example, `base` is the result of calling the `sound()` method from the `Animal` class, and then we append the `Dog` class's specific sound to it. This way, you can extend the functionality of the parent `Animal` class while still keeping its original behavior.
There's also multiple inheritance, where a child class can inherit from more than one parent class.
Here's the basic syntax of multiple inheritance:
```py
class Parent:
# Attributes and methods for Parent
class Child:
# Attributes and methods for Child
class GrandChild(Parent, Child):
# GrandChild inherits from both Parent and Child
# GrandChild can combine or override behavior from each
```
A simple way to demonstrate multiple inheritance is with a frog, which can both walk on land and swim in water:
```py
class Walker:
def walk(self):
return 'I can walk on land'
class Swimmer:
def swim(self):
return 'I can swim in water'
# Amphibian inherits from both Walker and Swimmer
class Amphibian(Walker, Swimmer):
def __init__(self, name):
self.name = name
def introduce(self):
return f"I'm {self.name} the frog. {self.walk()} and {self.swim()}."
frog = Amphibian('Freddy')
print(frog.introduce())
# Output: I'm Freddy the frog. I can walk on land and I can swim in water.
```
# --questions--
## --text--
What is the primary purpose of inheritance in object-oriented programming?
## --answers--
To create identical copies of a parent class.
### --feedback--
Think about how a child class can reuse and extend attributes and methods from a parent class.
---
To allow a child class to reuse and extend attributes/methods from a parent class.
### --feedback--
Think about how a child class can reuse and extend attributes and methods from a parent class.
---
To make all methods in a class private.
### --feedback--
Think about how a child class can reuse and extend attributes and methods from a parent class.
---
To prevent code duplication by merging unrelated classes.
### --feedback--
Think about how a child class can reuse and extend attributes and methods from a parent class.
## --video-solution--
2
## --text--
What is multiple inheritance in Python?
## --answers--
A child class inheriting from one parent class.
### --feedback--
Think about the style of inheritance that allows a child to inherit from several classes.
---
A parent class with multiple child classes.
### --feedback--
Think about the style of inheritance that allows a child to inherit from several classes.
---
A child class inheriting from multiple parent classes.
### --feedback--
Think about the style of inheritance that allows a child to inherit from several classes.
---
A class that cannot inherit at all.
### --feedback--
Think about the style of inheritance that allows a child to inherit from several classes.
## --video-solution--
3
## --text--
How do you make a child class inherit from a parent class in Python?
## --answers--
By calling `Parent.inherit(Child)`.
### --feedback--
The syntax involves parentheses in the class definition.
---
By copying all methods manually.
### --feedback--
The syntax involves parentheses in the class definition.
---
By using the `extends` keyword.
### --feedback--
The syntax involves parentheses in the class definition.
---
By passing the Parent class as an argument to the Child class.
### --feedback--
The syntax involves parentheses in the class definition.
## --video-solution--
4

View File

@@ -0,0 +1,263 @@
---
id: 68c3bc4ddb7b469fb7d17c28
title: What Is Polymorphism and How Does It Promote Code Reuse?
challengeType: 19
dashedName: what-is-polymorphism-and-how-does-it-promote-code-reuse
---
# --description--
Polymorphism is the next key concept of object-oriented programming (OOP) we will talk about.
With polymorphism, you have access to an interface where you can interact with many objects of the same kind.
Let's take a deeper look at polymorphism and how it lets you reuse code.
Polymorphism allows methods in different classes to share the same name but perform different tasks. You call the same method name on different objects, and each responds in its own way.
Here's the basic example of polymorphism:
```py
class A:
def action(self): ...
class B:
def action(self): ...
class C:
def action(self): ...
Class().method() # Works for A, B, or C
```
Here's an example using different animal sounds to depict polymorphism:
```py
class Cat:
def speak(self):
return "A cat meow"
class Bird:
def speak(self):
return "A bird tweet"
class Monkey:
def speak(self):
return "A monkey ooh ooh aah aah ooh ooh aah aah"
def animal_sound(animal):
print(animal.speak())
animal_sound(Cat())
animal_sound(Bird())
animal_sound(Monkey())
```
In this example, `animal_sound()` is a function that takes any object with a `speak()` method. 
When you pass in a `Cat`, `Bird`, or `Monkey`, it calls the `speak()` method of the object and prints the result. Because each class defines `speak()` differently, you get different outputs from the same function. That's polymorphism in action.
Here's another example, this time with instances and an attribute:
```py
class Twitter:
def __init__(self, content):
self.content = content
def post(self):
return f"🐦 Tweet: '{self.content}' (280 chars max)"
class Instagram:
def __init__(self, content):
self.content = content
def post(self):
return f"📸 Instagram Post: '{self.content}' + ✨ filters"
class LinkedIn:
def __init__(self, content):
self.content = content
def post(self):
return f"💼 LinkedIn Article: '{self.content}' (Professional Mode)"
def start(social_media):
print(social_media.post()) # Calls .post() on any object
# Instances
tweet = Twitter('Just learned Python polymorphism!')
photo = Instagram('Sunset vibes 🌅')
article = LinkedIn('Why OOP matters in 2024')
# The polymorphic calls - same function, different outputs
start(tweet) # 🐦 Tweet: 'Just learned Python polymorphism!' (280 chars max)
start(photo) # 📸 Instagram Post: 'Sunset vibes 🌅' + ✨ filters
start(article) # 💼 LinkedIn Article: 'Why OOP matters in 2024' (Professional Mode)
```
There's also a kind of polymorphism called **inheritance-based polymorphism**.
In inheritance-based polymorphism, a parent class defines a method, and multiple child classes override that method in their own way. You can then call the same method on any child object, and it behaves differently depending on which child class it is.
Here's an example:
```py
class Animal:
def speak(self):
return 'Some generic sound'
class Cat(Animal):
def speak(self):
return 'A cat meow'
class Dog(Animal):
def speak(self):
return 'A dog barks woof woof'
class Monkey(Animal):
def speak(self):
return 'A monkey ooh ooh aah aah ooh ooh aah aah'
print(Cat().speak()) # A cat meow
print(Dog().speak()) # A dog barks woof woof
print(Monkey().speak()) # A monkey ooh ooh aah aah ooh ooh aah aah
print(Animal().speak()) # Some generic sound
```
You can see that each child class of the parent `Animal` class overrides the `speak()` method to provide its own implementation. So when you call the `speak()` method on an instance of each subclass, it returns the specific sound associated with that animal.
You can also take things further and do the calling in a list, then loop through the list to display what the `speak()` method returns for each:
```py
animals = [Cat(), Dog(), Monkey()]
for animal in animals:
print(animal.speak())
# Output:
# A cat meow
# A dog barks woof woof
# A monkey ooh ooh aah aah ooh ooh aah aah
```
# --questions--
## --text--
What is polymorphism in object-oriented programming?
## --answers--
When a class cannot have methods with the same name.
### --feedback--
Think about how the word “polymorphism” means “many forms” of the same action.
---
When different classes can use the same method name but implement it differently.
### --feedback--
Think about how the word “polymorphism” means “many forms” of the same action.
---
When a method must be unique across all classes.
### --feedback--
Think about how the word “polymorphism” means “many forms” of the same action.
---
When a child class deletes a parent class's method.
### --feedback--
Think about how the word “polymorphism” means “many forms” of the same action.
## --video-solution--
2
## --text--
In inheritance-based polymorphism, what must a child class do to provide its own implementation of a method defined in the parent class?
## --answers--
Override the method by redefining it with the same name.
### --feedback--
Think about how a child class redefines the method while keeping the same signature.
---
Declare the method as `@static`.
### --feedback--
Think about how a child class redefines the method while keeping the same signature.
---
Delete the parent method first.
### --feedback--
Think about how a child class redefines the method while keeping the same signature.
---
Use the `@polymorphic` decorator.
### --feedback--
Think about how a child class redefines the method while keeping the same signature.
## --video-solution--
1
## --text--
What is the key benefit of inheritance-based polymorphism?
## --answers--
It forces all child classes to implement identical behavior.
### --feedback--
Look out for how polymorphism lets you call the same method on different child classes.
---
It prevents child classes from modifying parent methods.
### --feedback--
Look out for how polymorphism lets you call the same method on different child classes.
---
It allows calling the same method name on different child objects, with each executing its own version.
### --feedback--
Look out for how polymorphism lets you call the same method on different child classes.
---
It merges all child methods into the parent class.
### --feedback--
Look out for how polymorphism lets you call the same method on different child classes.
## --video-solution--
3

View File

@@ -0,0 +1,288 @@
---
id: 68c3bc80f67363a31791fe1c
title: What Is Abstraction and How Does It Help Keep Complex Systems Organized?
challengeType: 19
dashedName: what-is-abstraction-and-how-does-it-help-keep-complex-systems-organized
---
# --description--
Now that we've looked at encapsulation, inheritance, and polymorphism, let's discuss the next key concept of object-oriented programming abstraction.
Abstraction is the process of hiding complex implementation details and showing only the essential features of an object or system. Think of it as focusing on what something does rather than how it does it.
Abstraction is not limited to Python. It's a programming concept that can be implemented in many languages that support object-oriented programming.
To illustrate abstraction, imagine you're driving a car. When you're in the driver's seat, you interact with essential parts like the steering wheel, shifter, and the accelerator and brake pedals. You don't need to know the intricate details of how engine works, how the transmission shifts gears, or the physics behind the braking system, as all of those are the complex implementation details.
That's an abstraction at work! It provides you with a simplified interface to interact with a complex system.
In the case of a car again, the simplified interface is the steering wheel, brakes, and accelerator, while the complex system is the car itself.
As for how Python implements abstraction, it does so through the `abc` module.
This module provides the `ABC` class (standing for “abstract base class”) and the `@abstractmethod` decorator.
`ABC` is the class that is meant to be inherited from, but you cannot create direct objects from it. It is what defines a common interface of methods and properties that its subclasses must implement.
On the other hand, an abstract method is a method declared in an Abstract Base Class (ABC) using the `@abstractmethod` decorator. It may have no implementation or a basic default one. However, any subclass must override it to be considered concrete and instantiable, even if a default implementation is provided.
Here's the basic syntax of abstract class in Python:
```py
from abc import ABC, abstractmethod
# Define an abstract base class
class AbstractClass(ABC):
@abstractmethod
def abstract_method(self):
pass
# Concrete subclass that implements the abstract method
class ConcreteClassOne(AbstractClass):
def abstract_method(self):
print('Implementation in ConcreteClassOne')
# Another concrete subclass
class ConcreteClassTwo(AbstractClass):
def abstract_method(self):
print('Implementation in ConcreteClassTwo')
```
Here's a basic example:
```py
from abc import ABC, abstractmethod
class Animal(ABC): # Inherits from abstract base class
@abstractmethod # Abstract method decorator
def make_sound(self): # The method subclasses must override
pass
# Concrete class that will override the abstract method
class Dog(Animal):
def make_sound(self):
print('Woof!')
# Another concrete class that will override the abstract method
class Cat(Animal):
def make_sound(self):
print('Meow!')
# Another concrete class that will override the abstract method
class Monkey(Animal):
def make_sound(self):
print('Ooh ooh aah aah!')
# Create instances of each concrete class
animals = [Dog(), Cat(), Monkey()]
# Loop through the instances to call the make_sound method
for animal in animals:
animal.make_sound()
# Output:
# Woof!
# Meow!
# Ooh ooh aah aah!
```
In this example:
- We are importing the `ABC` class and `abstractmethod` from the `abc` module.
- We then create an `Animal` class that inherits from `ABC`, and create an abstract method `make_sound` in it that each subclass of `Animal` must override.
- We create the concrete classes `Dog`, `Cat`, and `Monkey`, which must override the `make_sound` abstract method.
- We instantiate the concrete classes and call their `make_sound` method to show how each of them implements the `make_sound` abstract method in its own way.
Remember that you cannot create an instance of the Animal class. Here's what happens if you try to do that:
```py
dog = Animal()
# TypeError: Can't instantiate abstract class Animal
# without an implementation for abstract method 'make_sound'
```
The same rule applies to subclasses that don't provide an implementation for the abstract method. Even if you define a subclass of Animal, you can't instantiate it until it overrides make_sound. Here's an example showing that:
```py
class Bird(Animal):
pass
bird = Bird()
# TypeError: Can't instantiate abstract class Bird
# without an implementation for abstract method 'make_sound'
```
Here's another example, this time with an instance attribute you can pass to the instances of the concrete methods:
```py
from abc import ABC, abstractmethod
# The blueprint for any toy that can speak
class TalkingToy(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def speak(self):
pass
class RobotToy(TalkingToy):
def speak(self):
print(f'{self.name} says beep boop! I am a robot!')
class TeddyBearToy(TalkingToy):
def speak(self):
print(f"{self.name} says hug me! I'm cuddly!")
class DinosaurToy(TalkingToy):
def speak(self):
print(f'{self.name} says ROOOOAR!')
# Create toys
rusty = RobotToy('Rusty')
fluffy = TeddyBearToy('Fluffy')
rex = DinosaurToy('Rex')
toys = [rusty, fluffy, rex]
for toy in toys:
toy.speak()
# Output:
# Rusty says beep boop! I am a robot!
# Fluffy says hug me! I'm cuddly!
# Rex says ROOOOAR!
```
In this example: 
- We have an abstract base class `TalkingToy` that defines a blueprint for any toy that can speak.
- The subclasses `RobotToy`, `TeddyBearToy`, and `DinosaurToy` implement the `speak` method in their own way.
- When we create instances of these subclasses and call the `speak` method, each toy speaks in its own unique way.
In conclusion, abstraction in Python simplifies complex systems by increasing reusability.
You've seen how you can reuse a single method from an abstract class across multiple subclasses while forcing each subclass to provide its specific behavior.
This approach keeps your code organized, flexible, and easier to maintain, especially as your application grows.
# --questions--
## --text--
What is the primary goal of abstraction in object-oriented programming?
## --answers--
To expose all internal implementation details to users.
### --feedback--
Think about how abstraction simplifies interaction with complex systems.
---
To merge multiple classes into one.
### --feedback--
Think about how abstraction simplifies interaction with complex systems.
---
To prevent methods from being overridden in child classes.
### --feedback--
Think about how abstraction simplifies interaction with complex systems.
---
To hide complex logic and only show essential features
### --feedback--
Think about how abstraction simplifies interaction with complex systems.
## --video-solution--
4
## --text--
How does Python implement abstraction through its `ABC` module?
## --answers--
By allowing direct instantiation of abstract classes.
### --feedback--
The ABC module enforces method implementation in child classes.
---
By using decorators and inheritance to define abstract methods that subclasses must implement.
### --feedback--
The ABC module enforces method implementation in child classes.
---
By automatically hiding all methods with double underscores.
### --feedback--
The ABC module enforces method implementation in child classes.
---
By converting classes to functions.
### --feedback--
The ABC module enforces method implementation in child classes.
## --video-solution--
2
## --text--
In the car analogy for abstraction, what represents the simplified interface and the complex system?
## --answers--
The interface is the engine pistons, and the complex system is the steering wheel.
### --feedback--
Think about how the interface is what you directly interact with, while the complex system is how it works.
---
The Interface is the steering wheel, brakes, and accelerator, and the complex system is the engine, transmission, and braking physics.
### --feedback--
Think about how the interface is what you directly interact with, while the complex system is how it works.
---
The interface is the car manual, and the complex system is the dashboard controls.
### --feedback--
Think about how the interface is what you directly interact with, while the complex system is how it works.
---
The interface is the fuel type, and the complex system is the tyres.
### --feedback--
Think about how the interface is what you directly interact with, while the complex system is how it works.
## --video-solution--
2

View File

@@ -0,0 +1,236 @@
---
id: 68ca71c3f0ee7ece4ea1e286
title: What is Name Mangling and How Does it Work?
challengeType: 19
dashedName: what-is-name-mangling-and-how-does-it-work
---
# --description--
In a previous lecture, you learned about prefixing attributes with a single underscore and a double underscore.
To remind you of the difference between them, a single underscore is a convention that means the attribute is meant for internal use in the class and should not be directly accessed from outside the class. Double underscore, on the other hand, prevents that attribute from being accessed directly from outside the class.
Here's an example that demonstrates how the two work:
```py
class Example:
def __init__(self):
self._internal = 'I can be accessed from outside the class, but should not'
self.__private = 'You cannot access me directly from outside the class'
obj = Example()
print(obj._internal) # I can be accessed from outside the class, but should not
print(obj.__private) # AttributeError: 'Example' object has no attribute '__private'
```
Prefixing an attribute with a double underscore triggers Python's name mangling process, in which Python internally renames the attribute by adding an underscore and the class name as a prefix, turning `__attribute` into `_ClassName__attribute`.
To see this in action, you create an instance of the class and use the `__dict__` special attribute of that instance, which is a dictionary containing the object's attributes:
```py
class Example:
def __init__(self, internal, private):
self._internal = internal
self.__private = private
example1 = Example(
'I can be accessed from outside the class, but should not',
'I cannot be accessed directly from outside the class'
)
print(example1.__dict__)
```
The result would be:
```py
{
'_internal': 'I can be accessed from outside the class, but should not',
'_Example__private': 'I cannot be accessed directly from outside the class'
}
```
As you can see, the `__private` attribute is stored as `_Example__private`. This means you can still access that attribute outside the class this way:
```py
class Example:
def __init__(self, internal, private):
self._internal = internal
self.__private = private
example1 = Example(
'I can be accessed from outside the class, but should not',
'I cannot be accessed directly from outside the class'
)
example2 = Example(
'I should not be accessed from outside the class',
'But I can be accessed from outside the class with name mangling'
)
print(example1._Example__private) # I cannot be accessed directly from outside the class
print(example2._Example__private) # But I can be accessed from outside the class with name mangling
```
So, why does Python do name mangling?
The main purpose of name mangling is to prevent accidental attribute and method overriding when you use inheritance. Here's an example that makes that clear:
```py
class Parent:
def __init__(self):
self.__data = 'Parent data'
class Child(Parent):
def __init__(self):
super().__init__()
self.__data = 'Child data'
c = Child()
print(c.__dict__) # {'_Parent__data': 'Parent data', '_Child__data': 'Child data'}
```
You can see that both the `Parent` class and the `Child` that inherits from it have their separate `_class__data` attributes. This is made possible with name mangling. Otherwise, the `Child` would have overwritten the Parent data by accident.
Here's what would have happened without allowing Python to do the name mangling, that is if you don't prefix the attributes in both classes with double underscore:
```py
class Parent:
def __init__(self):
self.data = 'Parent data'
class Child(Parent):
def __init__(self):
super().__init__()
self.data = 'Child data'
c = Child()
print(c.__dict__) # {'data': 'Child data'}
```
So, which should you use to prefix attributes between single underscore (`_`) and double underscore (`__`)? It depends. If an attribute is only meant for internal use within the class, stick with a single underscore.
But if you're working with a class that will be inherited, you should use a double underscore so the attribute from the parent doesn't get overridden.
# --questions--
## --text--
What is the difference between a single underscore and a double underscore?
## --answers--
A single underscore and a double underscore are treated the same way by Python.
### --feedback--
Focus on how Python handles attributes that start with `_` vs `__`.
---
A single underscore makes attributes completely private, while a double underscore makes them protected.
### --feedback--
Focus on how Python handles attributes that start with `_` vs `__`.
---
A single underscore prevents direct access, while a double underscore allows direct access.
### --feedback--
Focus on how Python handles attributes that start with `_` vs `__`.
---
A single underscore is just a convention, while a double underscore triggers name mangling.
### --feedback--
Focus on how Python handles attributes that start with `_` vs `__`.
## --video-solution--
4
## --text--
What is name mangling?
## --answers--
A process in which Python converts all attributes into methods for easier access.
### --feedback--
Think about how Python internally renames double underscore attributes.
---
A process in which Python deletes attributes with a single underscore to keep them hidden.
### --feedback--
Think about how Python internally renames double underscore attributes.
---
A process in which Python changes `__attribute` into `_ClassName__attribute` to avoid accidental overriding in subclasses.
### --feedback--
Think about how Python internally renames double underscore attributes.
---
A process in which Python encrypts private data to make it inaccessible from outside the class.
### --feedback--
Think about how Python internally renames double underscore attributes.
## --video-solution--
3
## --text--
What happens when you don't prefix attributes in a parent and child classes with a double underscore?
## --answers--
Both classes keep their own separate copies of the attribute without interfering with each other.
### --feedback--
Think about what `print(c.__dict__)` shows when both classes use the same attribute name without `__`.
---
The child class completely overrides the parent class attribute, and the parent's data is lost.
### --feedback--
Think about what `print(c.__dict__)` shows when both classes use the same attribute name without `__`.
---
The parent class attributes become read-only and cannot be changed by the child class.
### --feedback--
Think about what `print(c.__dict__)` shows when both classes use the same attribute name without `__`.
---
Python raises an error because attributes must always be prefixed with a double underscore.
### --feedback--
Think about what `print(c.__dict__)` shows when both classes use the same attribute name without `__`.
## --video-solution--
2

View File

@@ -4,6 +4,31 @@
"dashedName": "lecture-understanding-object-oriented-programming-and-encapsulation",
"blockType": "lecture",
"blockLayout": "challenge-list",
"challengeOrder": [{ "id": "68420be9af9d89620af7944a", "title": "Step 1" }],
"challengeOrder": [
{
"id": "68420be9af9d89620af7944a",
"title": "What is Object-Oriented Programming, and How Does Encapsulation Work?"
},
{
"id": "68c128cbd77e4ba9ed671937",
"title": "What are Getters and Setters"
},
{
"id": "68c3ba8940e4df8afa83a723",
"title": "What Is Inheritance and How Does It Promote Code Reuse?"
},
{
"id": "68c3bc4ddb7b469fb7d17c28",
"title": "What Is Polymorphism and How Does It Promote Code Reuse?"
},
{
"id": "68ca71c3f0ee7ece4ea1e286",
"title": "What is Name Mangling and How Does it Work"
},
{
"id": "68c3bc80f67363a31791fe1c",
"title": "What Is Abstraction and How Does It Help Keep Complex Systems Organized?"
}
],
"helpCategory": "Python"
}