Skip to content
Python_Review

Python Review

Warning These notes are written primarily for my own study needs. They may not be comprehensive or suitable for everyone, so feel free to browse selectively. Acknowledgments Part of these notes draws inspiration from the excellent Python notes. I am also grateful to ChatGPT for its teaching and guidance in the course, which helped shape the understanding reflected in these notes.

Chapter 1 Python Basic Syntax

1.1 Variables and Collections

# Floor division rounds towards negative infinity
# True and False are actually 1 and 0 but with different keywords

# List
list = [1, 2]
list.append(3) # list is now [1, 2, 3]
list.pop() # => 3 and li is now [1, 2]
# li[start:end:step]
list[::-1] # Return list in reverse order
list2 = list[:] # Make a one layer deep copy using slices
del list[1] # list is now [1,]
list.insert(1, 2) # li is now [1, 2] again
list.index(2) # => 1
list + other_list
list.extend(other_list)
1 in list # => True
len(li) # => 2

# Tuples (immutable)
tup = (1, 2, 3)
tup[0] # => 1
len(tup) # => 3
tup + (4, 5, 6) # => (1, 2, 3, 4, 5, 6)
2 in tup # => True
a, *b, c = (1, 2, 3, 4) # unpack tuples (or lists) into variables

# Dictionaries
# Note keys for dictionaries have to be immutable types.
filled_dict = {"one": 1, "two": 2, "three": 3}
filled_dict["one"] # => 1
filled_dict.get("one") # avoid the KeyError
filled_dict.setdefault("five", 5) # filled_dict["five"] is set to 5
list(filled_dict.keys()) # => ["one", "two", "three"]
list(filled_dict.values()) # => [1, 2, 3]
"one" in filled_dict # => True
1 in filled_dict # => False
filled_dict.update({"four":4}) # Adding to a dictionary
del filled_dict["one"] # Removes the key "one" from filled dict

# Sets
# elements of a set have to be immutable.
filled_set = {1, 1, 2, 2, 3, 4} # filled_set is now {1, 2, 3, 4}
filled_set.add(5) # filled_set is now {1, 2, 3, 4, 5}
# Do set intersection with &
# set union with |
# set difference with -
# Do set symmetric difference with ^
{1, 2} <= {1, 2, 3} # => True // Check subset
2 in filled_set # => True
filled_set = some_set.copy() # filled_set is {1, 2, 3, 4, 5}
filled_set is some_set # => False

1.2 Control Flow and Iterables

animals = ["dog", "cat", "mouse"]
for i, value in enumerate(animals):
print(i, value)

# Handle exceptions with a try/except block
try:
# Use "raise" to raise an error
raise IndexError("This is an index error")
except IndexError as e:
pass # Refrain from this, provide a recovery (next example).
except (TypeError, NameError):
pass # Multiple exceptions can be processed jointly.
else: # Optional clause to the try/except block. Must follow
# all except blocks.
print("All good!") # Runs only if the code in try raises no exceptions
finally: # Execute under all circumstances
print("We can clean up resources here")

with open("myfile.txt") as f:
for line in f:
print(line)

contents = {"aa": 12, "bb": 21}
with open("myfile1.txt", "w") as file:
file.write(str(contents))

with open("myfile1.txt") as file:
contents = file.read() # reads a string from a file
print(contents)

our_iterable = filled_dict.keys()
print(our_iterable) # => dict_keys(['one', 'two', 'three']).
# An iterable is an object that knows how to create an iterator.
our_iterator = iter(our_iterable)
# Initialization: Starting point (not yet pointing to any element)
next(our_iterator) # => "one"
list(our_iterable) # => Returns ["one", "two", "three"]

1.3 Functions

def add(x, y):
print("x is {} and y is {}".format(x, y))
return x + y

add(5, 6)
add(y=6, x=5)

# *args is used to receive a variable number of positional arguments
# **kwargs is used to receive a variable number of keyword arguments
def all_the_args(*args, **kwargs):
print(args)
print(kwargs)
"""
all_the_args(1, 2, a=3, b=4) prints:
(1, 2)
{"a": 3, "b": 4}
"""
args = (1, 2, 3, 4)
kwargs = {"a": 3, "b": 4}
all_the_args(*args, **kwargs) # equivalent: all_the_args(1, 2, 3, 4, a=3, b=4)

def create_adder(x):
def adder(y):
return x + y
return adder

add_10 = create_adder(10)
add_10(3) # => 13

def create_avg():
total = 0
count = 0
def avg(n):
nonlocal total, count
total += n
count += 1
return total/count
return avg
avg = create_avg()
avg(3) # => 3.0
avg(5) # (3+5)/2 => 4.0
avg(7) # (8+7)/3 => 5.0

# built-in higher order functions
list(map(add_10, [1, 2, 3])) # => [11, 12, 13]
list(map(max, [1, 2, 3], [4, 2, 1])) # => [4, 2, 3]

(lambda x: x > 2)(3) # => True
(lambda x, y: x ** 2 + y ** 2)(2, 1) # => 5

[add_10(i) for i in [1, 2, 3]] # => [11, 12, 13]
[x for x in [3, 4, 5, 6, 7] if x > 5] # => [6, 7]

# construct set and dict comprehensions as well
{x for x in "abcddeef" if x not in "abc"} # => {'d', 'e', 'f'}
{x: x**2 for x in range(5)} # => {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

1.4 Modules

import math
print(math.sqrt(16)) # => 4.0

from math import *
print(ceil(3.7)) # => 4

import math as m
math.sqrt(16) == m.sqrt(16) # => True

