0% found this document useful (0 votes)
12 views44 pages

Unit III Python

This document covers key concepts in Python programming, including standalone programs, command-line arguments, modules, and classes. It explains how to create and run standalone programs, handle command-line arguments using sys and argparse, and utilize modules for code reusability. Additionally, it discusses class definition, inheritance, and the benefits of using classes in Python.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views44 pages

Unit III Python

This document covers key concepts in Python programming, including standalone programs, command-line arguments, modules, and classes. It explains how to create and run standalone programs, handle command-line arguments using sys and argparse, and utilize modules for code reusability. Additionally, it discusses class definition, inheritance, and the benefits of using classes in Python.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

UNIT - III

Modules, Packages, and Programs: Standalone Programs – Command-Line Arguments –


Modules and the import Statement – The Python Standard Library. Objects and Classes:
Define a Class with class – Inheritance – Override a Method – Add a Method – Get Help
from Parent with super–InselfDefense –Get and SetAttributeValueswithProperties –
NameMangling for Privacy – Method Types – Duck Typing – Special Methods –
Composition

STANDALONE PROGRAMS

Standalone Programs in Python

A standalone program in Python is a self-contained script that can be executed


independently. It is typically designed to perform a specific task without needing to be imported
as a module. Standalone programs are written in such a way that they can run directly from the
command line or a Python interpreter.

Key Characteristics of Standalone Programs:

1. Self-contained: They include all the logic and components necessary for execution.
2. Executable directly: They can be run using python script_name.py.
3. Conditional execution: They often use the if __name__ == "__main__": block to ensure
the program's main functionality executes only when run directly.

How if __name__ == "__main__": Works

In Python:

 When a script is executed directly, Python sets the special variable __name__ to
"__main__".
 If the script is imported as a module in another script, __name__ is set to the script's
name instead.

This makes it possible to differentiate between executing a script directly and importing it.

Structure of a Standalone Program

Here’s a typical structure for a standalone program:

# Import necessary libraries


import sys

1
# Define functions
def greet_user(name):
return f"Hello, {name}!"

# Main function
def main():
if len([Link]) > 1:
name = [Link][1]
print(greet_user(name))
else:
print("Usage: python script_name.py <name>")

# Conditional execution
if __name__ == "__main__":
main()

Example: A Standalone Calculator Program

Here’s a simple calculator script:

def add(a, b):


return a + b

def subtract(a, b):


return a - b

def multiply(a, b):


return a * b

def divide(a, b):


return a / b if b != 0 else "Division by zero is not allowed"

def main():
print("Simple Calculator")
print("1. Add")
print("2. Subtract")
print("3. Multiply")
print("4. Divide")

choice = input("Enter choice (1/2/3/4): ")

2
if choice in ('1', '2', '3', '4'):
num1 = float(input("Enter first number: "))
num2 = float(input("Enter second number: "))

if choice == '1':
print(f"Result: {add(num1, num2)}")
elif choice == '2':
print(f"Result: {subtract(num1, num2)}")
elif choice == '3':
print(f"Result: {multiply(num1, num2)}")
elif choice == '4':
print(f"Result: {divide(num1, num2)}")
else:
print("Invalid input")

if __name__ == "__main__":
main()

Steps to Run a Standalone Program

1. Save the script: Save the program in a .py file, e.g., [Link].
2. Execute the script:
o Open a terminal or command prompt.
o Navigate to the directory containing the script.
o Run the script with python [Link].

Advantages of Standalone Programs

1. Modularity: Code can be reused by importing functions from standalone scripts.


2. Portability: They can be easily distributed and run on any system with Python installed.
3. Easy Testing: The if __name__ == "__main__": construct allows easy separation of
main program execution and testing.

COMMAND LINE ARGUMENTS IN PYTHON

Command-Line Arguments in Python

Command-line arguments allow users to pass information to a Python script when it is


executed from the command line. These arguments are often used to control the script's behavior
without requiring code modifications.

3
Accessing Command-Line Arguments in Python

Python provides the [Link] list from the sys module to handle command-line arguments. Here's
how it works:

1. [Link]:
o A list containing the command-line arguments passed to the script.
o The first element, [Link][0], is the name of the script itself.
o Subsequent elements ([Link][1], [Link][2], etc.) are the additional arguments
passed.

Example of [Link]

Script: [Link]
import sys

def main():
print(f"Script name: {[Link][0]}")
if len([Link]) > 1:
print("Arguments passed:")
for i, arg in enumerate([Link][1:], start=1):
print(f"Argument {i}: {arg}")
else:
print("No arguments passed.")

if __name__ == "__main__":
main()

Running the Script:


$ python [Link] arg1 arg2 arg3
Output:
Script name: [Link]
Arguments passed:
Argument 1: arg1
Argument 2: arg2
Argument 3: arg3

Handling Command-Line Arguments with argparse

The argparse module offers a more robust way to handle arguments. It supports features like:

 Specifying required and optional arguments.


 Automatic help messages.

4
 Type-checking.

Example with argparse


python
Copy code
import argparse

def main():
parser = [Link](description="A simple script to demonstrate argparse.")
parser.add_argument("name", type=str, help="Your name")
parser.add_argument("--age", type=int, help="Your age (optional)", required=False)

args = parser.parse_args()

print(f"Hello, {[Link]}!")
if [Link]:
print(f"You are {[Link]} years old.")

if __name__ == "__main__":
main()

Running the Script:


$ python [Link] John --age 30
Output:
Hello, John!
You are 30 years old.

Common Use Cases

1. File processing: Pass file names as arguments to process them dynamically.

$ python [Link] [Link] [Link]

2. Dynamic behavior: Enable or disable features based on arguments.

$ python [Link] --verbose

3. Configuration: Provide configuration settings without modifying the code.

$ python [Link] --config [Link]

