Published on

Python Learning Journey

Table of Contents

Built In Constants

Maximum and Minimum Integer Values

1.sys.maxint - The maximum integer value that can be stored in a Python integer variable. 2.sys.minint - The minimum integer value that can be stored in a Python integer variable.

Keywords

  1. with: The with keyword in Python is used for resource management and exception handling, particularly when working with unmanaged resources like file streams.1

The key points about the with statement in Python are:

  1. Resource Management: The with statement ensures that a resource (e.g., a file, a lock, a database connection) is properly acquired and released, even in the presence of exceptions. This helps avoid resource leaks and ensures that the resource is always properly cleaned up.

  2. Exception Handling: The with statement simplifies the use of try-finally blocks, which are commonly used to ensure that a resource is properly released, regardless of whether an exception is raised or not.

  3. Syntax: The with statement is used as follows:

with <context_expression> as <target>:
    <suite>

The <context_expression> is an expression that returns a context manager object, which must have __enter__() and __exit__() methods. The <target> is an optional name that is bound to the object returned by the __enter__() method.

  1. Context Managers: Objects that support the with statement are called "context managers". They provide the __enter__() and __exit__() methods, which are called when the with block is entered and exited, respectively.

  2. Examples: The most common use of the with statement is with file handling, where it ensures that the file is properly closed, even if an exception is raised:

with open('file.txt', 'r') as f:
    data = f.read()

The with statement can also be used with other context managers, such as locks, database connections, and custom context managers. In summary, the with keyword in Python is a powerful tool for resource management and exception handling, making code more concise, readable, and less prone to resource leaks.

  1. RunnablePassThrough(): The RunnablePassthrough() component is part of the LangChain library and is used to handle the input to the RAG (Retrieval-Augmented Generation) chain. Here's a breakdown of what RunnablePassthrough() does and the implications of not using it:

    Purpose of RunnablePassthrough():

    The RunnablePassthrough() component is a utility class in LangChain that allows you to pass the input question directly to the next step in the chain, without any additional processing. It is often used in the context of a RAG chain, where the input question needs to be passed to the retriever and the language model without any modifications. What happens if you don't use RunnablePassthrough(): If you don't use RunnablePassthrough() in the provided code, the input question would not be passed directly to the next step in the chain. Instead, the input question would need to be handled by a separate component, such as a custom prompt template or a custom input processing function.

Naming conventions:

Python has the following method naming conventions:

  1. Single Underscore Before a Name (_name): This is a convention to indicate that the name is intended for internal use and should be treated as "private". It's a way to signal to other programmers that the name is an implementation detail and should not be accessed directly from outside the class. However, this is just a convention, and Python does not enforce true privacy. The name can still be accessed from outside the class if needed.

  2. Double Underscore Before a Name (name): This is not a convention, but a specific language feature called "name mangling". Python automatically renames these methods to _ClassName_name to avoid naming conflicts with subclasses. This is used to create a form of name obfuscation and to make it harder for subclasses to accidentally override these methods. These "name-mangled" methods are still accessible, but the mangled name should be used to access them.

  3. Double Underscore Before and After a Name (name): These are special "magic" or "dunder" (double underscore) methods in Python. They are part of the Python language and have specific meanings and behaviors. Examples include init, str, len, etc. These methods are called by the Python interpreter in specific contexts. You can also define your own "magic" methods, but it's generally recommended to avoid this unless you have a specific reason, as it can lead to confusion.

In summary, the underscores before method names in Python are used for different purposes: Single underscore (_name) is a convention for "private" methods. Double underscore (**name) is a language feature for name mangling to avoid naming conflicts. Double underscore before and after (**name__) are special "magic" or "dunder" methods.

Here are examples for the different uses of underscores in Python method names:

Single Underscore (_name):

class MyClass:
    def __init__(self):
        self._internal_variable = 42

    def _internal_method(self):
        print("This is an internal method.")

obj = MyClass()
obj._internal_method()  # Access is possible, but discouraged
print(obj._internal_variable)  # Access is possible, but discouraged

In this example, the _internal_method and _internal_variable are marked with a single underscore to indicate that they are intended for internal use and should be treated as "private" by other parts of the code.

Double Underscore (__name):

class ParentClass:
    def __private_method(self):
        print("This is a private method in the parent class.")

class ChildClass(ParentClass):
    def __private_method(self):
        print("This is a private method in the child class.")

parent = ParentClass()
parent._ParentClass__private_method()  # Access the parent's private method

child = ChildClass()
child._ChildClass__private_method()  # Access the child's private method

