Skip to main content

Introduction To Python

Disclaimer

This is not an exotic list of important nor basic python features, it is your own responsibility and you can always access internet during labs.

Style Guide

Types in Python

Python type system is unlike other languages you might have encountered, everything in python is an object. That object carries:

- Type
- Reference count
- Value

Read more about garbage collection in python and basics of Memory Management in Python here.

Type hinting

Python is a dynamically typed language, which means that you don't have to declare the type of the variable when you create one. The interpreter infers the type of the variable based on the value it is assigned. This is unlike statically typed languages like C, C++, Java, etc. where you have to declare the type of the variable when you create one.

Type hinting is a way for developers to add metadata to a codebase to make it easier to perform static analysis during development. Some have speculated that Python type hinting could in time give rise to a fork of the language that is statically typed, perhaps as a way to make Python faster. Plus, it hugely improves the readability of your code.

Variables and Constants

from typing import Final

COURSE_NAME: Final[str] = "CMPS446"
var_one: int = 3
var_two: bool = True
var_three: str = "Hello World"
print(var_one, var_two, var_three)
var_four: str = "your favv TA"
print(f"{COURSE_NAME}: {var_three}, by {var_four} :)")
3 True Hello World
CMPS446: Hello World, by your favv TA :)

from typing import Optional
x: Optional[int] = None
print('x is None' if x is None else 'x is not None')
x is None

Printing in Python

Printing to file

Consider running Python Basic Syntax.ipynb inside the following code structure:

(.venv) ziadh@Ziads-MacBook-Air lab-one-sol % tree  
.
├── Lab_one_std.ipynb
├── Lab_one_ta.ipynb
├── Python Basic Syntax.ipynb
├── README.md
├── data
│ ├── coffee.jpeg
│ ├── histogram
│ │ ├── ex1.jpg
│ │ ├── ex2.png
│ │ └── ex3.png
│ ├── hsv
│ │ ├── ex1.png
│ │ ├── ex2.jpg
│ │ └── ex3.jpg
│ └── pyramids.jpeg
├── lab-one-std.zip
├── materials
│ ├── Image Processing - Lab - Intro to Python and Jupyter.pdf
│ ├── Lab1-HSV-Noise-Histogram.pptx
│ ├── Lab1-Intro to Image Processing in Python.pptx
│ └── Lab1.pdf
├── output
│ └── output.log
└── requirements.txt

5 directories, 19 files
"""Printing to file in Python
Example logging to a file
"""
import os
from pathlib import Path

BASE_PATH: Final[Path] = Path().resolve()
log_file_path: Final[Path] = os.path.join(BASE_PATH, "output", "output.log")

def log_to_file(file_path: Path, message: str) -> None:
with open(file_path, "w") as f:
import datetime
timestamp: datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message: str = f"[{timestamp}] {message}"
print(log_message, file=f)

log_to_file(log_file_path, "Hello World!")
[2023-10-04 01:39:18] Hello World!

Operators in Python

Division

# division in python
print(5/2)