5
MODULES AND THE IMPORT STATEMENT

Modules in Python are files containing Python code (functions, classes, or variables) that
can be reused across different programs. Modules help organize code logically, improve
reusability, and enhance maintainability.

What is a Module?

 A module is simply a Python file with a .py extension that can define functions, classes,
and variables.
 Modules can also include runnable code, but their primary purpose is to make reusable
components available to other programs.

Example of a Module: my_module.py


# my_module.py
def greet(name):
return f"Hello, {name}!"

PI = 3.14159

Importing a Module

You can use the import statement to include a module in your script and access its components.

Syntax:
import module_name

Example:
import my_module

# Using the module's functions and variables


print(my_module.greet("Alice"))
print(f"Value of PI: {my_module.PI}")

Using Specific Components of a Module

You can import specific functions, classes, or variables from a module using the from ... import
... syntax.

Example:
from my_module import greet, PI

print(greet("Bob"))

6
print(f"Value of PI: {PI}")

Using Aliases for Modules

You can assign an alias to a module or its components using the as keyword for brevity or
clarity.

Example:
import my_module as mm

print([Link]("Charlie"))
print(f"Value of PI: {[Link]}")

Types of Modules

1. Built-in Modules:
o Python provides many pre-installed modules like math, os, sys, etc.
o Example:

import math

print([Link](16)) # Outputs: 4.0

2. Standard Library Modules:


o Python's standard library includes modules like datetime, random, and json.
o Example:

import random

print([Link](1, 10)) # Outputs a random number between 1 and 10

3. Third-Party Modules:
o Installed via package managers like pip (e.g., numpy, pandas).
o Example:

import numpy as np

arr = [Link]([1, 2, 3])


print(arr)

4. Custom Modules:
o User-created modules like my_module.

7
The __name__ Variable and Module Execution

Modules have a special variable called __name__. When a module is run directly, __name__ is
set to "__main__". If the module is imported, __name__ is set to the module's name.

Example:
# my_module.py
def greet(name):
return f"Hello, {name}!"

if __name__ == "__main__":
print("Running my_module directly.")
print(greet("Direct User"))

Behavior:

1. When executed directly:

$ python my_module.py

Output:

Running my_module directly.


Hello, Direct User

2. When imported:

import my_module

print(my_module.greet("Imported User"))

Output:

Hello, Imported User

Importing All Components

You can import all components of a module using from module_name import *.

Example:
from my_module import *

print(greet("Dave"))

8
print(PI)

Managing Module Search Paths

Python searches for modules in the following locations:

1. The directory of the script being run.


2. Directories listed in the PYTHONPATH environment variable.
3. Standard library and installed packages.

You can view the module search paths with:

import sys
print([Link])

Advantages of Using Modules

1. Code Reusability: Share functions and variables across multiple scripts.


2. Better Organization: Modularize code into separate files for clarity.
3. Easier Maintenance: Fix issues or update features in a single module without affecting
the entire codebase.

The Python Standard Library

The Python Standard Library is a collection of pre-installed modules and packages that
provide standardized solutions to common programming tasks. These modules allow developers
to perform tasks like file handling, string manipulation, mathematical computations, data
serialization, networking, and more without installing additional packages.

Characteristics of the Python Standard Library

1. Comprehensive: Offers modules for various domains like mathematics, file I/O, system
operations, and more.
2. Cross-Platform: Works seamlessly across operating systems (e.g., Windows, macOS,
Linux).
3. Pre-installed: Available with every Python distribution.
4. Time-Saving: Reduces the need to write custom code for common tasks.

Categories of Standard Library Modules

Here are some popular categories and examples:

9
1. String and Text Handling

Modules: re, string, textwrap

Example:

import re

text = "Python is amazing!"


pattern = r"\b\w+\b"
words = [Link](pattern, text)
print(words) # Output: ['Python', 'is', 'amazing']

2. Mathematics and Numbers

Modules: math, cmath, random, statistics

Example:

import math

print([Link](16)) # Output: 4.0


print([Link]) # Output: 3.141592653589793

3. File and Directory Access

Modules: os, [Link], shutil, pathlib

Example:

import os

print([Link]()) # Prints the current working directory

4. Data Serialization and Persistence

Modules: json, pickle, shelve

Example:

import json

data = {"name": "Alice", "age": 25}


json_string = [Link](data)
print(json_string) # Output: '{"name": "Alice", "age": 25}'

10
5. Date and Time

 Modules: datetime, time, calendar


 Example:

from datetime import datetime

now = [Link]()
print([Link]("%Y-%m-%d %H:%M:%S")) # Outputs current date and time

6. Networking and Internet

Modules: socket, http, urllib, email

Example:

python
Copy code
import [Link]