In this example, the __private_method methods are "name-mangled" by Python to avoid naming conflicts between the parent and child classes. The mangled names can be accessed using the _ClassName__method_name syntax.

Double Underscore Before and After (__name__):

class MyClass:
    def __init__(self):
        self.name = "MyClass"

    def __str__(self):
        return f"Instance of {self.name}"

    def __len__(self):
        return 42

obj = MyClass()
print(obj)  # Output: Instance of MyClass
print(len(obj))  # Output: 42

In this example, the __init__, __str__, and __len__ methods are special "magic" or "dunder" methods in Python. These methods are called by the Python interpreter in specific contexts, such as when creating an instance of the class, printing the object, or using the len() function on the object.

Packages and Modules

Based on the search results provided, here are the key points on when to use from package import module vs import module in Python:

  1. Importing Modules vs Packages:

    • You can only import modules, not packages23. Packages are just containers for modules or sub-packages.
    • When you "import" a package, you are actually importing the __init__.py module within the package23.
  2. Using from package import module:

    • The from package import module syntax allows you to directly access the module's contents without having to use dot notation (e.g., package.module.function())243.
    • This is useful when you only need to access specific modules or functions from a package, as it makes the code more concise24.
  3. Using import module:

    • The import module syntax allows you to access the module's contents using dot notation (e.g., module.function())243.
    • This is useful when you need to access multiple modules or functions from a package, as it keeps the namespace cleaner and avoids naming conflicts24.
  4. Aliasing Modules:

    • You can use the as keyword to assign an alias to a module when importing it (e.g., import math as m)45.
    • This is helpful when you want to shorten the module name or avoid naming conflicts with other modules45.

In general, you should use from package import module when you only need to access specific modules or functions from a package, and import module when you need to access multiple modules or functions from a package. Aliasing modules can be useful in both cases to make the code more readable and maintainable.

Libraries and built-in modules

  1. bisect.bisect_left, bisect.insort_left
  2. enumerate()
  3. any()
  4. lambda
  5. heapify

ORD

The ord() function in Python is a built-in function that returns the Unicode code point of a given character. It takes a single character (a string of length 1) as an argument and returns its corresponding integer Unicode value.

Key points about ord():

  • Purpose: It converts a character to its numerical representation based on the Unicode standard.

  • Input: It requires a single character string. Providing a string with more than one character will result in a TypeError.

  • Output: It returns an integer representing the Unicode code point of the input character.

  • Inverse of chr(): The ord() function is the inverse of the chr() function, which converts an integer Unicode code point back to its corresponding character.

  • Use Cases: It is particularly useful in scenarios involving character encoding/decoding, text processing, cryptography, and custom sorting based on character values.

Example:

print(ord('A'))  # Output: 65
print(ord('a'))  # Output: 97
print(ord('€'))  # Output: 8364

Sorting a dictionary

my_dict = {'apple': 3, 'orange': 1, 'banana': 2}

Method 1: Get sorted keys and then build a new dictionary

sorted_keys = sorted(my_dict.keys())
sorted_dict_by_keys = {key: my_dict[key] for key in sorted_keys}
print(sorted_dict_by_keys)

Method 2: Sort items and convert back to dictionary

This directly creates a new dictionary with items sorted by key:

sorted_dict_by_keys_items = dict(sorted(my_dict.items()))
print(sorted_dict_by_keys_items)

Counter

The Counter in Python is a specialized dictionary subclass found within the collections module. It is designed for efficiently counting hashable objects within an iterable.

Key Characteristics and Usage:

  • Counting Occurrences: It takes an iterable (like a list, string, or tuple) and creates a dictionary-like object where keys are the unique elements from the iterable and values are their respective counts.

  • Subclass of dict: Counter inherits from dict, meaning it behaves largely like a standard dictionary. You can access counts using keys, iterate through items, keys, or values, and use methods like update().

  • Handling Missing Keys: Unlike a regular dictionary, accessing a key that is not present in a Counter object will return a count of 0 instead of raising a KeyError.

  • most_common() Method: This method returns a list of the n most common elements and their counts, ordered from most frequent to least frequent.

  • elements() Method: This method returns an iterator that yields each element as many times as its count.

  • Arithmetic Operations: Counter objects support basic arithmetic operations like addition, subtraction, intersection, and union, allowing for set-like operations on counts.

Example:

from collections import Counter

# Counting elements in a list
my_list = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
counts = Counter(my_list)
print(counts)  # Output: Counter({'apple': 3, 'banana': 2, 'orange': 1})

# Accessing a count
print(counts['apple'])  # Output: 3

