Python Constructs

From NoskeWiki
Jump to navigation Jump to search

About

NOTE: This page is a daughter page of: Python


Here are some python constructs that are useful to memorize:


Python Constructs

Basic Loops: for & while

There is no `do while` in python, but they have for and while in this format:

for i in range(10):   # If just one arg, starts at 0.
  print(i)            # Prints 0 up to 9 (10 not included).

for i in range(10, 0, -1):  # From 10 to 0... incrementing by -1. 
  print(i)                  # Prints 10 down to 1 (0 not included).

nums = ['a','b','c']
for num in nums:
  print(num)                  # Prints 'a' to 'c'.


low, high = 0, 5
while low < high:
  print('range: ', low, '-', high)
  low += 1


Enumerated For Loop

for (i, item) in enumerate(item_arr):
  if i == 0:
    print('This is first row')
  print(str(item))


Imports and Typecasts

Import in the syntax: `from typing import List`...

The optional typecast in the syntax: `MAX_INT: int = sys.maxsize`

from typing import List  # Actually from v 3.9 you can just say "list"

def list_to_string(arr: List[str], max_items: int = 100) -> str:   # Type cast here.
  """Turns a list of strings into a single comma-separated double-quoted string.

  NOTE: If you just wanted to print: `print(arr)` does the job,
  and `','.join(arr)` produces a nice list too.
  """
  return_str = ''
  for i in range (0, len(arr)):
    if i > 0:
      return_str += ', '
    if i >= max_items:
      return_str += '...'
      break
    return_str += '"' + arr[i] + '"'
  return return_str

if __name__ == '__main__':
  arr = ['a', 'b', 'c', 'd', 'e']
  print(list_to_string(arr, 3))  # Prints:   "a", "b", "c", ...


List (Array) Operations

Python calls its array a "list".

arr1: list[int] = [1,4,6,1]     # Init array... we could also have done `arr1: []` (to start it empty).
arr1.pop()            # Pops the last element (which is a 1).
arr1.append(2)        # Adds a 2.
print(arr1)           # Prints: `[1, 4, 6, 2]`

arr1.extend([3,5])    # Adds this array to the end.
arr1.sort()           # Sorts.
print(arr1)           # Prints `[1, 2, 3, 4, 5, 6]`.
print(arr1.index(3))  # Prints `2` (the index of '3')
print(sum(arr1))      # Prints `` (sum of all number)

# Sort using lambda:
people = [('Alice', 30), ('Bob', 25), ('Charlie', 35), ('Dana', 28)]  # List of tuples with name & age.
sorted_people = sorted(people, key=lambda person: person[1])          # Sorts by age.
print(sorted_people)                                                  # Bob will be first (lowest age).

# NOTE: If we had a class we could do: `sorted(people, key=lambda person: person.age, reverse=True)`.

Complete list of string functions include:

  • append() - Adds an element at the end of the list.
  • clear() - Removes all the elements from the list.
  • copy() - Returns a copy of the list.
  • count() - Returns the number of elements with the specified value.
  • extend() - Add the elements of a list (or any iterable), to the end of the current list.
  • index() - Returns the index of the first element with the specified value.
  • insert() - Adds an element at the specified position.
  • pop() - Removes the element at the specified position.
  • remove() - Removes the first item with the specified value.
  • reverse() - Reverses the order of the list.
  • sort() - Sorts the list.

For two dimensional arrays (think list[list[int]]):

width, height = 3, 4
small_board = [[x for x in range(width)] for _ in range(height)]
print(small_board)        # Prints `[[0,1,2], [0,1,2], [0,1,2], [0,1,2]]`,
print(small_board[3][1])  # Prints `1` --> y=3 (last row 4), x=1 (col 2).

chess_board = [['-'] * 8] * 8    # Create multiple of the same element with '*'.
print(chess_board)               # Empty grid of 8x8 '-' chars... where chess_board[y][x].


String Operations

arr = 'mon tue wed'.split(' ')        # Splits by whitespace (the default anyway).
arr_string = ','.join('arr')          # Becomes "mon,tue,wed".

path = '/../folder'
if path.startswith('/../'):           # Is true
  path = path.replace('/../', '/')    # Replaces just first occurance > '/folder'.