response = [Link]('[Link]
print([Link]) # Output: 200

7. System and OS Interaction

Modules: sys, subprocess, platform

Example:

import sys

print([Link]) # Outputs the Python version

8. Concurrent Programming

Modules: threading, multiprocessing, asyncio

Example:

import threading

def print_numbers():
for i in range(5):
print(i)

11
thread = [Link](target=print_numbers)
[Link]()

9. Testing and Debugging

Modules: unittest, pdb, doctest

Example:

python
Copy code
import unittest

class TestSum([Link]):
def test_addition(self):
[Link](1 + 1, 2)

[Link]()

10. Data Compression and Archiving

Modules: zipfile, tarfile, gzip

Example:

import zipfile

with [Link]('[Link]', 'w') as zipf:


[Link]('[Link]')

Viewing Available Standard Library Modules

You can view a list of available modules in your Python installation by running:

help('modules')

Advantages of the Python Standard Library

1. Convenience: Ready-to-use solutions save development time.


2. Reliability: Officially maintained by Python, ensuring stability.
3. Portability: Works across platforms, reducing compatibility concerns.

12
Popular Modules at a Glance

Module Purpose
os Interact with the operating system.

sys Access system-specific parameters.

math Perform mathematical operations.

random Generate random numbers.

datetime Handle date and time operations.

json Work with JSON data.

re Perform regular expressions.

threading Enable multi-threading.

urllib Handle URLs and network requests.

sqlite3 Work with SQLite databases.

DEFINE A CLASS WITH CLASS

In Python, defining a class is done using the class keyword. A class is a blueprint for
creating objects, and it can include attributes (data) and methods (functions) that operate on that
data.

Defining a class in Python with a simple example.

1. Define the Class: Use the class keyword followed by the class name and a colon (:).
2. Initialize with __init__ Method: The __init__ method is a special method called the
constructor. It initializes the object's attributes when an instance of the class is created.
3. Create Instance Variables: These are variables specific to each object created from the
class.
4. Define Methods: Methods are functions that belong to a class. They can operate on the
data (attributes) within the class.

Example: Creating a Car Class

Let’s create a simple Car class with attributes like make and year, and methods to display
information and update the year.

# Define the Car class


class Car:
# Constructor to initialize the object

13
def __init__(self, make, year):
[Link] = make # Instance variable for car make
[Link] = year # Instance variable for car year

# Method to display car information


def display_info(self):
print(f"Car Make: {[Link]}, Year: {[Link]}")

# Method to update the year of the car


def update_year(self, new_year):
[Link] = new_year

# Create an instance (object) of the Car class


car1 = Car("Toyota", 2015)

# Access and display car information


car1.display_info() # Output: Car Make: Toyota, Year: 2015

# Update the year of the car


car1.update_year(2020)

# Display updated car information


car1.display_info() # Output: Car Make: Toyota, Year: 2020

OUTPUT :

Car Details: 2015 Toyota Camry The car is 8 years old.

Benefits of Using Classes

 Encapsulation: Classes help to bundle data and methods together, making it easier to
organize code.
 Reusability: Once a class is defined, you can create multiple objects (instances) from it.
 Modularity: Methods within a class can be easily modified without affecting other parts
of the code.

This is a basic example, but classes in Python can be much more complex and can include
features like inheritance, polymorphism, and encapsulation, allowing for advanced and organized
programming.

INHERITANCE IN PYTHON

In Python, inheritance is a feature that allows a class (called the child class or subclass) to
inherit attributes and methods from another class (called the parent class or superclass). This
helps in reusing code and establishing relationships between classes, like an "is-a" relationship.

14
Types of Inheritance in Python

1. Single Inheritance: A child class inherits from a single parent class.


2. Multiple Inheritance: A child class inherits from multiple parent classes.
3. Multilevel Inheritance: A class inherits from a child class, creating a chain of
inheritance.
4. Hierarchical Inheritance: Multiple child classes inherit from the same parent class.
5. Hybrid Inheritance: A combination of two or more types of inheritance.

1. Single Inheritance

In single inheritance, a child class inherits from a single parent class.

class Animal:
def speak(self):
print("Animal speaks")

class Dog(Animal): # Dog class inherits from Animal


def bark(self):
print("Dog barks")

# Creating an object of Dog class


dog = Dog()
[Link]() # Inherited from Animal
[Link]() # Defined in Dog

Output:

Animal speaks
Dog barks

2. Multiple Inheritance

In multiple inheritance, a child class inherits from more than one parent class.

class Father:
def show_father(self):
print("This is the Father class")

class Mother:
def show_mother(self):
print("This is the Mother class")

class Child(Father, Mother): # Child class inherits from both Father and Mother
def show_child(self):
print("This is the Child class")

15
# Creating an object of Child class
child = Child()
child.show_father()
child.show_mother()
child.show_child()

Output:

This is the Father class


This is the Mother class
This is the Child class

3. Multilevel Inheritance

In multilevel inheritance, a class inherits from another child class, forming a chain.

class Animal:
def eat(self):
print("Animal eats")

class Mammal(Animal): # Mammal inherits from Animal


def walk(self):
print("Mammal walks")

class Dog(Mammal): # Dog inherits from Mammal


def bark(self):
print("Dog barks")

# Creating an object of Dog class


dog = Dog()
[Link]() # Inherited from Animal
[Link]() # Inherited from Mammal
[Link]() # Defined in Dog

Output:

Animal eats
Mammal walks
Dog barks

4. Hierarchical Inheritance

In hierarchical inheritance, multiple child classes inherit from the same parent class.

16
class Animal:
def sound(self):
print("Animal makes a sound")

class Dog(Animal): # Dog inherits from Animal


def bark(self):
print("Dog barks")

class Cat(Animal): # Cat also inherits from Animal


def meow(self):
print("Cat meows")

# Creating objects of Dog and Cat classes


dog = Dog()
cat = Cat()

[Link]() # Inherited from Animal


[Link]() # Defined in Dog

[Link]() # Inherited from Animal


[Link]() # Defined in Cat

Output:

Animal makes a sound


Dog barks
Animal makes a sound
Cat meows

5. Hybrid Inheritance

Hybrid inheritance is a combination of two or more types of inheritance. Here’s a basic example
that combines multiple and multilevel inheritance.

class Animal:
def eat(self):
print("Animal eats")

class Mammal(Animal): # Inherits from Animal


def walk(self):
print("Mammal walks")

class Bird(Animal): # Inherits from Animal


def fly(self):

17
print("Bird flies")

class Bat(Mammal, Bird): # Bat inherits from both Mammal and Bird
def hang(self):
print("Bat hangs upside down")

# Creating an object of Bat class


bat = Bat()
[Link]() # Inherited from Animal
[Link]() # Inherited from Mammal
[Link]() # Inherited from Bird
[Link]() # Defined in Bat

Output:

Animal eats
Mammal walks
Bird flies
Bat hangs upside down

Summary of Inheritance Types in Python

1. Single Inheritance: One child, one parent.


2. Multiple Inheritance: One child, multiple parents.
3. Multilevel Inheritance: Inheritance chain with more than two classes.
4. Hierarchical Inheritance: Multiple children from one parent.
5. Hybrid Inheritance: Combination of the above types.

METHOD OVERRIDING

Method overriding in Python is a concept where a method in a child class has the same
name as a method in the parent class. The method in the child class "overrides" the method in the
parent class. This allows the child class to provide a specific implementation for that method,
which will be used when called on an object of the child class.

Example Code

class Animal:
# Parent class method
def sound(self):
print("Animals make different sounds")

class Dog(Animal):

18
# Overriding the sound method in the Dog class
def sound(self):
print("Dog barks")

# Creating objects of both classes


animal = Animal()
dog = Dog()

# Calling the sound method on both objects


[Link]() # Calls the method in Animal class
[Link]() # Calls the overridden method in Dog class

Explanation

1. Parent Class (Animal):


o The Animal class has a method sound() that prints "Animals make different
sounds".
2. Child Class (Dog):
o The Dog class inherits from Animal and overrides the sound() method. Instead of
using the parent class's implementation, Dog provides its own version of sound()
which prints "Dog barks".
3. Method Call:
o When we call [Link](), it uses the sound() method in Animal and outputs
"Animals make different sounds".
o When we call [Link](), it uses the overridden sound() method in Dog,
outputting "Dog barks".

Expected Output

Animals make different sounds


Dog barks

Summary

 Method overriding allows a child class to provide a specific implementation of a method


that is already defined in its parent class.
 When the method is called on an object of the child class, the overridden version in the
child class is executed instead of the parent's method.

This is useful in cases where subclasses need to behave differently from their parent classes
while sharing the same method names.

ADD A METHOD IN PYTHON

19
In Python, adding a method to a class allows you to define a specific action or behavior
that objects of that class can perform. A method is essentially a function defined within a class,
with self as its first parameter, which refers to the instance of the class.

Adding a Method Example

Let's define a Car class with a method to display information about the car. Then, we’ll add
another method to start the car.

class Car:
# Constructor to initialize the Car object with make and model
def __init__(self, make, model):
[Link] = make
[Link] = model

# Method to display car details


def display_info(self):
print(f"Car: {[Link]} {[Link]}")

# Adding a new method to start the car


def start(self):
print(f"The {[Link]} {[Link]} is now starting...")

# Creating an object of the Car class


my_car = Car("Toyota", "Corolla")

# Calling the display_info method


my_car.display_info() # Output: Car: Toyota Corolla

# Calling the start method


my_car.start() # Output: The Toyota Corolla is now starting...

Explanation

1. Constructor (__init__):
o The __init__ method initializes the make and model attributes when an object of
the Car class is created.
2. display_info Method:
o This method prints out the details of the car (make and model).
o display_info is called by using the my_car.display_info() syntax on the my_car
object.
3. start Method (Newly Added):
o We added a new method, start, which prints a message indicating that the car is
starting.
o This method can also be called using the my_car.start() syntax.

20
Expected Output

Car: Toyota Corolla


The Toyota Corolla is now starting...

Summary

 Adding a Method: Adding a method involves defining a new function inside the class,
with self as the first parameter.
 Accessing Methods: Once the method is added, it can be called using the syntax
object_name.method_name().

Adding methods like this allows you to define actions or behaviors that objects of the class
can perform, making the class more functional and versatile.

GET HELP FROM PARENT WITH SUPER

In Python, the super() function is used to call methods from a parent (or superclass) in a
child (or subclass). This is especially useful when you want to add functionality in the child class
while still using some logic or behavior from the parent class.

Using super() helps in maintaining code reusability and prevents duplicate code.

Example: Using super() to Call the Parent's Constructor

In this example, the Animal class has a constructor that initializes the name attribute, and
the Dog class extends it by adding the breed attribute. The super() function allows us to call the
Animal constructor from the Dog constructor.

class Animal:
# Constructor for Animal class
def __init__(self, name):
[Link] = name

# Method to display the animal's name


def display_name(self):
print(f"Animal's name is: {[Link]}")

class Dog(Animal):
# Constructor for Dog class
def __init__(self, name, breed):
# Using super() to call the parent (Animal) class's constructor
super().__init__(name)
[Link] = breed

# Method to display the dog's breed

21
def display_breed(self):
print(f"{[Link]} is a {[Link]}")

# Creating an object of the Dog class


dog = Dog("Buddy", "Golden Retriever")

# Calling methods to display information


dog.display_name() # Output: Animal's name is: Buddy
dog.display_breed() # Output: Buddy is a Golden Retriever

Explanation

1. Parent Class (Animal):


o The Animal class has an __init__ constructor that initializes the name attribute.
o The display_name method prints the name of the animal.
2. Child Class (Dog):
o The Dog class inherits from Animal.
o In its __init__ constructor, super().__init__(name) is used to call the Animal
class’s constructor, initializing the name attribute.
o The breed attribute is then initialized specifically for Dog.
o The display_breed method prints the breed of the dog.
3. Using super():
o super().__init__(name) calls the __init__ method of the Animal class, which
allows Dog to inherit the functionality of setting name without duplicating code.

Expected Output

Animal's name is: Buddy


Buddy is a Golden Retriever

Summary

 super(): This function is used to access methods from the parent class.
 Code Reusability: By using super(), the child class can reuse methods from the parent
class without duplicating code.
 Constructor Chaining: super().__init__(...) allows the child class to initialize attributes
from the parent class.

This approach is helpful in cases where you want to extend the functionality of a method but also
want to retain some functionality from the parent class.

GETTER AND SETTER METHODS

In Python, getter and setter methods are used to retrieve and modify the values of an
object’s attributes, especially if we want to add some control or validation on attribute access.
These methods help us control access to private attributes and manage data safely.

22
What Are Getter and Setter Methods?

1. Getter Method:
o A method used to retrieve the value of an attribute.
o It is typically used to access a private attribute.
2. Setter Method:
o A method used to set or modify the value of an attribute.
o It can include validation to ensure the attribute is set to a valid value.

Why Use Getter and Setter Methods?

Using getters and setters allows you to:

 Control access to attributes (e.g., restrict direct access to private attributes).


 Validate attribute values (e.g., ensure only valid values are assigned).
 Encapsulate functionality, maintaining a clean interface.

Example: Using Getter and Setter Methods in Python

Let’s create a Person class with an attribute age. We’ll use a getter to retrieve the age and
a setter to validate that age is not negative.

class Person:
def __init__(self, name, age):
[Link] = name
self._age = age # Private attribute for age

# Getter for age


def get_age(self):
return self._age

# Setter for age


def set_age(self, value):
if value < 0:
raise ValueError("Age cannot be negative.")
self._age = value

def display(self):
print(f"{[Link]} is {self.get_age()} years old.")

# Using the class


person1 = Person("Alice", 30)
[Link]() # Output: Alice is 30 years old.

# Accessing age using the getter


print(person1.get_age()) # Output: 30

23
# Modifying age using the setter
person1.set_age(35)
print(person1.get_age()) # Output: 35

# Trying to set a negative age (will raise an error)


try:
person1.set_age(-5)
except ValueError as e:
print(e) # Output: Age cannot be negative.

Explanation of the Code

1. Private Attribute: _age is used as a private attribute (indicated by the underscore).


2. Getter Method (get_age):
o This method returns the current value of _age.
o It allows controlled access to the _age attribute.
3. Setter Method (set_age):
o This method accepts a parameter value and checks if it’s non-negative.
o If value is negative, it raises a ValueError.
o Otherwise, it updates _age with the new value.
4. Using the Class:
o We create an instance of Person, then use get_age to access age and set_age to
modify it.
o Attempting to set a negative age triggers a ValueError.

Output

lice is 30 years old.


30
35
Age cannot be negative.

Summary

Using getter and setter methods:

 Getter (get_age) retrieves the value of _age.


 Setter (set_age) allows setting _age but includes validation to prevent invalid values.

NAME MANGLING FOR PRIVACY

In Python, name mangling is a technique used to make an attribute private and avoid
accidental or unintended access or modification. It is mainly used to prevent subclass overrides.

24
Name mangling is achieved by prefixing an attribute name with two underscores (__). This
makes it harder to access the attribute from outside the class directly, but it’s still accessible
through a specific name-mangled format.

How Name Mangling Works

When an attribute is prefixed with __, Python changes its name internally to include the
class name as a prefix. This modified name prevents accidental access and overrides, but it’s still
accessible if needed using a special syntax.

For example, if you have a class MyClass with a private attribute __data, Python
internally changes the attribute name to _MyClass__data. This is done to avoid conflicts in
subclasses and to signal that it’s a private attribute.

Why Use Name Mangling?

1. Prevent Accidental Access: It helps prevent accidental access to or modification of


private data from outside the class.
2. Avoid Naming Conflicts in Subclasses: It prevents subclasses from unintentionally
overriding private attributes of the parent class.

Example: Name Mangling in Action

Let’s look at an example where we define a class with a private attribute and demonstrate
how name mangling affects access.

class MyClass:
def __init__(self, value):
self.__data = value # Private attribute with name mangling

def get_data(self):
return self.__data

# Create an instance of MyClass


obj = MyClass(10)

# Trying to access the private attribute directly


try:
print(obj.__data) # This will raise an AttributeError
except AttributeError as e:
print(e) # Output: 'MyClass' object has no attribute '__data'

# Accessing the private attribute using the name-mangled version


print(obj._MyClass__data) # Output: 10

Explanation of the Code

25
1. Private Attribute: __data is a private attribute in MyClass. The double underscore
before data triggers name mangling.
2. Accessing the Attribute Directly:
o print(obj.__data) raises an AttributeError because __data has been renamed
internally to _MyClass__data.
3. Accessing with Name Mangling:
o To access the attribute directly, we can use obj._MyClass__data. This accesses
the actual underlying name of the attribute, which is _MyClass__data.

Important Points About Name Mangling

1. Name Mangling is not True Privacy:


o It’s a convention to signal that the attribute should be treated as private. However,
since it’s still accessible using _ClassName__attribute, it’s not true encapsulation
like in some other languages (e.g., private keywords in Java or C++).
2. Use in Inheritance:
o Name mangling is particularly useful in inheritance to avoid name conflicts in
subclasses.
3. Single Underscore (_):
o Using a single underscore before an attribute (e.g., _data) is a convention that
indicates the attribute is intended for internal use but does not invoke name
mangling. The double underscore is what triggers name mangling.

Example with Inheritance

Name mangling helps avoid conflicts in subclass attributes. Here’s an example:

class Parent:
def __init__(self):
self.__value = 42 # Private attribute in the parent class

class Child(Parent):
def __init__(self):
super().__init__()
self.__value = 99 # Attempt to define a similar private attribute in the child class

# Create an instance of Child


child = Child()

# Accessing both attributes using name mangling


print(child._Parent__value) # Output: 42 (Parent's attribute)
print(child._Child__value) # Output: 99 (Child's attribute)

Explanation of Inheritance Example

1. Parent Class:

26
o Parent has a private attribute __value, which is name-mangled to _Parent__value.
2. Child Class:
o Child also defines an attribute __value, which is name-mangled to _Child__value.
o This avoids conflict with the parent class attribute since both have been name-
mangled to unique names based on their classes.
3. Accessing Attributes:
o Using child._Parent__value retrieves the __value from the Parent class.
o Using child._Child__value retrieves the __value from the Child class.

Summary

 Name Mangling is a technique where Python renames an attribute prefixed with double
underscores by adding the class name as a prefix.
 Purpose: To signal the attribute is private and avoid name conflicts in subclasses.
 Access: Though it makes direct access less straightforward, the attribute is still accessible
using the name-mangled version (_ClassName__attribute).

GETATTR AND SETATTR WITH PROPERTIES IN PYTHON

In Python, getattr and setattr are built-in functions that allow dynamic access to object
attributes, including properties. Properties in Python are a way to manage access to an attribute,
allowing additional logic to be executed when the attribute is retrieved, set, or deleted.

Using getattr and setattr

1. getattr:
o Retrieves the value of an attribute of an object.
o Syntax: getattr(object, attribute_name[, default])
o If the attribute doesn't exist, a default value can be provided to avoid raising an
AttributeError.
2. setattr:
o Sets the value of an attribute of an object.
o Syntax: setattr(object, attribute_name, value)
o If the attribute doesn't exist, it is created dynamically.

Example: getattr and setattr in Action

class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

# Create an instance
person = Person("Alice", 25)

# Use getattr to retrieve attribute values

27
print(getattr(person, "name")) # Output: Alice
print(getattr(person, "age")) # Output: 25

# Use setattr to modify attributes


setattr(person, "age", 30)
print([Link]) # Output: 30

# Use getattr with a default value


print(getattr(person, "gender", "Not specified")) # Output: Not specified

Using Properties with getattr and setattr

Properties provide controlled access to an object's attributes by defining methods for getting,
setting, or deleting the attribute. They use the @property decorator.

Defining a Property

 Getter: Retrieves the attribute value.


 Setter: Sets the attribute value, often with additional validation.

Example: Properties with getattr and setattr


class Circle:
def __init__(self, radius):
self._radius = radius # Using a private attribute

@property
def radius(self):
return self._radius

@[Link]
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def area(self):
import math
return [Link] * (self._radius ** 2)

# Create an instance
circle = Circle(10)

# Access radius using getattr (property works transparently)


print(getattr(circle, "radius")) # Output: 10

28
# Set radius using setattr (setter is called automatically)
setattr(circle, "radius", 15)
print([Link]) # Output: 15

# Use getattr to access a computed property


print(getattr(circle, "area")) # Output: 706.8583470577034

Key Points

1. Dynamic Access:
o getattr and setattr allow dynamic interaction with attributes, even for those
defined with properties.
2. Validation via Properties:
o Properties ensure that attribute access or updates include logic, such as validation
or computed values.
3. Error Handling:
o Use getattr with a default argument to avoid errors when accessing non-existent
attributes.
o setattr will dynamically create attributes if they don’t exist, which should be used
cautiously.

Advantages of Using getattr and setattr with Properties

 Flexibility: Dynamically retrieve or set attributes based on runtime decisions.


 Encapsulation: Control attribute access while maintaining compatibility with Python’s
object-oriented paradigm.
 Validation: Enforce constraints and execute additional logic during attribute updates.

When to Use Properties with getattr and setattr

 Use getattr when:


o You need to dynamically retrieve an attribute's value based on runtime conditions.
 Use setattr when:
o You need to dynamically set or modify an attribute's value.
 Use properties when:
o Attribute access or modification requires additional logic, such as validation or
computation.

METHOD TYPES IN PYTHON

In Python, methods are functions defined within a class that operate on instances of the
class or the class itself. Methods can be categorized based on their behavior and the type of
access they have to the class or its instances.

Types of Methods in Python

29
1. Instance Methods
2. Class Methods
3. Static Methods

1. Instance Methods

 Definition: Operate on instances of the class and have access to instance attributes and
methods. They are the most common type of method in Python.
 Access: Requires the instance (self) as the first parameter.
 Usage: Used for working with the object’s data.

Example:
class Circle:
def __init__(self, radius):
[Link] = radius

def calculate_area(self): # Instance method


import math
return [Link] * ([Link] ** 2)

# Create an instance
circle = Circle(5)
print(circle.calculate_area()) # Output: 78.53981633974483

2. Class Methods

 Definition: Operate on the class itself rather than instances. These methods can modify
class-level attributes but cannot access instance-specific data directly.
 Access: Requires the class (cls) as the first parameter.
 Declaration: Defined using the @classmethod decorator.
 Usage: Useful for factory methods or modifying class-level data.

Example:
class Circle:
count = 0 # Class-level attribute

def __init__(self, radius):


[Link] = radius
[Link] += 1

@classmethod
def get_count(cls): # Class method
return [Link]

# Create instances

30
circle1 = Circle(5)
circle2 = Circle(10)

print(Circle.get_count()) # Output: 2

3. Static Methods

 Definition: Do not operate on the instance or class directly. They work like regular
functions but are included in the class for logical grouping.
 Access: Do not require self or cls.
 Declaration: Defined using the @staticmethod decorator.
 Usage: Used for utility methods that perform operations independent of the instance or
class.

Example:
class Circle:
@staticmethod
def calculate_circumference(radius): # Static method
import math
return 2 * [Link] * radius

# Access without creating an instance


print(Circle.calculate_circumference(5)) # Output: 31.41592653589793

Comparison Table

Choosing the Right Method Type

 Use Instance Methods:


o When you need to access or modify an instance’s attributes or call other instance
methods.
o Example: Calculating an object-specific value like area or perimeter.
 Use Class Methods:

31
o When you need to operate on class-level data or create instances in a specific way
(factory methods).
o Example: Tracking the number of instances created.
 Use Static Methods:
o When you need a utility method that does not depend on the class or instance.
o Example: Validating input values or performing standalone calculations.

Mixed Example

Here’s a class with all three types of methods:

class Account:
bank_name = "XYZ Bank" # Class attribute

def __init__(self, account_holder, balance):


self.account_holder = account_holder
[Link] = balance

def deposit(self, amount): # Instance method


[Link] += amount
return [Link]

@classmethod
def get_bank_name(cls): # Class method
return cls.bank_name

@staticmethod
def validate_amount(amount): # Static method
return amount > 0

# Usage
account = Account("Alice", 1000)

# Instance method
[Link](500)
print([Link]) # Output: 1500

# Class method
print(Account.get_bank_name()) # Output: XYZ Bank

# Static method
print(Account.validate_amount(100)) # Output: True

32
DUCK TYPING IN PYTHON

Duck typing is a concept in Python where the type or class of an object is determined by
its behavior (what it can do) rather than its explicit type or class. The name comes from the
phrase:

"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck."

This philosophy emphasizes the behavior of an object over its specific type, enabling
more flexible and dynamic code.

Key Characteristics of Duck Typing

1. Behavior-Oriented:
o If an object implements the required behavior or methods, it can be used in place
of another object, regardless of its type.
2. No Explicit Type Checking:
o Python does not require you to declare variable types or use type-checking for
method arguments. It trusts the object to provide the required behavior.
3. Dynamic Typing:
o Python allows objects to be used interchangeably as long as they support the
required operations.

Example: Duck Typing in Action

Let’s consider a function that expects an object to have a method named quack:

class Duck:
def quack(self):
print("Quack!")

class Person:
def quack(self):
print("I'm pretending to be a duck!")

def make_it_quack(duck_like):
duck_like.quack()

# Both Duck and Person can "quack"


duck = Duck()
person = Person()

make_it_quack(duck) # Output: Quack!


make_it_quack(person) # Output: I'm pretending to be a duck!

Here:

33
 The make_it_quack function does not care if the object is a Duck or Person. It only cares
that the object has a quack method.

Benefits of Duck Typing

1. Flexibility:
o Encourages writing flexible and reusable code by focusing on behavior rather
than type.
2. Simplicity:
o Avoids complex inheritance or type checks, simplifying the code.
3. Extensibility:
o Easy to add new classes that work with existing functions as long as they conform
to the expected behavior.

Risks of Duck Typing

1. Runtime Errors:
o Errors due to missing methods or unsupported operations are only caught at
runtime, not during compilation.
o Example:

python
Copy code
class Dog:
def bark(self):
print("Woof!")

make_it_quack(Dog()) # AttributeError: 'Dog' object has no attribute 'quack'

2. Lack of Explicitness:
o It may be unclear what type of object a function expects, making code harder to
understand.

Safeguarding Against Runtime Errors

1. Use hasattr to Check Behavior:

 Check if the required method exists before calling it.

34
python
Copy code
def make_it_quack(duck_like):
if hasattr(duck_like, 'quack'):
duck_like.quack()
else:
print("This object cannot quack!")

2. Type Hinting (Optional):

 Use Python type hints to provide a hint about the expected behavior (e.g., using
[Link]).

python
Copy code
from typing import Protocol

class Quackable(Protocol):
def quack(self) -> None:
...

def make_it_quack(duck_like: Quackable):


duck_like.quack()

Real-World Example: File-Like Objects

The open function in Python returns file-like objects, but you can use custom objects with
the same methods (read, write, etc.) without worrying about their exact type.

class FileMock:
def write(self, content):
print(f"Mock write: {content}")

def save_to_file(file_obj):
file_obj.write("Hello, Duck Typing!")

# Using a real file


with open("[Link]", "w") as file:
save_to_file(file)

# Using a mock file


mock_file = FileMock()
save_to_file(mock_file)

Output:

35
Mock write: Hello, Duck Typing!

Duck Typing vs Polymorphism

 Duck Typing: Behavior is determined dynamically based on the presence of


methods/attributes at runtime.
 Polymorphism: Requires explicitly defined relationships between classes (like
inheritance).

SPECIAL METHODS

Special methods in Python, also known as magic methods or dunder (double


underscore) methods, are predefined methods with double underscores (__) at the beginning
and end of their names. They allow developers to customize and define how objects of a class
behave in certain operations.

For example:

 Using + to add objects can be customized with the __add__ method.


 Printing an object invokes the __str__ method.

Categories of Special Methods

1. Initialization and Construction


2. String Representation
3. Arithmetic and Comparison
4. Container Emulation
5. Callable Objects
6. Context Management
7. Other Special Methods

1. Initialization and Construction

__init__(self, ...)

 Called when an instance of a class is created.


 Used to initialize instance attributes.

Example:
class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

person = Person("Alice", 30)

36
print([Link]) # Output: Alice

__new__(cls, ...)

 Used to control the creation of a new instance.


 Rarely used but can be useful for singleton patterns.

2. String Representation

__str__(self)

 Defines the "informal" or human-readable string representation of an object.


 Used when print(object) is called.

__repr__(self)

 Defines the "formal" or developer-friendly string representation of an object.


 Used in debugging and in the interactive interpreter.

Example:
class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

def __str__(self):
return f"{[Link]}, {[Link]} years old"

def __repr__(self):
return f"Person(name='{[Link]}', age={[Link]})"

person = Person("Alice", 30)


print(str(person)) # Output: Alice, 30 years old
print(repr(person)) # Output: Person(name='Alice', age=30)

3. Arithmetic and Comparison

Arithmetic Operations

 __add__(self, other) for +


 __sub__(self, other) for -
 __mul__(self, other) for *, etc.

37
Example:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, other):


