BCA-302: Python Programming Notes

Python Concepts

  • Origin: Python was created by Guido van Rossum in the late 1980s, with its first release in 1991. It was conceived as a successor to the ABC programming language, with a focus on code readability and a clear syntax. The name "Python" was inspired by the Monty Python's Flying Circus television show.
  • Comparison: Python is often compared to other languages like Java, C++, and JavaScript. Key differences include:
    • Syntax: Python emphasizes readability with a clean, English-like syntax, using indentation for block delimitation. Java and C++ use braces `{}`. JavaScript is more flexible but can lead to less readable code if not styled carefully.
    • Typing: Python is dynamically typed (you don't need to declare variable types), while Java and C++ are statically typed (you must declare types). JavaScript is also dynamically typed.
    • Interpretation: Python is interpreted (code is executed line by line), while Java is compiled to bytecode and then interpreted by the JVM. C++ is compiled directly to machine code.
    • Memory Management: Python uses automatic memory management (garbage collection), relieving the programmer from manual memory allocation/deallocation. C++ requires manual memory management, which can be a source of errors. Java also uses garbage collection.
    • Use Cases: Python is widely used for web development, data science, scripting, AI, and more. Java is prevalent in enterprise applications. C++ is used for systems programming, game development, and high-performance computing. JavaScript is the primary language for front-end web development and is also used on the server-side (Node.js).
  • Comments: Used to add explanations to code, improving readability and maintainability. Ignored by the Python interpreter.
    • Single-line comment:
      # This is a comment
    • Multi-line comment (docstring):
      """This is a
          multi-line comment,
          often used as a docstring
          to document functions
          or classes."""
  • Variables and Assignment: Variables are names that store data values. Assignment uses the `=` operator to bind a value to a variable name.
    x = 10
    name = "Alice"
    pi = 3.14159
  • Identifiers: Names given to variables, functions, classes, modules, etc.
    • Rules:
      • Must start with a letter (a-z, A-Z) or an underscore (\_).
      • Can contain letters, numbers (0-9), and underscores.
      • Case-sensitive (e.g., `myVar` and `myvar` are different).
      • Cannot be a Python keyword (e.g., `if`, `for`, `while`, `def`, `class`).
    • Best Practices:
      • Use descriptive names that indicate the variable's purpose.
      • Use lowercase with underscores for variable and function names (e.g., `my_variable`, `calculate_sum`). This is known as snake_case.
      • Use CamelCase for class names (e.g., `MyClass`).
      • Avoid single-character names (except for loop counters in simple cases).
  • Basic Style Guidelines (PEP 8): PEP 8 is the style guide for Python code. It provides recommendations for:
    • Indentation: Use 4 spaces per indentation level.
    • Line length: Limit lines to 79 characters.
    • Blank lines: Use blank lines to separate functions and classes, and to improve readability within functions.
    • Naming conventions: As described in Identifiers.
    • Whitespace: Use spaces around operators and after commas.
    • Imports: Place imports at the beginning of the file, group them, and follow a specific order.
    Following PEP 8 makes your code more consistent and easier to read by others (and yourself!).
  • Standard Types: Built-in data types that represent different kinds of values.
    • Numbers:
      • Integers (`int`): Whole numbers (e.g., `10`, `-5`, `0`).
        x = 10
        print(type(x)) # Output: <class 'int'>
      • Floating-point numbers (`float`): Numbers with a decimal point (e.g., `3.14`, `-2.5`, `0.0`).
        y = 3.14
        print(type(y)) # Output: <class 'float'>
      • Complex numbers (`complex`): Numbers with a real and imaginary part (e.g., `2+3j`, where `j` is the imaginary unit).
        z = 2 + 3j
        print(type(z)) # Output: <class 'complex'>
    • Strings (`str`): Immutable sequences of characters (e.g., `"Hello"`, `'World'`, `"""This is a multi-line string"""`).
      message = "Hello, world!"
      print(type(message)) # Output: <class 'str'>
    • Boolean (`bool`): Represents truth values: `True` or `False`.
      is_valid = True
      print(type(is_valid)) # Output: <class 'bool'>
  • Internal Types: Types used internally by the Python interpreter. Programmers don't usually interact with these directly. Examples include types related to code objects, frames, and tracebacks.
  • Operators: Symbols that perform operations on values (operands).
    • Arithmetic operators: `+` (addition), `-` (subtraction), `*` (multiplication), `/` (division), `//` (floor division - returns the integer part of the division), `%` (modulo - returns the remainder), `**` (exponentiation).
      result = 5 + 2      # 7
      result = 5 - 2      # 3
      result = 5 * 2      # 10
      result = 5 / 2      # 2.5
      result = 5 // 2     # 2
      result = 5 % 2      # 1
      result = 5 ** 2     # 25
    • Comparison operators: `==` (equal to), `!=` (not equal to), `>` (greater than), `<` (less than), `>=` (greater than or equal to), `<=` (less than or equal to). Return a Boolean value.
      x = 5
      y = 10
      print(x == y)    # False
      print(x != y)    # True
      print(x > y)     # False
      print(x < y)     # True
      print(x >= y)    # False
      print(x <= y)    # True
    • Logical operators: `and` (returns `True` if both operands are `True`), `or` (returns `True` if at least one operand is `True`), `not` (returns the opposite of the operand's Boolean value).
      a = True
      b = False
      print(a and b)  # False
      print(a or b)   # True
      print(not a)    # False
    • Assignment operators: `=` (simple assignment), `+=`, `-=`, `*=`, `/=`, `//=`, `%=`, `**=` (compound assignment operators that combine an arithmetic operation with assignment).
      x = 10
      x += 5    # x is now 15
      x -= 5    # x is now 10
      x *= 5    # x is now 50
      x /= 5    # x is now 10.0
      x //= 3   # x is now 3.0
      x %= 3    # x is now 0.0
      x **= 2   # x is now 0.0
    • Membership operators: `in` (returns `True` if a value is found in a sequence), `not in` (returns `True` if a value is not found in a sequence).
      my_list = [1, 2, 3]
      print(2 in my_list)         # True
      print(4 in my_list)         # False
      print(4 not in my_list)     # True
    • Identity operators: `is` (returns `True` if two variables refer to the same object), `is not` (returns `True` if two variables do not refer to the same object).
      x = [1, 2, 3]
      y = x
      z = [1, 2, 3]
      print(x is y)    # True (x and y refer to the same object)
      print(x is z)    # False (x and z are different objects, even with the same content)
      print(x is not z) # True
  • Built-in Functions: Functions that are always available in Python without needing to import any modules. Examples: `print()`, `len()`, `type()`, `int()`, `float()`, `str()`, `bool()`, `input()`, `range()`, `sum()`, `max()`, `min()`.
    print("Hello")
    length = len("Python")
    print(length)           # Output: 6
    data_type = type(10)
    print(data_type)      # Output: <class 'int'>
    converted_int = int("123")
    print(converted_int) # Output: 123
    converted_float = float("123.45")
    print(converted_float) # Output: 123.45
    text = str(123)
    print(text)          # Output: "123"
    value = bool(0)
    print(value)         # Output: False
    user_input = input("Enter your name: ")
    print("Hello, " + user_input)
    numbers = range(5)
    print(list(numbers))  # Output: [0, 1, 2, 3, 4]
    total = sum([1, 2, 3, 4])
    print(total)         # Output: 10
    maximum = max([1, 5, 2, 8])
    print(maximum)       # Output: 8
    minimum = min([1, 5, 2, 8])
    print(minimum)       # Output: 1
  • Numbers and Strings: See "Standard Types" above for more details.
  • Sequences: Ordered collections of items.
    • Strings (`str`): Immutable sequences of characters.
    • Lists (`list`): Mutable sequences of items (can contain items of different types).
    • Tuples (`tuple`): Immutable sequences of items.
  • String Operators & Functions:
    • Concatenation (`+`): Joins two strings.
      str1 = "Hello"
      str2 = "World"
      result = str1 + " " + str2  # "Hello World"
    • Repetition (`*`): Repeats a string multiple times.
      text = "abc"
      repeated_text = text * 3  # "abcabcabc"
    • Slicing (`[start:end:step]`): Extracts a portion of a string.
      my_string = "Python"
      substring = my_string[1:4]    # "yth"
      substring = my_string[:3]     # "Pyt"
      substring = my_string[2:]     # "thon"
      substring = my_string[::2]    # "Pto"
      substring = my_string[::-1]   # "nohtyP"
    • Methods: Functions that are called on a string object (e.g., `.upper()`, `.lower()`, `.strip()`, `.find()`, `.replace()`, `.split()`, `.join()`, `.format()`).
      text = "  Python is Fun  "
      upper_text = text.upper()       # "  PYTHON IS FUN  "
      lower_text = text.lower()       # "  python is fun  "
      stripped_text = text.strip()     # "Python is Fun"
      index = text.find("Fun")       # 11
      replaced_text = text.replace("Fun", "Awesome") # "  Python is Awesome  "
      words = text.split()          # ['Python', 'is', 'Fun']
      joined_text = "-".join(words)    # "Python-is-Fun"
      formatted_text = "{} is {}".format("Python", "Awesome") # "Python is Awesome"
      f_string = f"{'Python'} is {'Awesome'}"      # "Python is Awesome"
  • Special Features of Strings:
    • Immutability: Strings cannot be changed after they are created. Operations that appear to modify a string actually create a new string.
    • String methods: Provide powerful ways to manipulate and work with string data.
    • Formatted string literals (f-strings): A concise way to embed expressions inside string literals.
  • Memory Management: Python uses automatic memory management through a process called garbage collection. The interpreter automatically allocates memory for objects and reclaims memory that is no longer being used, reducing the risk of memory leaks and other memory-related errors. Python also has a small object allocator for frequently used small objects, which improves efficiency.

Conditionals and Loops

  • `if` statement: Executes a block of code if a condition is `True`. The condition is an expression that evaluates to a Boolean value.
    x = 15
    if x > 0:
        print("Positive")
  • `else` statement: Provides an alternative block of code to execute if the `if` condition is `False`.
    x = 10
    if x % 2 == 0:
        print("Even")
    else:
        print("Odd")
  • `elif` statement: Short for "else if." Allows you to check multiple conditions in sequence.
    grade = 85
    if grade >= 90:
        print("A")
    elif grade >= 80:
        print("B")
    elif grade >= 70:
        print("C")
    else:
        print("Below C")
  • `while` statement: Repeatedly executes a block of code as long as a condition is `True`. It's used for indefinite iteration (when you don't know in advance how many times the loop will execute).
    count = 0
    while count < 5:
        print(count)
        count += 1
  • `for` statement: Iterates over a sequence (e.g., list, tuple, string, range) and executes a block of code for each item in the sequence. It's used for definite iteration (when you know how many times the loop will execute).
    fruits = ["apple", "banana", "cherry"]
    for fruit in fruits:
        print(fruit)
  • `break` statement: Immediately terminates the innermost loop (either `for` or `while`) and execution continues with the statement after the loop.
    for i in range(10):
        if i == 5:
            break
        print(i)
  • `continue` statement: Skips the rest of the current iteration of the loop and proceeds to the next iteration.
    for i in range(5):
        if i == 3:
            continue
        print(i)
  • `pass` statement: A null operation; it does nothing. It's used as a placeholder where a statement is syntactically required but no code needs to be executed.
    if True:
        pass  # Placeholder
  • `else` statement with loop:
    • In a `for` loop, the `else` block is executed after the loop completes normally (i.e., without encountering a `break` statement).
      for i in range(3):
          print(i)
      else:
          print("Loop finished normally")
    • In a `while` loop, the `else` block is executed when the loop condition becomes `False`.
      count = 0
      while count < 3:
          print(count)
          count += 1
      else:
          print("While loop finished")

Object and Classes

  • Classes in Python, Principles of Object Orientation, Creating Classes, Instance Methods, Class variables, Inheritance, Polymorphism. Type Identification, Python libraries(Strings. Data structures & algorithms )
  • Classes: Blueprints for creating objects.
    class Dog:
        def __init__(self, name, breed):
            self.name = name
            self.breed = breed
        def bark(self):
            print("Woof!")
    my_dog = Dog("Buddy", "Golden Retriever")
    print(my_dog.name)  # Output: Buddy
    my_dog.bark()      # Output: Woof!
  • Principles of Object Orientation:
    • Encapsulation: Bundling data (attributes) and methods that operate on the data within a single unit (class).
    • Abstraction: Hiding complex implementation details and showing only necessary information to the user.
    • Inheritance: Creating new classes (derived or child classes) based on existing classes (base or parent classes), inheriting their attributes and methods.
    • Polymorphism: The ability of objects of different classes to respond to the same method call in their own way.
  • Creating Classes: Using the `class` keyword. The `__init__` method is a special constructor.
  • Instance Methods: Methods that operate on the instance (object) of a class. They have `self` as the first parameter.
  • Class Variables: Variables that are shared by all instances of a class. They are defined within the class but outside any methods.
    class Counter:
        count = 0 # Class variable
        def __init__(self):
            Counter.count += 1
    c1 = Counter()
    c2 = Counter()
    print(Counter.count) # Output: 2
  • Inheritance: Creating a new class that inherits from an existing class.
    class Animal:
        def speak(self):
            print("Generic animal sound")
    class Cat(Animal):
        def speak(self): # Method overriding
            print("Meow!")
    my_cat = Cat()
    my_cat.speak() # Output: Meow!
  • Polymorphism: Different classes responding to the same method.
    def animal_sound(animal):
        animal.speak()
    animal1 = Animal()
    animal2 = Cat()
    animal_sound(animal1) # Output: Generic animal sound
    animal_sound(animal2) # Output: Meow!
  • Type Identification: Using functions like `type()` and `isinstance()` to check the type of an object.
    x = 10
    print(type(x))        # <class 'int'>
    print(isinstance(x, int)) # True
    print(isinstance(x, float)) # False
  • Python libraries(Strings. Data structures & algorithms )

Lists and Sets

  • Built-in Functions, List type built in Methods. Tuples. Tuple Operators Special Features of Tuples, Set: Introduction, Accessing, Built-in Methods (Add, Update, Clear, Copy, Discard, Remove), Operations (Union, Intersection, Difference )
  • Lists: Mutable ordered sequences (already covered in Unit-I).
  • Sets: Unordered collections of unique elements, defined using curly braces `{}` or the `set()` constructor.
    my_set = {1, 2, 3, 3, 4} # {1, 2, 3, 4} (duplicates are automatically removed)
    another_set = set([4, 5, 6])
  • Built-in Functions (for Lists and Sets): `len()`, `max()`, `min()`, `sorted()`, `sum()`.
    my_list = [3, 1, 4, 1, 5, 9, 2, 6]
    print(len(my_list))       # 8
    print(max(my_list))       # 9
    print(min(my_list))       # 1
    print(sorted(my_list))    # [1, 1, 2, 3, 4, 5, 6, 9]
    print(sum(my_list))       # 31
  • Tuples: Immutable ordered sequences (already covered in Unit-I).
  • Tuple Operators: Similar to list operators (concatenation `+`, repetition `*`).
    tuple1 = (1, 2, 3)
    tuple2 = (4, 5, 6)
    combined_tuple = tuple1 + tuple2 # (1, 2, 3, 4, 5, 6)
    repeated_tuple = tuple1 * 2     # (1, 2, 3, 1, 2, 3)
  • Special Features of Tuples: Immutability makes them suitable for representing fixed collections of items and as keys in dictionaries.
  • Set Introduction: Already covered above.
  • Accessing: You can iterate through sets using a `for` loop, but you cannot access elements by index because they are unordered.
    for item in my_set:
        print(item)
  • Built-in Methods (Sets):
    • `add(element)`: Adds an element to the set.
      my_set.add(10)
    • `update(iterable)`: Adds multiple elements from an iterable.
      my_set.update([7, 8, 9])
    • `clear()`: Removes all elements from the set.
      my_set.clear()
    • `copy()`: Returns a shallow copy of the set.
      new_set = my_set.copy()
    • `discard(element)`: Removes an element if it is present (no error if not found).
      my_set.discard(3)
    • `remove(element)`: Removes an element (raises a `KeyError` if not found).
      my_set.remove(2)
  • Operations (Sets):
    • Union (`|` or `union()`): Returns a new set containing all elements from both sets.
      set1 = {1, 2, 3}
      set2 = {3, 4, 5}
      union_set = set1 | set2 # {1, 2, 3, 4, 5}
      union_set = set1.union(set2)
    • Intersection (`&` or `intersection()`): Returns a new set containing common elements.
      intersection_set = set1 & set2 # {3}
      intersection_set = set1.intersection(set2)
    • Difference (`-` or `difference()`): Returns a new set containing elements in the first set but not in the second.
      difference_set = set1 - set2 # {1, 2}
      difference_set = set1.difference(set2)
  • Dictionaries: Introduction to Dictionaries, Built-in Functions, Built-in Methods, Dictionary Keys, Sorting and Looping, Nested Dictionaries.
  • Dictionaries: Unordered collections of key-value pairs, defined using curly braces `{}` with keys and values separated by colons `:`. Keys must be unique and immutable.
    my_dict = {"name": "Alice", "age": 30, "city": "New York"}
  • Built-in Functions (for Dictionaries): `len()`, `str()` (string representation), `type()`.
    print(len(my_dict))  # 3
    print(str(my_dict))  # "{'name': 'Alice', 'age': 30, 'city': 'New York'}"
    print(type(my_dict)) # <class 'dict'>
  • Built-in Methods (for Dictionaries):
    • `keys()`: Returns a view object that displays a list of all the keys in the dictionary.
      keys = my_dict.keys() # dict_keys(['name', 'age', 'city'])
      print(list(keys))    # ['name', 'age', 'city']
    • `values()`: Returns a view object that displays a list of all the values in the dictionary.
      values = my_dict.values() # dict_values(['Alice', 30, 'New York'])
      print(list(values))  # ['Alice', 30, 'New York']
    • `items()`: Returns a view object that displays a list of all the key-value pairs (as tuples).
      items = my_dict.items() # dict_items([('name', 'Alice'), ('age', 30), ('city', 'New York')])
      print(list(items))    # [('name', 'Alice'), ('age', 30), ('city', 'New York')]
  • Important Dictionary Methods:
    • `get(key, default)`: Returns the value for the specified key. If the key is not present, it returns the `default` value (or `None` if no default is provided).
      name = my_dict.get("name")      # "Alice"
      country = my_dict.get("country")  # None
      country = my_dict.get("country", "Unknown") # "Unknown"
    • `update(other_dict)`: Updates the dictionary with the key-value pairs from `other_dict`. Existing keys are overwritten, new keys are added.
      more_data = {"country": "USA", "occupation": "Engineer"}
      my_dict.update(more_data) # my_dict is now {"name": "Alice", "age": 30, "city": "New York", "country": "USA", "occupation": "Engineer"}
    • `pop(key, default)`: Removes the key and returns the corresponding value. If the key is not found, it returns the `default` value (or raises a `KeyError` if no default is provided).
      age = my_dict.pop("age") # age is 30, my_dict is now {"name": "Alice", "city": "New York", "country": "USA", "occupation": "Engineer"}
      job = my_dict.pop("job", "Not found") # job is "Not found"
    • `popitem()`: Removes and returns an arbitrary (key, value) pair from the dictionary.
      item = my_dict.popitem()
    • `clear()`: Removes all items from the dictionary.
      my_dict.clear()
  • Dictionary Keys: Keys must be immutable (e.g., strings, numbers, tuples).
  • Sorting and Looping:
    • Iterating through a dictionary:
      for key in my_dict:
          print(key, my_dict[key])
      for key, value in my_dict.items():
          print(key, value)
    • Sorting a dictionary (returns a sorted list of keys):
      sorted_keys = sorted(my_dict) # Sorts by keys
      sorted_items = sorted(my_dict.items(), key=lambda item: item[1]) # Sorts by values
      print(sorted_keys)
      print(sorted_items)
  • Nested Dictionaries: Dictionaries can contain other dictionaries as values, allowing you to represent hierarchical data structures.
    employee = {
        "name": "Alice",
        "details": {
            "age": 30,
            "city": "New York",
            "office": {
                "building": "Empire State Building",
                "floor": 10
            }
        }
    }
    print(employee["details"]["office"]["building"]) # "Empire State Building"

Files

  • File Objects, File Built-in Function, File Built-in Methods, File Built-in Attributes, Standard Files, Command-line Arguments. File System. File Execution, Persistent Storage Modules.
  • File Objects: Represent files in the operating system. You interact with files in Python through file objects.
  • File Built-in Function: `open()`: Used to open a file. It takes the file path and the mode of opening the file as arguments (e.g., 'r' for read, 'w' for write, 'a' for append, 'b' for binary).
    file = open("my_file.txt", "r") # Opens the file in read mode
  • File Built-in Methods: Methods for reading from and writing to files.
    • `read()`: Reads the entire file content as a string.
      file_content = file.read()
    • `readline()`: Reads a single line from the file.
      line = file.readline()
    • `readlines()`: Reads all lines from the file and returns them as a list of strings.
      lines = file.readlines()
    • `write(string)`: Writes a string to the file.
      file.write("Hello, world!")
    • `writelines(list_of_strings)`: Writes a list of strings to the file.
      file.writelines(["Line 1\n", "Line 2\n"])
    • `close()`: Closes the file. It's crucial to close files to free up system resources and ensure that any buffered data is written to the file. It's often used with a `try...finally` block or the `with` statement to ensure closure.
      file.close()
      try:
          file = open("my_file.txt", "r")
          # Do something with the file
      finally:
          file.close()
      with open("my_file.txt", "r") as file:
          # Do something with the file
      # File is automatically closed after the 'with' block
    • `seek(offset, whence)`: Changes the file's current position.
      file.seek(0) # Go to the beginning of the file
      file.seek(10) # Go to the 10th byte
    • `tell()`: Returns the file's current position.
      position = file.tell()
      print(position)
  • File Built-in Attributes: Attributes of a file object.
    • `name`: The name of the file.
      print(file.name)
    • `mode`: The mode in which the file was opened.
      print(file.mode)
    • `closed`: A Boolean indicating whether the file is closed.
      print(file.closed)
  • Standard Files:
    • `sys.stdin`: The standard input stream (usually the keyboard).
    • `sys.stdout`: The standard output stream (usually the console).
    • `sys.stderr`: The standard error stream (used for displaying error messages).
  • Command-line Arguments: Arguments passed to a Python script when it's executed from the command line. They are accessible through the `sys.argv` list. `sys.argv[0]` is the name of the script itself.
    import sys
    print("Script name:", sys.argv[0])
    for i, arg in enumerate(sys.argv[1:]):
        print(f"Argument {i+1}: {arg}")
    # If you run the script as: python my_script.py arg1 arg2 arg3
    # Output:
    # Script name: my_script.py
    # Argument 1: arg1
    # Argument 2: arg2
    # Argument 3: arg3
  • File System: Interacting with the file system using modules like `os` and `shutil`. Operations include:
    • Creating and deleting directories (`os.mkdir()`, `os.rmdir()`, `os.makedirs()`, `os.removedirs()`).
      import os
      os.mkdir("my_directory")
      os.makedirs("my_directory/sub_directory") # Creates nested directories
      os.rmdir("my_directory") # Removes an empty directory
      os.removedirs("my_directory/sub_directory") # Removes directory and all its sub-directories
    • Checking file/directory existence (`os.path.exists()`, `os.path.isfile()`, `os.path.isdir()`).
      import os.path
      print(os.path.exists("my_file.txt"))
      print(os.path.isfile("my_file.txt"))
      print(os.path.isdir("my_directory"))
    • Renaming files/directories (`os.rename()`).
      os.rename("old_name.txt", "new_name.txt")
    • Copying files (`shutil.copy()`, `shutil.copy2()`, `shutil.copytree()`).
      import shutil
      shutil.copy("file1.txt", "file2.txt") # Copy file1.txt to file2.txt
      shutil.copy2("file1.txt", "file3.txt") # Copy file1.txt to file3.txt with metadata
      shutil.copytree("my_directory", "my_directory_backup") # Copies entire directory tree
    • Getting file/directory information (e.g., size, modification time: `os.path.getsize()`, `os.path.getmtime()`).
      import os.path
      size = os.path.getsize("my_file.txt")
      mod_time = os.path.getmtime("my_file.txt")
      print(size)
      print(mod_time)
    • Changing the current working directory (`os.chdir()`, `os.getcwd()`).
      import os
      os.chdir("/path/to/my/directory")
      current_dir = os.getcwd()
      print(current_dir)
    • Listing files and directories (`os.listdir()`, `os.scandir()`).
      import os
      files = os.listdir()
      print(files)
      with os.scandir() as entries:
          for entry in entries:
              print(entry.name, entry.is_file(), entry.is_dir())
  • File Execution: Running other programs or scripts from within a Python script using modules like `subprocess` or `os.system()`.
    import subprocess
    subprocess.run(["ls", "-l"]) # Runs the 'ls -l' command
    # To capture the output:
    result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
    print(result.stdout)
  • Persistent Storage Modules: Modules for storing data persistently (i.e., the data remains even after the program terminates).
    • `pickle`: Serializes Python objects into a byte stream, which can be stored in a file. Used for storing complex data structures.
      import pickle
      data = {"name": "Alice", "age": 30}
      with open("my_data.pickle", "wb") as f:
          pickle.dump(data, f)
      with open("my_data.pickle", "rb") as f:
          loaded_data = pickle.load(f)
      print(loaded_data)
    • `json`: Encodes and decodes data in JSON (JavaScript Object Notation) format, a lightweight and human-readable format. Useful for data exchange between applications.
      import json
      data = {"name": "Alice", "age": 30}
      json_string = json.dumps(data)
      print(json_string)
      with open("my_data.json", "w") as f:
          json.dump(data, f)
      with open("my_data.json", "r") as f:
          loaded_data = json.load(f)
      print(loaded_data)
    • `shelve`: Provides a dictionary-like interface for persistent storage of Python objects, using a database-backed storage.
      import shelve
      with shelve.open("my_data.shelve") as db:
          db["name"] = "Alice"
          db["age"] = 30
      with shelve.open("my_data.shelve") as db:
          print(db["name"])
          print(db["age"])
  • Regular Expression: Introduction/Motivation , Special Symbols and Characters for REs , REs and Python.
  • Regular Expressions (Regex): Powerful tools for pattern matching in strings. They provide a concise way to search, validate, and manipulate text based on specific patterns.
  • Introduction/Motivation:
    • Why use regular expressions?
      • Searching: Find specific patterns within large amounts of text (e.g., finding all email addresses in a document).
      • Validation: Verify that strings conform to a specific format (e.g., validating phone numbers, email addresses, URLs).
      • Manipulation: Replace or extract parts of a string based on a pattern (e.g., replacing all occurrences of a word, extracting data from a log file).
  • Special Symbols and Characters for REs (Metacharacters):
    • `.` (dot): Matches any character except a newline.
      import re
      text = "abc\ndef"
      match = re.search(r".", text)
      if match:
          print(match.group(0)) # Output: a
    • `^` (caret): Matches the beginning of a string.
      text = "Start here"
      match = re.search(r"^Start", text)
      if match:
          print("Match at the beginning")
    • `$` (dollar): Matches the end of a string.
      text = "End here"
      match = re.search(r"here$", text)
      if match:
          print("Match at the end")
    • `\*` (asterisk): Matches the preceding character zero or more times.
      text = "abccc"
      match = re.search(r"bc*", text)
      if match:
          print(match.group(0)) # Output: bccc
    • `+` (plus): Matches the preceding character one or more times.
      text = "abccc"
      match = re.search(r"bc+", text)
      if match:
          print(match.group(0)) # Output: bccc
    • `?` (question mark): Matches the preceding character zero or one time.
      text = "abc"
      match = re.search(r"ab?c", text)
      if match:
          print(match.group(0)) # Output: abc
      text = "ac"
      match = re.search(r"ab?c", text)
      if match:
          print(match.group(0)) # Output: ac
    • `[]` (square brackets): Defines a character class, matching any character within the brackets (e.g., `[a-z]` matches any lowercase letter).
      text = "a1b2c3d4"
      match = re.findall(r"[a-c]", text)
      print(match) # Output: ['a', 'b', 'c']
    • `[^]` (negated square brackets): Matches any character *not* within the brackets.
      text = "a1b2c3d4"
      match = re.findall(r"[^0-9]", text)
      print(match) # Output: ['a', 'b', 'c', 'd']
    • `\` (backslash): Escapes special characters (e.g., `\.` matches a literal dot, `\d` matches a digit).
      text = "123.456"
      match = re.search(r"\.", text)
      if match:
          print(match.group(0)) # Output: .
      text = "abc123def"
      match = re.search(r"\d+", text)
      if match:
          print(match.group(0)) # Output: 123
    • `|` (pipe): Specifies alternation (e.g., `a|b` matches either "a" or "b").
      text = "apple or banana"
      match = re.findall(r"apple|banana", text)
      print(match) # Output: ['apple', 'banana']
    • `()` (parentheses): Groups characters together and creates capturing groups (used for extracting matched portions of the string).
      text = "name: John, age: 30"
      match = re.search(r"name: (\w+), age: (\d+)", text)
      if match:
          print(match.group(1)) # Output: John
          print(match.group(2)) # Output: 30
    • `{m,n}` (curly braces): Matches the preceding character at least `m` times and at most `n` times.
      text = "aab"
      match = re.search(r"a{2,3}b", text)
      if match:
        print(match.group(0))  # Output: aab
      text = "aaab"
      match = re.search(r"a{2,3}b", text)
      if match:
        print(match.group(0)) # Output: aaab
      
      text = "aaaaab"
      match = re.search(r"a{2,3}b", text)
      if match:
        print(match.group(0)) # Output: None
                          
  • REs and Python: The `re` module in Python provides functions for working with regular expressions.
    • `re.search(pattern, string)`: Searches for the first occurrence of the pattern in the string. Returns a match object if found, otherwise `None`.
      import re
      text = "The quick brown fox"
      match = re.search(r"quick", text)
      if match:
          print("Found a match!")
          print(match.start(), match.end()) # 4, 9
      else:
          print("No match")
    • `re.match(pattern, string)`: Tries to match the pattern at the \*beginning\* of the string. Returns a match object if found, otherwise `None`.
      import re
      text = "The quick brown fox"
      match = re.match(r"The", text)
      if match:
          print("Match at the beginning")
      else:
          print("No match at the beginning")
      match = re.match(r"quick", text)
      if match:
          print("Match at the beginning")
      else:
          print("No match at the beginning")
    • `re.findall(pattern, string)`: Finds all occurrences of the pattern in the string and returns them as a list of strings.
      import re
      text = "The quick brown fox jumps over the lazy fox"
      matches = re.findall(r"fox", text)
      print(matches) # Output: ['fox', 'fox']
    • `re.finditer(pattern, string)`: Returns an iterator yielding match objects for all occurrences of the pattern.
      import re
      text = "The quick brown fox jumps over the lazy fox"
      for match in re.finditer(r"fox", text):
          print(match.start(), match.end())
    • `re.sub(pattern, replacement, string)`: Replaces occurrences of the pattern in the string with the `replacement` string.
      import re
      text = "The quick brown fox"
      new_text = re.sub(r"fox", "dog", text)
      print(new_text) # Output: The quick brown dog
    • `re.split(pattern, string)`: Splits the string into a list of substrings, using the pattern as the delimiter.
      import re
      text = "apple,banana,cherry"
      result = re.split(r",", text)
      print(result) # Output: ['apple', 'banana', 'cherry']
    • `re.compile(pattern)`: Compiles the pattern into a regex object, which can be used for multiple matching operations, improving efficiency.
      import re
      pattern = re.compile(r"\d+")
      text1 = "abc123def456"
      text2 = "ghi789jkl"
      match1 = pattern.search(text1)
      match2 = pattern.search(text2)
      print(match1.group(0)) # 123
      print(match2.group(0)) # 789

UNIT-V

  • Database Interaction: SQL Database Connection using Python, Creating and Searching Tables, Reading and storing config information on database, Programming using database connections.
  • SQL Database Connection using Python: Python can connect to various relational database management systems (RDBMS) using database connectors. Examples:
    • `sqlite3`: For working with SQLite databases (a lightweight, file-based database). Included in the Python standard library.
      import sqlite3
      conn = sqlite3.connect("my_database.db")
      cursor = conn.cursor()
    • `psycopg2`: For PostgreSQL.
      import psycopg2
      conn = psycopg2.connect(
          host="localhost",
          database="my_database",
          user="my_user",
          password="my_password")
      cursor = conn.cursor()
    • `mysql-connector-python`: For MySQL.
      import mysql.connector
      conn = mysql.connector.connect(
          host="localhost",
          user="my_user",
          password="my_password",
          database="my_database")
      cursor = conn.cursor()
    • `pyodbc`: For ODBC connections (can connect to various databases).
      import pyodbc
      conn = pyodbc.connect(
          "Driver={SQL Server};"
          "Server=my_server;"
          "Database=my_database;"
          "UID=my_user;"
          "PWD=my_password;")
      cursor = conn.cursor()
  • Creating and Searching Tables: Using SQL statements (e.g., `CREATE TABLE`, `SELECT`, `INSERT`, `UPDATE`, `DELETE`) within Python code to interact with the database. This involves:
    • Establishing a connection to the database.
    • Creating a cursor object, which allows you to execute SQL queries.
    • Executing SQL queries using the cursor's methods (e.g., `execute()`, `executemany()`).
      # Create table
      cursor.execute("""
          CREATE TABLE IF NOT EXISTS users (
              id INTEGER PRIMARY KEY,
              name TEXT,
              age INTEGER
          )
      """)
      # Insert data
      cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30))
      cursor.executemany("INSERT INTO users (name, age) VALUES (?, ?)", [("Bob", 25), ("Charlie", 35)])
      # Select data
      cursor.execute("SELECT * FROM users")
      rows = cursor.fetchall()
      for row in rows:
          print(row)
      # Update data
      cursor.execute("UPDATE users SET age = ? WHERE name = ?", (31, "Alice"))
      # Delete data
      cursor.execute("DELETE FROM users WHERE name = ?", ("Charlie",))
      conn.commit()
      
    • Fetching data from the result set (e.g., `fetchone()`, `fetchall()`).
    • Committing changes to the database.
    • Closing the cursor and the connection.
      conn.close()
  • Reading and storing config information on database: Databases can be used to store configuration settings for applications, instead of using flat files. This allows for easier management, querying, and updating of configuration data.
  • Programming using database connections: Building applications that interact with databases to store and retrieve data. This is a fundamental aspect of many software applications, including web applications, data-driven applications, and enterprise systems.
  • Python Multithreading: Understanding threads, Forking threads, synchronizing the threads, Programming using multithreading.
  • Understanding threads:
    • A thread is a lightweight unit of execution within a process. A process can have multiple threads running concurrently.
    • Multithreading allows a program to perform multiple tasks seemingly simultaneously, improving responsiveness and efficiency, especially for I/O-bound operations.
    • The Global Interpreter Lock (GIL) in CPython (the standard Python implementation) limits true parallelism for CPU-bound tasks. Only one thread can hold the GIL at a time. However, multithreading can still be beneficial for I/O-bound tasks, as the GIL is released when a thread is waiting for I/O.
  • Forking threads: Creating new threads using the `threading` module.
    import threading
    def my_function(arg1, arg2):
        print(f"Thread started with args: {arg1}, {arg2}")
        # Perform some task
    thread = threading.Thread(target=my_function, args=("hello", 123))
    thread.start()
    thread.join()  # Wait for the thread to finish
  • Synchronizing the threads: Ensuring that multiple threads can access shared resources without causing data corruption or race conditions. Techniques include:
    • Locks (`threading.Lock`, `threading.RLock`): A lock is a synchronization primitive that allows only one thread to acquire it at a time. Threads must acquire the lock before accessing shared resources and release it when they are done. RLock (reentrant lock) allows a thread to acquire the same lock multiple times.
      import threading
      lock = threading.Lock()
      def my_function(shared_resource):
          lock.acquire()
          try:
              # Access and modify the shared resource
              shared_resource += 1
          finally:
              lock.release()
    • Semaphores (`threading.Semaphore`): A semaphore manages a counter that represents the number of available resources. Threads can acquire a resource (decrement the counter) or release a resource (increment the counter).
      import threading
      semaphore = threading.Semaphore(3) # Allow 3 threads to access concurrently
      def my_function(resource):
          semaphore.acquire()
          try:
              # Use the resource
              print(f"Thread using resource: {resource}")
          finally:
              semaphore.release()
    • Conditions (`threading.Condition`): Allows threads to wait for a specific condition to become true. It's used with a lock.
      import threading
      condition = threading.Condition()
      shared_resource = 0
      def producer():
          global shared_resource
          condition.acquire()
          shared_resource = 1
          condition.notify() # Notify waiting consumer
          condition.release()
      def consumer():
          global shared_resource
          condition.acquire()
          while shared_resource == 0:
              condition.wait() # Wait for producer to notify
          print(f"Consumer got resource: {shared_resource}")
          condition.release()
    • Events (`threading.Event`): A simple synchronization object that can be set to `True` (signaled) or `False`. Threads can wait for the event to be set.
      import threading
      event = threading.Event()
      def worker():
          event.wait() # Wait until the event is set
          print("Worker received the signal")
      def master():
          # Perform some setup
          event.set() # Signal the worker thread
    • Queues (`queue.Queue`, `queue.LifoQueue`, `queue.PriorityQueue`): Thread-safe data structures that can be used for communication between threads.
      import queue
      q = queue.Queue()
      def producer():
          for i in range(5):
              q.put(i) # Put items in the queue
      def consumer():
          while not q.empty():
              item = q.get() # Get items from the queue
              print(f"Consumer got: {item}")
  • Programming using multithreading: Designing and implementing applications that use multiple threads to improve performance or handle concurrent tasks. Careful consideration is needed to avoid common multithreading problems like:
    • Race conditions: When the outcome of a program depends on the unpredictable order of execution of multiple threads.
    • Deadlocks: When two or more threads are blocked indefinitely, waiting for each other to release resources.
    • Starvation: When a thread is repeatedly denied access to a resource and cannot make progress.

</html>