andersch.dev

<2024-10-26 Sat>

Python

Python is a dynamically-typed, interpreted programming language. It has gained widespread use due to its resemblance to pseudo-code and numerous libraries.

Toolchain

Python Package Managers (pip, uv)

Pip Installs Packages (pip):

  • most widely used package manager
  • Comes pre-installed with Python >3.4
  • Installs packages from the Python Package Index (PyPI)
  • Usage: pip install package_name

virtualenv

  • tool for creating isolated Python environments
  • Allows managing of dependencies for projects separately
  • Usage: virtualenv env_name and source env_name/bin/activate (activate virtenv)

pipenv

  • Combines pip and virtualenv
  • Uses a Pipfile to specify dependencies and a Pipfile.lock for version locking.
  • Usage: pipenv install package_name and pipenv shell (Activate virtenv)

uv

  • Fast single-binary Python package manager and resolver written in Rust
  • Drop-in replacement for pip, pip-tools, virtualenv
  • Handles virtual environments natively
  • unified CLI for tasks like uv pip install, uv venv, or uv python
  • Manages projects with a pyproject.toml file
  • uvx (alias for uv tool run) for executing scripts in isolated environment

Compiling Python (Cython, PyPy)

CPython

  • Standard interpreter that can compile python to bytecode
  • Runs python line-by-line via the Python Virtual Machine (PVM)
  • To compile Python to bytecode: python -m py_compile source.py
  • Compiled bytecode lands in the __pycache__ directory.
  • To execute it from the command line: python -m source
  • Python by default is not compiled to an .exe or JIT-compiled to machine code.
  • However, there are tools to compile python code.

Cython:

  • translates Python code to C/C++ code
  • supports calling C functions and declaring C types

PyPy

  • Python implementation with a JIT compiler.
  • Runtime optimisations, fully language compliant
  • Can run most Python code, except for CPython extensions
  • PyPy's meta-tracing toolchain is called RPython.
  • Uses meta-tracing: interpreter as input and a tracing JIT compiler as output

Type Checking in Python (mypy, ty)

Python supports type hinting since version 3.5:

import typing

# primitive type hints
def add(a: int, b: int) -> int:
    return a + b
x : int = add(4,5)
print(x)

# type hints for lists (or tuples, dicts)
def get_floats(input : list[float]) -> list[float]:
    floats : list[float] = [3.4, 2.8, 2.5, 3.9]
    result = [input[i] + floats[i] for i in range(len(input))]
    return result
print(get_floats([3.5,2.8,3.1]))

# union type hints (allow more than one type)
def sum_ab(a: int | float, b: int | float) -> int | float:
    return a + b

Python will not check the types by default. Instead, the types can be statically checked before running the program by a tool like mypy or ty.

Concepts

Fundamentals

Object:

  • Everything in Python is an object
  • Including integers, strings, lists, functions, classes and class instances

Attributes:

  • An attribute is a value associated with an object
  • Instance Attributes are specific to an instance of a class.
  • Class Attributes are shared among all instances (static variables)
  • Module Attributes are defined at the top level

Syntax

my_list = [1, 2, 3]         # declaring list syntax
my_dict = {'key': 'value'}  # declaring dict syntax

# unpack/unwrap operator
def add(a, b, c):
    return a + b + c
iterables = [1,2,3]
print(add(*iterables)) # same as add(1,2,3)

# list comprehensions
squares = [x**2 for x in range(4)] # = [0,1,4,9]

# slicing
sub_list = squares[1:3]  # = [1,4]

add = lambda x, y: x + y  # lambda functions

name = "Alice" # normal string
greeting = f"Hello, {name}!"  # formatted string (f-string)
# imports
import math                    # import module
from datetime import datetime  # import specific function/class
import module as alias         # import module with alias
from module import *           # import all names from a module

# if else
if x > 0:    # conditional
elif x < 0:  # else if condition
else:        # else statement

def my_function():  # function definition
    return 42       # return value
    pass            # null operation (placeholder)

class MyClass:      # class definition

# loops
for i in range(5):  # for loop
for _ in range(n):  # for loop with unused n
while x > 0:        # while loop
break               # exit loop
continue            # skip to next iteration

# exceptions
try:                       # Start of try block
    # ...
except ZeroDivisionError:  # Handle specific exception
    # ...
finally:                   # runs no matter what
    # ...
raise Exception("Err")     # raise exception

with open('file.txt', 'r') as file:  # scoped resource management

global x    # declare global variable
nonlocal y  # declare non-local variable in nested function

assert x == 4  # assert statement

# async
async def my_coroutine():               # define asynchronous function
    await some_async_function()         # await asynchronous call
async with some_async_context_manager:  # async scoped resource manager
    # ...

del x  # Delete a variable or object

# generators
yield value                     # yield value from generator function
yield from another_generator()  # Yield all values from another generator

if __name__ == "__main__":      # check if script is run directly

Decorators

def my_decorator(func):
    def wrapper():
        print("Decorator start.")
        func()
        print("Decorator end.")
    return wrapper

@my_decorator
def say_hello():
    print("Decorated Function")
say_hello()

Generators

A generator is a special type of iterator that allows iteration through a sequence of values. Each time the yield statement of a generator is executed, the function's state is saved, and it can be resumed later.

def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

fib_gen = fibonacci(4) # get generator object

for number in fib_gen: # using the generator
    print(number)

Exceptions

try:
    numerator = 10
    denominator = 0
    result = numerator / denominator
except ZeroDivisionError:
    print("Error: Cannot divide by zero.")
else:
    print("Result:", result)
finally:
    print("Execution completed.") # runs no matter what

Built-In Functions

len()       # Returns length (number of items) of object
type()      # Returns type of object.

# data structures
list() tuple() set() dict()

# math
max() min() sum() abs() round() pow(base, exp) divmod(a, b)
complex(real, imag) # create complex number

# operations
zip()       # Combines elements from multiple iterables into tuples.
map()       # Apply function to iterable and return map
filter()    # Construct iterator from elements of an iterable for which a function returns true.
all()       # Return True if all elements of an iterable are true
any()       # Returns True if any element of an iterable is true. If the iterable is empty, returns False.

# attributes
getattr(obj, name)       # retrieve attribute from object
hasattr(obj, name)       # check if object has specified attribute
delattr(obj, name)       # delete attribute from object
setattr(obj, name, val)  # set attribute on object
property()               # create property attribute

# special attributes
__doc__         # attribute that stores a docstring
__loader__      # attribute for the module loader object
__name__        # attribute that stores name of module
__package__     # attribute that stores package name of module
__spec__        # attribute that stores module's import specification

# interpreter
compile(src, file, mode) # compile source code into code object
eval(expr)               # evaluate python expression from string
exec(object)             # execute python code dynamically
exit()                   # exit interpreter
quit()                   # exit interpreter
breakpoint()             # drop into the debugger at the call site