return Point(self.x + other.x, self.y + other.y)

def __str__(self):
return f"({self.x}, {self.y})"

p1 = Point(2, 3)
p2 = Point(4, 5)
p3 = p1 + p2
print(p3) # Output: (6, 8)

Comparison Operations

 __eq__(self, other) for ==


 __lt__(self, other) for <
 __gt__(self, other) for >, etc.

Example:
class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

def __eq__(self, other):


return [Link] == [Link]

alice = Person("Alice", 30)


bob = Person("Bob", 30)
print(alice == bob) # Output: True

4. Container Emulation

__getitem__(self, key)

 Defines how to access elements using indexing or slicing.

38
__setitem__(self, key, value)

 Defines how to assign values to specific indices or keys.

Example:
class MyList:
def __init__(self):
[Link] = []

def __getitem__(self, index):


return [Link][index]

def __setitem__(self, index, value):


[Link][index] = value

def __str__(self):
return str([Link])

lst = MyList()
[Link] = [1, 2, 3]
print(lst[1]) # Output: 2
lst[1] = 10
print(lst) # Output: [1, 10, 3]

5. Callable Objects

__call__(self, *args, **kwargs)

 Makes an instance of a class callable, like a function.

Example:
class Adder:
def __init__(self, increment):
[Link] = increment

