""" Now that we know the basics of classes, variables, and functions, we will move on to more abstract ideas. I'm also going to introduce type hinting; this reduces ambiguity and improves readability. Key Concepts: **Encapsulation** is the concept of grouping related data and methods within a class and restricting access to them to control how they are used. **Access Modifiers** are used to determine who/what can access a certain attribute or method. - **Public**: Accessible from any part of the code. Used for attributes and methods intended to be part of the class's interface for external use. - **Protected**: Intended for internal use within the class and its subclasses. "_value" represents it. Useful for attributes/methods that should be accessible in subclasses but not publicly. - **Private**: Only accessible within the class, represented by "__value". Useful for securing sensitive data or internal methods that should not be altered externally. IMPORTANT: Python does not strictly enforce access control, so these conventions mainly serve as guidelines. """ # This is the abstraction library in Python from abc import ABC, abstractmethod # Type hinting, not needed but reduces ambiguity. Note: not enforced at runtime. from typing import Optional # An abstract class, provides a blueprint for other classes to inherit. class Shape(ABC): # This method is required in all subclasses, ensuring they provide their own area calculation. @abstractmethod def area(self) -> float: """Implemented to ensure each subclass contains this method.""" class Rectangle(Shape): def __init__(self, width: float, height: float): # A private attribute to store width. self.__width = width # A private attribute to store height. self.__height = height # This allows access to the private 'width' attribute in a controlled way. @property def width(self) -> float: return self.__width # This allows you to set the value of 'width' after initialization. @width.setter def width(self, value: float): if value <= 0: raise ValueError("Width must be positive.") self.__width = value # This allows access to the private 'height' attribute in a controlled way. @property def height(self) -> float: return self.__height # This allows you to set the value of 'height' after initialization. @height.setter def height(self, value: float): if value <= 0: raise ValueError("Height must be positive.") self.__height = value def area(self) -> float: # Calculates the area of the rectangle. return self.__width * self.__height def set_dimensions(self, width: Optional[float] = None, height: Optional[float] = None): if width is not None: # Sets 'width' using the setter. self.width = width if height is not None: # Sets 'height' using the setter. self.height = height rect = Rectangle(5, 10) # Prints the area of the rectangle (5 * 10 = 50). print(rect.area()) # Updates 'width' using the setter, height remains the same. rect.set_dimensions(width=8) # Prints the new area of the rectangle (8 * 10 = 80). print(rect.area())