# Handling a missing key
print(counts['grape'])  # Output: 0

# Finding the most common elements
most_common_items = counts.most_common(2)
print(most_common_items)  # Output: [('apple', 3), ('banana', 2)]

Deque

collections.deque (pronounced "deck") in Python is a list-like container that provides efficient appending and popping of elements from both ends of the sequence. It is a double-ended queue, which means it can function as both a stack (Last-In, First-Out) and a queue (First-In, First-Out).

Key Features and Advantages:

  • Efficient Operations at Both Ends: Unlike standard Python lists, which have O(n) time complexity for inserting or removing elements from the beginning, deque offers O(1) time complexity for append(), appendleft(), pop(), and popleft() operations. This makes it ideal for scenarios requiring frequent additions or removals from either end.

  • Memory Efficiency: deque is designed to be memory-efficient, especially when dealing with large sequences and frequent modifications.

  • Thread Safety: Append and pop operations on a deque are thread-safe, making it suitable for concurrent programming.

  • Fixed-Size Deque (maxlen): You can create a deque with a maxlen parameter, which automatically discards elements from the opposite end when the maximum length is reached. This is useful for implementing fixed-size buffers or sliding window algorithms.

Common Operations:

Creation:

from collections import deque
d = deque([1, 2, 3])

Adding elements:

d.append(4)         # Adds to the right: deque([1, 2, 3, 4])
d.appendleft(0)     # Adds to the left: deque([0, 1, 2, 3, 4])

Removing elements:

right_element = d.pop()       # Removes from the right: 4
left_element = d.popleft()    # Removes from the left: 0
  • Other Operations: extend(), extendleft(), rotate(), clear(), count(), remove().

Use Cases: Implementing queues and stacks, sliding window problems in algorithms, managing recent history or logs with a fixed size, and producer-consumer patterns in concurrent programming.

Defaultdict

In Python, a defaultdict is a specialized dictionary that returns a default value for missing keys, making data handling easier and more robust. Python does not have built-in concepts of "default depth" for lists or sets, but depth usually refers to nesting levels in lists. Here is a breakdown with examples and explanations for each concept.

Defaultdict in Python

A defaultdict lets you specify a type or function that produces default values for keys that don't exist. For example:

from collections import defaultdict

d = defaultdict(list)
d['fruits'].append('apple')
d['vegetables'].append('carrot')

print(d['fruits'])     # ['apple']
print(d['juices'])     # []

Here, d['juices'] returns an empty list instead of causing an error, because we initialized the dict with list as its default factory.

List Depth (Nesting Level)

"Default depth" of a list is not a standard term, but depth usually means how many levels of nested lists exist in a structure. For example:

my_list = [1, [2, [3, [4]]]]

# Function to get max depth
def list_depth(lst):
    if isinstance(lst, list):
        if len(lst) == 0:
            return 1
        return 1 + max(list_depth(item) for item in lst)
    else:
        return 0

print(list_depth(my_list))  # 4

In Python, defaultdict(list) creates a dictionary that gives an empty list as the default value for any new key, while defaultdict(set) provides an empty set as the default for missing keys. Both make it easy to append or add items to new keys without manual checks or initialization.

defaultdict(list)

  • When accessing a non-existent key, an empty list [] is automatically created.
  • It's ideal for grouping or collecting items by key.

Example:

from collections import defaultdict

d = defaultdict(list)
d['fruits'].append('apple')
d['fruits'].append('banana')
d['vegetables'].append('carrot')

print(d)
# Output: defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})

If d['unknown'] is accessed, it simply returns [] instead of raising a KeyError.

defaultdict(set)

  • When accessing a non-existent key, an empty set set() is automatically created.
  • Useful for collecting unique items by key.

Example:

from collections import defaultdict

d = defaultdict(set)
d['fruits'].add('apple')
d['fruits'].add('banana')
d['fruits'].add('apple')  # Won't be duplicated
d['vegetables'].add('carrot')

print(d)
# Output: defaultdict(<class 'set'>, {'fruits': {'apple', 'banana'}, 'vegetables': {'carrot']})

Citations:

Footnotes

  1. https://stackoverflow.com/questions/1369526/what-is-the-python-keyword-with-used-for

  2. https://note.nkmk.me/en/python-import-usage/ 2 3 4 5 6

  3. https://realpython.com/python-modules-packages/ 2 3 4

  4. https://www.geeksforgeeks.org/import-module-python/ 2 3 4 5 6

  5. https://www.digitalocean.com/community/tutorials/how-to-import-modules-in-python-3 2