def __call__(self, value):


return value + [Link]

add5 = Adder(5)
print(add5(10)) # Output: 15

6. Context Management

39
__enter__(self) and __exit__(self, exc_type, exc_value, traceback)

 Used to implement context managers for the with statement.

Example:
class FileManager:
def __init__(self, filename, mode):
[Link] = open(filename, mode)

def __enter__(self):
return [Link]

def __exit__(self, exc_type, exc_value, traceback):


[Link]()

with FileManager("[Link]", "w") as f:


[Link]("Hello, world!")
# File is automatically closed

7. Other Special Methods

 Hashing and Identity:


o __hash__(self) for using objects in sets or as dictionary keys.
o __eq__(self, other) for equality comparisons.
 Object Creation and Destruction:
o __del__(self) for defining cleanup behavior when an object is deleted.
 String Conversion:
o __format__(self, format_spec) for defining custom formatting.

Example of __format__:
class Person:
def __init__(self, name, age):
[Link] = name
[Link] = age

def __format__(self, format_spec):


if format_spec == "short":
return f"{[Link]}"
return f"{[Link]}, {[Link]} years old"

person = Person("Alice", 30)


print(f"{person:short}") # Output: Alice

COMPOSITION IN PYTHON

40
COMPOSITION in PYTHON

Composition is a design principle in object-oriented programming where one class