multiline_string = '''Store: Jeff's Burger House
Address: 30 Jeff Street
Famous for = "Smash burgers"
'''
print(multiline_string)  # NOTE: Ends in a newline, and our ' and " are nicely formatted.
# NOTE: Multiline strings also serve as docstrings.

Complete list of string functions include:

  • capitalization stuff: lower(), upper(), capitalize(), casefold(), swapcase(), title()
  • reduction: strip(), rstrip(), lstrip()
  • matching: startswith(), endswith(), ... isalnum(), isalpha(), isascii(), isdecimal(), isdigit(), isidentifier(), islower(), isnumeric(), isprintable(), isspace(), istitle(), isupper()
  • find/replace: replace(), find(), rfind(), index(), rindex(), count() # of times a specified value occurs
  • formatting: format(), format_map(), ljust(), rjust(), zfill()
  • array: join(), split(), splitlines(), rsplit()
  • misc: encode() encoded version of the string expandtabs() sets tab size maketrans() translation table, translate() returns a translated string, partition(), rpartition(), center()

What is very powerful is using formatted string literals with f'Hi {name}, you owe us ${money:10.2f}'

tax_rate, tax_owed = 0.085, 12.3421
print(f"{'Tax ' + str(tax_rate*100) + '%':30} ${tax_owed:10.2f}")  # f = float & .2 = decimals.
# Prints: `Tax 8.5%                       $     12.34` ... :30 does right fill, :10 is left fill.

See more examples.

Sets

Sets are declared like this: `set(<iterable>)`:

days_set = set(['mon', 'tue', 'wed', 'wed', 'thu', 'wed'])
days_set.add('fri')            # To add more... the other one you want is `remove()` and `clear()`.
print(days_set)                # {'wed', 'thu', 'mon', 'tue', 'fri'}  -> is unsorted.
print('mon' in days_set)       # true.
print('janurary' in days_set)  # false.

numbers_set = set(range(1, 11))  # Initializes set with numbers 1,2,3,4,5,6,7,8,9,10. (11 not included).

Dicts (Map)

The concept of a dict is the same as a map in other languages (like c++). An example:

# This dictionary represents a mix of types.
car = {
  "brand": "Toyota",
  "model": "Landcruise",
  "year": 1983  # You can put anything in here, such as a list or another dict.
}
car_extra_info = {
  "owner": "Andrew Noske"
}

car_info = {**car, **car_extra_info}  # You can merge dicts... in this case it adds the owner.
print(car_info)                       # Prints a dict nicely.

# Using zip(), you can create a dictionary from a list of keys and a list of values:
keys = ['k1', 'k2', 'k3']
values = [1, 2, 3]
d: dict[str,int] = dict(zip(keys, values))  # The `dict[str,int]` isn't necessary, but helps ensure typecasting.
print(d)

# Programmatically adding to a dict:
arr = ['a', 'a', 'b', 'c']
letter_freq = {}
for letter in arr:
  if letter not in letter_freq:  # Check if the key exists in dict.
    letter_freq[letter] = 0
  letter_freq[letter] += 1

# Now let's iterate over this dict:
for key, value in letter_freq.items():  # Without `.items()` just does keys.  
  print('- ', key, ' .... \t', value)

To get a key or a custom default value if it doesn't exist use get():

car = {
  "brand": "Toyota",
  "model": "Landcruiser",
  "year": 1983
}
print(car.get("price", "custom_default"))  # Prints 'custom_default'.

Note there are also functions and classes for sorting dicts or the `defaultdict`:

from collections import defaultdict

# Function to return a default value for keys
# that are not present.
def def_value():
  return 0  # Or could be a string like 'not present'.

d = defaultdict(def_value)
d["a"] = 1
d["b"] += 2  # Doesn't exist yet, so adds 2 to 0.
  
print(d["a"])  # Outputs: 1.
print(d["b"])  # Outputs: 2.
print(d["c"])  # Outputs: 0.

Linked List

I'm not a fan of linked lists, and I don't believe Python has a popular class or library for this. Instead, you can create one a bit like this:

class Node:
  def __init__(self, value):
    self.value = value
    self.next = None