# floor division or integer division
print(5 // 2)
2.5
2

Looping in Python

array: list[int] = [1, 2, 3, 4, 5]

for num in array:
print(num)
1
2
3
4
5

for i in range(5):
print(i)
0
1
2
3
4

# range(start, stop, step)
for i in range(0, 1_000_000, 10_000):
print(f"{i:,}")
0
10,000
20,000
30,000
...
970,000
980,000
990,000

for num in array:
if num%2 == 0:
print(num)

# Usage of list comprehension
even_items: list[int] = [num for num in array if num%2 == 0]
print(even_items)
print(*even_items)
print(*even_items, sep=', ')
2
4
[2, 4]
2 4
2, 4

Functions in python

Inline functions

from typing import Callable

add: Callable[[int, int], int] = lambda x, y: x + y
result: int = add(3, 5)
print(result)
8

Decorators in Python

A decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

import time
from typing import Callable
from pathlib import Path

BASE_PATH: Final[Path] = Path().resolve()
log_file_path: Final[str] = os.path.join(BASE_PATH, "output", "tune.log")

def log_to_file(file_path: str, message: str) -> None:
with open(file_path, "w") as f:
import datetime
timestamp: datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message: str = f"[{timestamp}] {message}"
print(log_message, file=f)

def timeit(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
log_to_file(log_file_path, f"Started {func.__name__}...")
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
log_to_file(log_file_path, f"Done {func.__name__} took {end_time - start_time:,} seconds to execute.")
return result
return wrapper

@timeit
def add(a: int, b: int) -> int:
time.sleep(10)
return a + b

add(1, 2)
[2023-10-04 03:12:59] Done add took 10.0048 seconds to execute.

Passing arguments to Functions

Formal parameters are mentioned in the function definition. Actual parameters(arguments) are passed during a function call. We can define a function with a variable number of arguments.

default arguments

Default arguments are values that are provided while defining functions. The assignment operator = is used to assign a default value to the argument. Take care of mutable Data Structures. Default arguments become optional during the function calls. If we provide a value to the default arguments during function calls, it overrides the default value. The function can have any number of default arguments. Default arguments should follow non-default arguments.

def add(a: int, b: int = 5, c: int = 10) -> int:
return a+b+c
print(add(3))
# Output:18
print(add(3, 4))
# Output:17
print(add(2, 3, 4))
# Output:9

Note: Default values are evaluated only once at the point of the function definition in the defining scope. So, it makes a difference when we pass mutable objects like a list or dictionary as default values.

keyword arguments

Functions can also be called using keyword arguments of the form kwarg=value. During a function call, values passed through arguments need not be in the order of parameters in the function definition. This can be achieved by keyword arguments. But all the keyword arguments should match the parameters in the function definition.

def add(a: int, b: int = 5, c: int = 10) -> int:
return a+b+c
print(add(b=10, c=15, a=20))
# Output:45
print(add(a=10))
# Output:25

positional arguments

During a function call, values passed through arguments should be in the order of parameters in the function definition. This is called positional arguments. Keyword arguments should follow positional arguments only.

def add(a: int, b: int, c: int) -> int:
return a+b+c
print(add(10, 20, 30))
# Output:60
print(add(10, c=30, b=20)) # Mix of positional and keyword arguments, keyword arguments should always follow positional arguments
# Output:60

Variable-length arguments

Variable-length arguments are also known as arbitrary arguments. If we don’t know the number of arguments needed for the function in advance, we can use arbitrary arguments.

🤓 arbitrary positional arguments

For arbitrary positional argument, an asterisk (*) is placed before a parameter in function definition which can hold non-keyword variable-length arguments. These arguments will be wrapped up in a tuple. Before the variable number of arguments, zero or more normal arguments may occur.

def add(*args: list[int]) -> int:
result: int = 0
for i in args:
result = result + i
return result

print(add(1, 2, 3, 4, 5))
# Output:15
print(add(10, 20))
# Output:30

🤓 arbitrary keyword arguments

For arbitrary positional argument, a double asterisk (**) is placed before a parameter in a function which can hold keyword variable-length arguments.

from typing import Any

def fn(**kwargs: dict[str, Any]) -> None:
for i in kwargs.items():
print(i)
fn(numbers=5, colors="blue", fruits="apple")
'''
Output:
('numbers', 5)
('colors', 'blue')
('fruits', 'apple')
'''

Special Parameters

By default, arguments may be passed to a Python function either by position or explicitly by keyword. For readability and performance, it makes sense to restrict the way arguments can be passed so that a developer need only look at the function definition to determine if items are passed by position, by position or keyword, or by keyword.

def func(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
"""pos1, pos2: Positional only parameters
pos_or_kwd: Can be passed by position or keyword
kwd1, kwd2: Keyword only parameters
"""
pass
# Example
def add(a: int, b: int, /, c:int, d: int, *, e: int, f: int) -> int:
return a + b + c + d + e + f
print(add(3, 4, 5, 6, e=7, f=8))
# Output:33

where / and * are optional. If used, these symbols indicate the kind of parameter by how the arguments may be passed to the function: positional-only, positional-or-keyword, and keyword-only.

🤓 Positional or keyword arguments

If / and * are not present in the function definition, arguments may be passed to a function by position or by keyword.

def func(a: int, b: int, c: int) -> int:
return a + b + c
print(add(3, 4, 5))
# Output:12
print(add(3, c=1, b=2))
# Output:6

🤓 Positional only arguments

Positional-only parameters are placed before a / (forward-slash) in the function definition. The / is used to logically separate the positional-only parameters from the rest of the parameters. Parameters following the / may be positional-or-keyword or keyword-only.

def add(a: int, b: int, /, c:int, d: int) -> int:
return a + b + c + d
print(add(3, 4, 5, 6))
# Output:12
print(add(3, 4, c=1, d=2))
# Output:6
print(add(3, b=4, c=1, d=2))
# Output:TypeError: add() got some positional-only arguments passed as keyword arguments: 'b'

🤓 Keyword only arguments

To mark parameters as keyword-only, place an * in the arguments list just before the first keyword-only parameter.

def add(a: int, b: int, *, c:int, d: int) -> int:
return a + b + c + d
print(add(3, 4, c=1, d=2))
# Output:10
print(add(3, 4, 1, d=2))
# Output:TypeError: add() takes 2 positional arguments but 3 positional arguments (and 1 keyword-only argument) were given.

Usage

  • Use positional-only if you want the name of the parameters to not be available to the user. This is useful when parameter names have no real meaning.
  • Use positional-only if you want to enforce the order of the arguments when the function is called.
  • Use keyword-only when names have meaning and the function definition is more understandable by being explicit with names.
  • Use keyword-only when you want to prevent users from relying on the position of the argument being passed.

Numpy

Generating NumPy arrays

import numpy as np
zeros_array: np.ndarray[int] = np.zeros(shape=(6, 5), dtype=int)
ones_array: np.ndarray[int] = np.ones(shape=(6, 5), dtype=int)

print(zeros_array)
print(ones_array)
print(ones_array.shape)
print(ones_array.shape[0])
[[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]]
[[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]]
(6, 5)
6

Array Indexing

array: list[int] = [0, 1, 2, 3, 4, 5]
print(array[:3])

second_array: np.ndarray[int] = np.array(array)
third_array: np.ndarray[int] = np.copy(second_array)

print(third_array)
third_array[:3] = 0
print(third_array)

third_array: np.ndarray[int] = np.copy(second_array)
third_array[third_array%2==0] = -1
print(third_array)

third_array: np.ndarray[int] = np.copy(second_array)
third_array[(third_array%2==0) & (third_array==2)] = -1
print(third_array)
[0, 1, 2]
[0 1 2 3 4 5]
[0 0 0 3 4 5]
[-1 1 -1 3 -1 5]
[ 0 1 -1 3 4 5]

original_matrix: np.ndarray[list[int]] = np.array([
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5]
])

copied_matrix: np.ndarray[list[int]] = np.copy(original_matrix)
print(copied_matrix)

copied_matrix[1:3, 1:3] = 0
print(copied_matrix)
[[1 2 3 4 5]
[1 2 3 4 5]
[1 2 3 4 5]]
[[1 2 3 4 5]
[1 0 0 4 5]
[1 0 0 4 5]]

Extra Useful Talks

Always keep yourself updated with each new python release changes.

REFERENCES