contains or is composed of objects of another class. Instead of inheriting behavior from a parent
class (as in inheritance), composition enables a class to delegate functionality to other objects it
contains.

In simple terms, composition is a "has-a" relationship. For example:

 A Car has a Engine.


 A Book has a Author.

Why Use Composition?

1. Better Code Reusability:


o You can use existing classes as components without modifying them.
2. Avoid Tight Coupling:
o Classes are loosely coupled, making the system more flexible and easier to
maintain.
3. Extensibility:
o You can easily extend functionality by composing new objects.
4. Avoid Inheritance Pitfalls:
o Composition avoids the complexities of multiple inheritance, reducing the risk of
errors.

Example of Composition

Let’s demonstrate composition with an example of a Car and its components, Engine and Tires.

Example:
class Engine:
def __init__(self, horsepower):
[Link] = horsepower

def start(self):
return "Engine started."

class Tires:
def __init__(self, brand):
[Link] = brand

def inflate(self):
return f"{[Link]} tires inflated."

class Car:
def __init__(self, brand, engine, tires):

41
[Link] = brand
[Link] = engine # Composition: Car has an Engine
[Link] = tires # Composition: Car has Tires

def drive(self):
return f"{[Link]} is driving. {[Link]()} {[Link]()}"

# Create components
engine = Engine(300)
tires = Tires("Michelin")