1.5 Classes

class Human:
species = "H. sapiens"

# Basic initializer, this is called when this class is instantiated.
def __init__(self, name):
self.name = name
# Initialize property
self._age = 0 # the leading underscore indicates the "age" property is intended to be used internally

def say(self, msg):
print("{name}: {message}".format(name=self.name, message=msg))

# A class method is shared among all instances
@classmethod
def get_species(cls):
return cls.species

# A static method is called without a class or instance reference
@staticmethod
def grunt():
return "*grunt*"

# A property is just like a getter.
@property
def age(self):
return self._age

# This allows the property to be set
@age.setter
def age(self, age):
self._age = age

# This allows the property to be deleted
@age.deleter
def age(self):
del self._age

# This __name__ check makes sure this code block is only executed when this module is the main program.
if __name__ == "__main__":
# Instantiate a class
i = Human(name="Ian")
i.say("hi") # "Ian: hi"
i.say(i.get_species()) # "Ian: H. sapiens"
# Change the shared attribute
Human.species = "H. neanderthalensis"
i.say(i.get_species()) # => "Ian: H. neanderthalensis"
i.age = 42
# Get the property
i.say(i.age) # => "Ian: 42"
del i.age

Inheritance

from human import Human
class Superhero(Human):
# inherit all of the parent's definitions without any modifications
pass

# for a unique child class
species = "superhuman"

def __init__(self, name, movie=False, superpowers=["super strength", "bulletproofing"]):
# add additional class attributes:
self.fictional = True
self.movie = movie
# be aware of mutable default values, since defaults are shared
self.superpowers = superpowers
# The "super" function lets you access the parent class's methods
super().__init__(name)

def boast(self):
for power in self.superpowers:
print("I wield the power of {pow}!".format(pow=power))

if __name__ == "__main__":
sup = Superhero(name="Tick")
# Instance type checks
if isinstance(sup, Human):
print("I am human")

print(Superhero.__mro__)
# MRO (Method Resolution Order)
# => (<class '__main__.Superhero'>, <class 'human.Human'>, <class 'object'>)

Multiple Inheritance

class Bat:
species = "Baty"
def __init__(self, can_fly=True):
self.fly = can_fly

# This class also has a say method
def say(self, msg):
msg = "... ... ..."
return msg

# And its own method as well
def sonar(self):
return "))) ... ((("

if __name__ == "__main__":
b = Bat()
print(b.say("hello"))
print(b.fly)

from superhero import Superhero
from bat import Bat

# Define Batman as a child that inherits from both Superhero and Bat
class Batman(Superhero, Bat):
def __init__(self, *args, **kwargs):
# Each parent class takes the parameters it needs, and the rest continue to be passed down.
Superhero.__init__(self, "anonymous", movie=True, superpowers=["Wealthy"], *args, **kwargs)
Bat.__init__(self, *args, can_fly=False, **kwargs)
# override the value for the name attribute
self.name = "Sad Affleck"
'''
It is recommended that all classes be standardized as:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
'''

def sing(self):
return "nan nan nan nan nan batman!"

if __name__ == "__main__":
sup = Batman()
# The Method Resolution Order
print(Batman.__mro__)
# => (<class '__main__.Batman'>, <class 'superhero.Superhero'>, <class 'human.Human'>, <class 'bat.Bat'>, <class 'object'>)

# Calls parent method but uses its own class attribute
print(sup.get_species()) # => Superhuman

# Calls overridden method
print(sup.sing()) # => nan nan nan nan nan batman!

# Calls method from Human, because inheritance order matters
sup.say("I agree") # => Sad Affleck: I agree

# Inherited class attribute
sup.age = 100
print(sup.age) # => 100

1.6 Advanced

# generate iterator (list comprehension)
def double_numbers(iterable):
for i in iterable:
yield i + i

# `range` is a generator.
for i in double_numbers(range(1, 900000000)):
print(i)
if i >= 30:
break

# create generator comprehension
values = (-x for x in [1,2,3,4,5])
for x in values:
print(x) # prints -1 -2 -3 -4 -5 to console/terminal

# cast a generator comprehension directly to a list.
gen_to_list = list(values)

# Decorators are a form of syntactic sugar.
# They make code easier to read while accomplishing clunky syntax.

# Wrappers are one type of decorator.
# They're really useful for adding logging to existing functions without needing to modify them.
def log_function(func):
def wrapper(*args, **kwargs):
print("Entering function", func.__name__)
result = func(*args, **kwargs)
print("Exiting function", func.__name__)
return result
return wrapper

@log_function # equivalent:
def my_function(x,y): # def my_function(x,y):
return x+y # return x+y
# my_function = log_function(my_function)

my_function(1,2) # => "Entering function my_function"
# => "3"
# => "Exiting function my_function"

print(my_function.__name__) # => 'wrapper'
print(my_function.__doc__) # => None (wrapper function has no docstring)

from functools import wraps

def log_function(func):
@wraps(func)
# this ensures docstring, function name, arguments list, etc. are all copied
# to the wrapped function - instead of being replaced with wrapper's info
def wrapper(*args, **kwargs):
print("Entering function", func.__name__)
result = func(*args, **kwargs)
print("Exiting function", func.__name__)
return result
return wrapper

print(my_function.__name__) # => 'my_function'
print(my_function.__doc__) # => 'Adds two numbers together.'

Pythonic code suggestions:

  • Use more built-in functions
  • Use data classes for structured data
  • Leverage the advantages of the Python language
  • Add a main function

About this Post

This post is written by Rinic, licensed under CC BY-NC 4.0.