class LinkedList:
  def __init__(self, head = None):
    self.head = head
    # NOTE: For quicker inserts, we'd track the tail too.

  def append(self, new_node: Node):
    if self.head == None:
      self.head = new_node
    else:
      current = self.head
      while current.next != None:
        current = current.next
      current.next = new_node
  
  # And here you'd probably want an 'insert()' and 'delete()' too.

  def length(self):
    """Return length of the linked list."""
    i = 0
    current = self.head
    while current != None:
      i += 1
      current = current.next
    return i
  
  def print_list(self):
    current = self.head
    while current != None:
      print(current.value)
      current = current.next

if __name__ == '__main__':
  head1 = Node('mon')
  n2 = Node('tue')
  n3 = Node('wed')
  list1 = LinkedList(head1)
  list1.append(n2)
  list1.append(n3)
  list1.print_list()


Enums

Surprisingly enum is not built in, but you can import `enum` to do this:

from enum import Enum, IntEnum

# Class syntax:
class Color(Enum):  # `Enum` is the same as `StrEnum` so often we might want `IntEnum`.
    RED = 0
    GREEN = 1
    BLUE = 2

color_set = [255, 0, 50]
print(color_set[Color.RED.value])  # Prints `255`. NOTE: If we call `.name` we get an error.

for c in Color:
    print(c, end=', ')  # Prints `Color.RED, Color.GREEN, Color.BLUE,` the (`.name`).


Print Options: including color output