# Compose Car with Engine and Tires


car = Car("Toyota", engine, tires)
print([Link]())

Output:

Toyota is driving. Engine started. Michelin tires inflated.

Key Points in the Example

1. Independent Components:
o Engine and Tires are independent classes, not subclasses of Car.
2. Reusability:
o The Engine and Tires classes can be reused in other classes (e.g., a Truck).
3. Modularity:
o You can replace the Engine or Tires objects in the Car class with different
implementations without modifying the Car class.

When to Use Composition vs Inheritance?

Composition Inheritance
Used when the relationship is "has-a". Used when the relationship is "is-a".

Promotes loose coupling and modular Increases tight coupling between


design. parent and child.

Better for flexibility and dynamic Better for sharing common


behavior changes. functionality.

Example: A Car has-a Engine. Example: A Dog is-a Animal.

Real-World Example: File Management System

42
Imagine a system that manages different types of files with shared functionality for encryption
and compression. Composition can be used to combine these features.

Example:
class Encryptor:
def encrypt(self, data):
return f"Encrypted({data})"

class Compressor:
def compress(self, data):
return f"Compressed({data})"

class FileManager:
def __init__(self, encryptor, compressor):
[Link] = encryptor
[Link] = compressor

def save(self, data):


encrypted = [Link](data)
compressed = [Link](encrypted)
return f"Saving: {compressed}"

# Create encryptor and compressor objects


encryptor = Encryptor()
compressor = Compressor()

# Compose FileManager with encryptor and compressor


file_manager = FileManager(encryptor, compressor)
print(file_manager.save("MyData"))

Output:

Saving: Compressed(Encrypted(MyData))

Advantages of Composition

1. Flexibility:
o You can change parts of the system by swapping out composed objects.
2. Encapsulation:
o Details of composed objects are hidden, reducing complexity.
3. Reuse:
o Components like Engine, Tires, Encryptor, and Compressor can be reused in other
systems.

Disadvantages of Composition

43
1. Slightly More Verbose:
o You need to create and manage multiple objects explicitly.
2. Manual Delegation:
o Methods from composed objects might need to be explicitly delegated.

44

You might also like