The full syntax for `print` is: print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False). The print() parameters are:

  • objects - object (str, int, class, anything) to the printed. * indicates that there may be more than one object.
  • end - end is printed at last. Default value: '\n'. (print('Hello ', end=) # We don't want newline.)
  • sep - objects are separated by sep. Default value: ' '. (print('|i=', i, '|j=', j, '|', sep=) # If we don't want spaces.)
  • file - must be an object with write(string) method. If omitted, sys.stdout will be used which prints objects on the screen. (sourceFile = open('demo.txt', 'w') print('Hello, Python!', file=sourceFile) sourceFile.close())
  • flush - If True, the stream is forcibly flushed. Default value: False.


In any python program, you can output color codes like '\033[31m' (for red) to a `print()` to change the font and background color of the standard output. This looks pretty messy though, so you might consider installing a library like colorama (type: $ pip install colorama) or termcolor (type: $ pip3 install termcolor).


# Colors (normally might import `termcolor` or `colorama`)

C_RESET = '\033[0m'      # Most imporant to reset at the end.
C_RED = '\033[31m'       # _1 = red, 2 = green, 3=yellow, ..., 30=gray... etc...
BG_PURPLE = '\033[45m'   # 4_ = background color.

print(C_RED + 'WARNING: Error occured ' + BG_PURPLE + '...' + C_RESET + ' .. now back to normal text')
from colorama import Fore, Back, Style
print(Fore.RED + 'some red text')
print(Style.DIM + 'red dim text')
print(Back.GREEN + 'with a green background' + Style.RESET_ALL)
print('back to normal now')


import sys
from termcolor import colored, cprint

text = colored("Hello, World!", "red", attrs=["reverse", "blink"])
print(text)
cprint("Hello, World!", "green", "on_red")

for i in range(10):
    cprint(i, "magenta", end=" ")


Errors

You can handle errors in input or programming using raise error like this:

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Division by zero is not allowed!")
    return a / b

Or a more sophisticated one:

def read_file(file_path):
  try:
    with open(file_path, 'r') as file:
      content = file.read()
      return content
  except FileNotFoundError:
    raise Exception("The specified file could not be found...")
  except IOError:
    raise Exception("Error occurred while trying to read the file!")

# Example usage:
file_path = "non_existent_file.txt"
try:
  content = read_file(file_path)
  print(content)
except Exception as e:
  print(f"An error occurred: {e}")

See example of testing this at Python - testing.


Functions

Importantly, python is "pass by object reference"... which means:

  • Immutable types (e.g., int, float, str, tuple): When you pass an immutable type to a function, any changes to that variable inside the function won't affect the original value outside the function. It might seem like it's "pass by value," but in reality, the reference to the object is being passed; it's just that the object itself cannot be changed.
  • Mutable types(e.g., list, dict, set): When you pass a mutable type to a function, changes to the contents of that object inside the function will affect the original object outside the function. This is because both the outer and inner variables refer to the same object in memory.

A simple demonstration:

def modify(x, y):
  x = 20      # Re-assigns the local variable x, original object remains unchanged
  y[0] = 999  # Mutates the passed list object
a = 10
b = [1, 2, 3]

modify(a, b)
print(a)  # Outputs: 10            (int = unchanged...... as are strings and tuples)
print(b)  # Outputs: [999, 2, 3]   (list = is changed ... as are objects etc)

you can pass a variable number of arguments into a function using special symbols * and ** before the parameter names.

Use *args for a variable number of non-keyword (positional) arguments:

def my_function(*args):
  for arg in args:
    print(arg)
my_function(1, 2, 3, 4)  # Prints 1, 2, 3, 4 on separate lines

Use **kwargs for a variable number of keyword arguments to a function:

def my_function(**kwargs):
    for key, value in kwargs.items():
        print(f"{key} = {value}")

my_function(a=1, b=2, c=3)  # Prints a=1, b=2, c=3 on separate lines

You can use both *args and **kwargs in the same function: def my_function(x, *args, **kwargs):. Note that args and kwargs are just naming conventions. What's important is the * and ** prefixes. Asterisks are only used in the function definition.

Classes

Here's a simple example of a Python class that represents a Dog.

class Dog:
    # Class attribute
    species = "Canis lupus familiaris"

    # Initializer / Instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

# Instantiate the Dog object
mikey = Dog("Mikey", 6)

# Access the instance attributes
print(f"{mikey.name} is {mikey.age} years old.")  # Expected: Mikey is 6 years old.

# Is Mikey a mammal?
if mikey.species == "Canis lupus familiaris":
    print(f"{mikey.name} is a {mikey.species}")  # Expected: Mikey is a Canis lupus familiaris

# Call our instance methods
print(mikey.description())  # Expected: Mikey is 6 years old
print(mikey.speak("Gruff gruff"))  # Expected: Mikey says Gruff gruff


Class Inheritance

The syntax for a class inheriting from another class is class DerivedClass(BaseClass):

from typing import List, Union

# Base `Cake` class:
class Cake:
    def __init__(self):
        self.description = ""
        self.cost = 0.0

    def get_description(self) -> str:
        return self.description

    def get_cost(self) -> float:
        return self.cost   # NOTE: If we wanted this defined in a concrete class we could use `pass`.

# Concrete Cakes:
class Cupcake(Cake):
    def __init__(self):
        super().__init__()
        self.description = "🧁"
        self.cost = 1.0

class Cookie(Cake):
    def __init__(self):
        super().__init__()
        self.description = "đŸȘ"
        self.cost = 2.0

# Toppings decorators
class Chocolate:
    def __init__(self, cake: Cake):
        self._cake = cake

    def get_description(self):
        return f"{self._cake.get_description()} with đŸ«"

    def get_cost(self):
        return self._cake.get_cost() + 0.1

PS: There are various words for inheritance classes: the subclass/derived class/extended class/child class.... is derived from a superclass/base class/parent class.

Custom Sorting and Printing of Classes: __lt__, __repr__, __str__

To create a less than function for sorting a Person class, you would typically implement the __lt__ magic method within the class. This method allows custom classes to define their own behavior when the < operator is used.


class Person:
    def __init__(self, name: str, year_level: int):
        self.name = name
        self.year_level = year_level

    # This `__repr__` is a special function how object is
    # represented in `print()`. You might also try `__str__`
    # for a prettier version when calling `str()``.
    def __repr__(self):
        return f"{self.name}, grade:{self.year_level}"

    # Define custom less than behavior based on year level, then name.
    def __lt__(self, other):
        if self.year_level < other.year_level:
            return True
        if self.year_level == other.year_level:
            return self.name < other.name
        return False

# Test:
people = [
    Person("Alice", 3),
    Person("Bob", 2),
    Person("Charlie", 3),
    Person("Edward", 3),
    Person("David", 3),
]

sorted_people = sorted(people)
print('SORTED BY GRADE, NAME:', sorted_people)


Dunder Methods

"Magic" or "dunder" methods (short for "double underscore") in Python provide a way to customize the behavior of objects. These methods allow user-defined classes to emulate some built-in types or behaviors in Python. Here's a list of commonly used dunder methods in Python 3 and their descriptions:

  1. Object Initialization and Representation:
    • `__new__(cls, ...)`: Used to create a new instance of the class. It's a constructor and is called before `__init__`.
    • `__init__(self, ...)`: Constructor method. Initializes attributes of the instance. ⭐⭐
    • `__del__(self)`: Destructor method. Called when the instance is about to be destroyed.
    • `__repr__(self)`: Return a string representation of the object suitable for developers, ideally one that can be used with `eval()`. ⭐
    • `__str__(self)`: Return a "pretty" or "informal" string representation of the object. ⭐⭐
    • `__bytes__(self)`: Return a byte-string representation of the object.
    • `__format__(self, format_spec)`: Used with the built-in `format()` function and string's `format()` method.
  2. Comparison and Ordering:
    • `__eq__(self, other)`: Implements equality (`==`). ⭐
    • `__ne__(self, other)`: Implements inequality (`!=`).
    • `__lt__(self, other)`: Implements less than (`<`). ⭐⭐ (for custom sorting)
    • `__le__(self, other)`: Implements less than or equal to (`<=`).
    • `__gt__(self, other)`: Implements greater than (`>`).
    • `__ge__(self, other)`: Implements greater than or equal to (`>=`).
    • `__hash__(self)`: Return the hash value of the object for use in dictionaries.
  3. Attribute Access and Descriptors:
    • `__getattr__(self, name)`: Access an attribute that isn't directly accessible.
    • `__setattr__(self, name, value)`: Set the value of an attribute.
    • `__delattr__(self, name)`: Delete an attribute.
    • `__dir__(self)`: Return the list of attributes available for the object.
    • `__getattribute__(self, name)`: Similar to `__getattr__` but with a higher precedence.
  4. Containers and Sequences:
    • `__len__(self)`: Return the length of the container or sequence. ⭐
    • `__getitem__(self, key)`: Retrieve an item using the given key.
    • `__setitem__(self, key, value)`: Set the value of an item using the given key.
    • `__delitem__(self, key)`: Delete an item using the given key.
    • `__contains__(self, item)`: Check if the container contains the given item (`in` operator).
    • `__iter__(self)`: Return an iterator object for the container or sequence.
    • `__reversed__(self)`: Return a reversed version of the object.
  5. Numeric Types Emulation:
    • `__add__(self, other)`: Implements addition (`+`).
    • `__sub__(self, other)`: Implements subtraction (`-`).
    • `__mul__(self, other)`: Implements multiplication (`*`).
    • `__truediv__(self, other)`: Implements true division (`/`).
    • `__floordiv__(self, other)`: Implements floor division (`//`).
    • `__mod__(self, other)`: Implements the modulo operator (`%`).
    • `__divmod__(self, other)`: Implements the built-in `divmod()` function.
    • `__pow__(self, exponent)`: Implements power (`**`).
    • `__round__(self, n)`: Round the number to the nearest integer or to the `n` decimal places.
  6. Function/Method Emulation:
    • `__call__(self, *args, **kwargs)`: Make the instance callable like a function.
  7. Context Managers:
    • `__enter__(self)`: Provides functionality for `with` statements.
    • `__exit__(self, exc_type, exc_value, traceback)`: Clean up when exiting a `with` block.
  8. Size and Truthiness:
    • `__bool__(self)`: Returns the truthiness of the object (`True` or `False`).
    • `__sizeof__(self)`: Return the size of the object in bytes.
  9. Type Conversion:
    • `__int__(self)`: Converts the object to an integer.
    • `__float__(self)`: Converts the object to a float.
  10. Miscellaneous:
    • `__copy__(self)`: Implements shallow copy for the `copy` module. ⭐
    • `__deepcopy__(self, memodict={})`: Implements deep copy for the `copy` module.

There are additional magic methods, especially for more specialized use cases and other numeric operations, but these are the most commonly used. Remember, these methods allow you to emulate and override Python's default behaviors for your own custom objects.

See Also