- Python list implementation
- TimeComplexity
- https://realpython.com/learning-paths/writing-pythonic-code/
- Books:
- Mod operator:
-1 % 10
results in9
instead of-1
. Usemath.fmod(-1, 10)
instead of-1 % 10
- Rounding integers towards zero
-
In most cases rounding down
-0.1
towards zero should give0
, this is not the case in Python unless you are usingint(x/y)
. The table below shows that usingint(x/y)
always works.Operation Result Correct math.floor(-1 / 10)
-1 No math.floor(1 / 10)
0 Yes 1 // 10
0 Yes -1 // 10
-1 No int(-1 / 10)
0 Yes int(1 / 10)
0 Yes
-
- Integers in Python3 are unbounded, the maximum integer representable is a function of the available memory
- Unlike integers, floats are not infinite precision, and it's convenient to refer to infinity as
float('inf')
andfloat('-inf')
. These values are comparable to integers, and can be used to create psuedo max-int and pseudo min-int.
-
Index based loop
for i in range(4): print(i)
-
Loop through list
x = [1,2,3] for n in x: print(n)
-
Loop through list with index
for i, n in enumerate(nums): print(i, n)
-
Loop through dictionary
x = {'x': 1, 'y': 2, 'z': 3} for k, v in x.items(): print(k, v)
-
Loop through characters of string
x = "hello" for c in x: print(c)
-
Documentation: https://wiki.python.org/moin/HowTo/Sorting#Sortingbykeys
- Starting with Python 2.2, sorts are guaranteed to be stable.
-
Sort list ASC by lambda
x = [2,3,1] new_list = sorted(x, key=lambda x: x)
-
Operator Module Functions: Python provides convenience functions to make accessor functions easier and faster. The operator module has
itemgetter
,attrgetter
, andmethodcaller
>>> from operator import itemgetter, attrgetter, methodcaller >>> sorted(student_tuples, key=itemgetter(2)) [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] >>> sorted(student_objects, key=attrgetter('age')) [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] # The operator module functions allow multiple levels of sorting. # For example, to sort by grade then by age: >>> sorted(student_tuples, key=itemgetter(1,2)) [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)] >>> sorted(student_objects, key=attrgetter('grade', 'age')) [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
-
-
Sort list in-place
x = [2,3,1] x.sort() print(x) # sorted list
-
Sort string
x = "zxa" sorted(x) # ["a", "x", "z"] ''.join(sorted(x)) # "axz"
-
Usage:
from collections import namedtuple # Define namedtuple type Point = namedtuple('Point', 'x y') # Create namedtuple instances point1 = Point(1, 2) point2 = Point(3, 4) # To access elements, use attribute names instead of indexes: x_coord = point1.x y_coord = point2.y
-
Operations:
- Namedtuples are fully iterable and unpackable like regular tuples.
- They support comparisons
(==, !=, etc.)
based on their elements. - They are immutable, meaning their elements cannot be changed after creation.
-
Additional features:
- Field names: Can be any valid Python identifier (avoid keywords).
- Optional rename argument: Automatically replaces invalid/duplicate names.
- _asdict() method: Converts namedtuple to a dictionary.
- _replace() method: Creates a new namedtuple with modified elements.
- Immutability: Ensures data consistency and simplifies multi-threaded programming.
defaultdict
: https://realpython.com/python-defaultdict/- Get dictionary keys
x = {'x': 1, 'y': 2, 'z': 3} keys = list(x) # much better! keys = list(x.keys())
- Instantiation:
[3, 5, 7, 11] [1] + [0] * 10 list(range(100))
- Existence check:
1 in [1,2,3]
- Copy list
# Shallow copy B = list(A) # or copy.copy(B) or A[:] # Deep copy B = copy.deepcopy(A)
- Binary search sorted list
bisect.bisect(A,6) bisect.bisect_left(A,6) bisect.bisect_right(A,6)
- Reverse list
A.reverse() # in-place reversed(A) # returns an iterator, wrap with list()
- Sort list
A.sort() # in-place sorted(A) # returns copy
- Delete items from list
del A[i] # delete single item del A[i:j] # remove slice
- Slicing a list
A[start:stop:step]
with all ofstart
,stop
, andstep
being optional# index 0 1 2 3 4 5 6 # reversed -5 -6 -5 -4 -3 -2 -1 A = [1, 6, 3, 4, 5, 2, 7] A[2:4] # [3,4] A[2:] # [3,4,5,2,7] A[:4] # [1,6,3,4] A[:-1] # [1,6,3,4,5,2] (except last item) A[-3:] # [5,2,7] (index from end) A[-3:-1] # [5,2], A[1:5:2] # [6, 4] A[5:1:-2] # [2, 4] A[::-1] # [7, 2, 5, 4, 3, 6, 1] (reverses list) A[k:] + A[:k] # rotates A by k to the left
- List comprehension
- A list comprehension consists of:
- An input sequence
- An iterator over the input sequence
- A logical condition over the iterator (optional)
- An expression that yields the elements of the derived list.
[x ** 2 for x in range(6)] # [0, 1, 4, 9, 16, 25] [x ** 2 for x in range(6) if x % 2 == 0] # [0, 4, 16]
- Nested
- As a general rule, it is best to avoid more than two nested comprehensions, and use conventional nested for loops
A = [1, 3, 5] B = ['d', 'b'] [(x, y) for x in A for y in B] # [(1, 'a'), (1, 'b'), (3, 'a'), (3, 'b'), (5, 'a'), (5, 'b')]
- Also supported in
set
anddict
- A list comprehension consists of:
- Lookup operation is O(1)
- Set is implemented as hashmap
- docs: https://docs.python.org/3/library/collections.html#collections.Counter
- Create counter
from collections import Counter freq = Counter([1,1,1,2,2]) print(freq) # freq {1: 3, 2: 2}
-
Create heap (default = min-heap)
import heapq h = [] heapq.heappush(h, 5) heapq.heappush(h, 0) print([heapq.heappop(h) for i in range(len(h))]) # [0 5]
-
Create max heap or priority queue
import heapq h = [] heapq.heappush(h, (-2, 5)) # priority = 2 (highest) heapq.heappush(h, (-1, 0)) # priority = 1 print([heapq.heappop(h)[1] for i in range(len(h))]) # [5, 0]
- Pad string from left
"4".rjust(4, "0") # 0004
- String formatting (Python 3.6+)
>>> f'Hello, {name}' 'Hello, Bob' >>> '{:.2}'.format(0.1234) # Limit number of decimals to show '0.12' # Format to Hex >>> f'{errno:#x}' '0xbadc0ffee'
- Trim / Strip characters
- By default
[l|r]strip
functions remove whitespace when no argument is passed
>>> s1 = '__abc__' >>> s1.lstrip('__') # Trim from left abc__ >>> s1.rstrip('__') # Trim from right __abc >>> s1.strip('__') # Trim from left and right abc
- By default
- Using list
s = [] # push O(1) s.append(1) s.append(2) print(s) # [1,2] # pop O(1) s.pop() # 2 print(s) # [1]
- collections.deque (pronounced "deck") - double-ended queue
- Usage:
from collections import deque d = deque(['a','b','c']) print(d) # deque(['a','b','c']) # Append from the right side of list d.append("f") # O(1) print(d) # deque(['a','b','c', 'f']) # Pop from the right side of list x = d.pop() # O(1) print(x) # 'f' print(d) # deque(['a','b','c']) # Append from the left side of list d.appendleft("z") # O(1) print(d) # deque(['z', 'a','b','c']) # Pop from the left side of list x = d.popleft() # O(1) print(x) # 'z' print(d) # deque(['a','b','c'])
- Usage:
from collections import deque queue = deque() # Append from right side O(1) queue.append(1) queue.append(2) print(queue) # deque([1,2]) # Pop from left side O(1) queue.popleft() # 1 print(queue) # deque([2])
-
Using
collections.deque
from collections import deque l = deque() # Append from end l.append(1) l.append(2) # Remove end l.pop() # iterate over items for n in d: print(n) # or while queue: print(queue.popleft())
-
Using custom data structure
class Node: def __init__(self, data): self.data = data self.next = None def __repr__(self): return self.data class LinkedList: def __init__(self): self.head = None def __repr__(self): node = self.head nodes = [] while node is not None: nodes.append(node.data) node = node.next nodes.append("None") return " -> ".join(nodes)
- Smart formatting and comma placement can make your list, dict, or set constants easier to maintain.
- Python’s string literal concatenation feature can work to your benefit, or introduce hard-to-catch bugs.
- Gotcha:
>>>'hello' 'world' 'helloworld'
- Always add trailing comma to container literal (list or dict):
>>> names = [ ... 'Alice', ... 'Bob', ... 'Dilbert', # <- this one ... ]
- Gotcha:
-
Context manager in Python is a an interface that your object needs to follow in order to support the
with
statement. -
Basically, all you need to do is add
__enter__
and__exit__
methods to an object if you want it to function as a context manager. Python will call these two methods at the appropriate times in the resource management cycle. -
Example 1: using class based approach
class ManagedFile: def __init__(self, name): self.name = name def __enter__(self): self.file = open(self.name, 'w') return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close()
-
Example 2: using
contextlib.contextmanager
from contextlib import contextmanager @contextmanager def managed_file(name): try: # generator function! f = open(name, 'w') yield f finally: f.close()
-
Both examples can be used as:
>>> with ManagedFile('hello.txt') as f: ... f.write('hello, world!') ... f.write('bye now')
-
Python’s assert statement is a debugging aid that tests a condition as an internal self-check in your program. Example:
assert counter == 10, "counter should be equal to 10"
-
Why asserts?
the proper use of assertions is to inform developers about unrecoverable errors in a program. Assertions are not intended to signal expected error conditions, like a File-Not-Found error, where a user can take corrective actions or just try again.
Assertions are meant to be internal self-checks for your program. They work by declaring some conditions as impossible in your code. If one of these conditions doesn’t hold, that means there’s a bug in the pr gram.
If your program is bug-free, these conditions will never occur. But if they do occur, the program will crash with an assertion error telling you exactly which “impossible” condition was triggered. This makes it much easier to track down and fix bugs in your programs. And I like anything that makes life easier—don’t you?
- Caveats
- Caveat #1 – Don’t Use Asserts for Data Validation
- Asserts should only be used to help developers identify bugs. They’re not a mechanism for handling run-time errors.
- Asserts can be globally disabled with an interpreter setting.
- Caveat #2 – Asserts That Never Fail due to syntax mis-interpretation
- This has to do with non-empty tuples always being truthy in Python
assert(1 == 2, 'This should fail')
- Caveat #1 – Don’t Use Asserts for Data Validation
- Single Leading Underscore
_var
- Single underscores are a Python naming convention that indicates a name is meant for internal use. It is generally not enforced by the Python interpreter and is only meant as a hint to the programmer.
- if you use a wildcard import to import all the names from the module, Python will not import names with a leading underscore (unless the module defines an
__all__
list that overrides this behavior)
- Single Trailing Underscore:
var_
- A single trailing underscore (postfix) is used by convention to avoid naming conflicts with Python keywords. Example:
def make_object(name, class): # SyntaxError: "invalid syntax" pass def make_object(name, class_): pass
- A single trailing underscore (postfix) is used by convention to avoid naming conflicts with Python keywords. Example:
- Double Leading Underscore:
__var
- Name mangling: the interpreter changes the name of the variable in a way that makes it harder to create collisions when the class is extended later.
class Test: def __init__(self): self.foo = 11 self._bar = 23 self.__baz = 23 >>> t = Test() >>> dir(t) ['_Test__baz', '_bar', 'foo', '__class__', '__dict__', ...]
__bar
becomes_Test__baz
- Name mangling: the interpreter changes the name of the variable in a way that makes it harder to create collisions when the class is extended later.
- Double Leading and Trailing Underscore:
__var__
- Reserved for special use in the language. This rule covers things like
__init__
for object constructors, or__call__
to make objects callable.
- Reserved for special use in the language. This rule covers things like
- Single underscore
_
- Per convention, a single stand-alone underscore is sometimes used as a name to indicate that a variable is temporary or insignificant.
for _ in range(32): print('Hello, World.')
- You can also use single underscores in unpacking expressions as a “don’t care” variable to ignore particular values.
- This meaning is per convention only and it doesn’t trigger any special behaviors in the Python parser. The single underscore is simply a valid variable name that’s sometimes used for this purpose.
- Per convention, a single stand-alone underscore is sometimes used as a name to indicate that a variable is temporary or insignificant.
- Dan’s Python String Formatting Rule of Thumb:
If your format strings are user-supplied, use Template Strings to avoid security issues. Otherwise, use Literal String Interpolation if you’re on Python 3.6+, and “New Style” String Formatting if you’re not.
- "New Style" String Formatting
>>> errno = 50159747054 >>> name = 'Bob' >>> f"Hey {name}, there's a {errno:#x} error!" "Hey Bob, there's a 0xbadc0ffee error!"
-
Functions are objects
def yell(text): return text.upper() + '!' >>> yell('hello') 'HELLO!' >>> bark = yell >>> bark('woof') 'WOOF!'
- The name of a function is just a pointer to the object where the function is stored, you can have multiple names pointing to same function.
- Python attaches a string identifier to every function at creation time for debugging purposes
>>> bark.__name__ 'yell'
- Python attaches a string identifier to every function at creation time for debugging purposes
- The name of a function is just a pointer to the object where the function is stored, you can have multiple names pointing to same function.
-
Functions Can Be Stored in Data Structures
>>>funcs = [bark, str.lower, str.capitalize] >>> for f in funcs: ... print(f, f('hey there')) <function yell at 0x10ff96510> 'HEY THERE!' <method 'lower' of 'str' objects> 'hey there' <method 'capitalize' of 'str' objects> 'Hey there' >>> funcs[0]('heyho') # call a function object stored 'HEYHO!'
-
Functions Can Be Passed to Other Functions
>>> def greet(func): ... greeting = func('Hi, I am a Python program') ... print(greeting) >>> def whisper(text): ... return text.lower() + '...' >>> greet(whisper) 'hi, i am a python program...'
- Higher-order function
-
The ability to pass function objects as arguments to other functions is powerful. It allows you to abstract away and pass around behavior in your programs. In this example, the greet function stays the same but you can influence its output by passing in different greeting behaviors. Functions that can accept other functions as arguments are also called higher-order functions.
-
The classical example for higher-order functions in Python is the built-in
map
function:>>> list(map(bark, ['hello', 'hey', 'hi'])) ['HELLO!', 'HEY!', 'HI!']
-
- Higher-order function
-
Functions Can Be Nested
def speak(text): def whisper(t): return t.lower() + '...' return whisper(text) >>> speak('Hello, World') 'hello, world...'
-
Functions Can Capture Local State
def get_speak_func(text, volume): def whisper(): return text.lower() + '...' def yell(): return text.upper() + '!' if volume > 0.5: return yell else: return whisper >>> get_speak_func('Hello, World', 0.7)() 'HELLO, WORLD!'
- Functions that do this are called closures. A closure remembers the values from its enclosing lexical scope even when the program flow is no longer in that scope.
-
Objects Can Behave Like Functions
-
While all functions are objects in Python, the reverse isn’t true
-
But objects can be made callable, which allows you to treat them like functions in many cases and invoke them with
()
syntax using the__call__
method.class Adder: def __init__(self, n): self.n = n def __call__(self, x): return self.n + x >>> plus_3 = Adder(3) >>> plus_3(4) 7
-
"calling" an object as a function attempts to execute the object’s
__call__
method. -
Use
callable(<object>) -> bool
to check weather an object is callable
-
-
Lambdas Are Single-Expression Functions
>>> add = lambda x, y: x + y >>> add(5, 3) 8
-
Lambda functions are restricted to a single expression.
- This means a lambda function can’t use statements or annotations — not even a return statement. How do you return values from lambdas then? Executing a lambda function evaluates its expression and then automatically returns the expression’s result, so there’s always an implicit return statement. That’s why some people refer to lambdas as single expression functions.
- Define an “add” func- tion inline and then immediately called it with the arguments 5 and 3.
>>> (lambda x, y: x + y)(5, 3) 8
-
Lambdas You Can Use
- Sort iterables by key
>>> tuples = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')] >>> sorted(tuples, key=lambda x: x[1]) [(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]
- Sort iterables by key
-
Decorators allow you to extend and modify the behavior of a callable (functions, methods, and classes) without permanently modifying the callable itself.
-
They “decorate” or “wrap” another function and let you execute code before and after the wrapped function runs.
- Decorator is a callable that takes a callable as input and returns another callable as output
-
Decorators are used for:
- logging
- enforcing access control and authentication
- instrumentation and timing functions
- rate-limiting
- caching, and more
-
Takeaways for understanding decorators are:
- Functions are objects: they can be assigned to variables and passed to and returned from other functions
- Functions can be defined inside other functions: and a child function can capture the parent function’s local state (closures)
-
Sample code:
def null_decorator(func): return func @null_decorator # this syntax is same as greet = null_decorator(greet) def greet(): return 'Hello!' >>>greet() 'Hello!'
-
Decorators with arguments
def trace(func): def wrapper(*args, **kwargs): print(f"TRACE: calling {func.__name__}' f' with args={args}, kwargs={kwargs}") result = func(*args, **kwargs) print(f"TRACE: {func.__name__} returned {result!r}") return result return wrapper
- It uses the * and ** operators in the wrapper closure definition to collect all positional and keyword arguments and stores them in variables (args and kwargs).
- The wrapper closure then forwards the collected arguments to the original input function using the * and ** argument unpacking operators.
-
Applying
functools.wraps
to the wrapper closure returned by the decorator carries over the docstring and other metadata of the input function:import functools def uppercase(func): @functools.wraps(func) def wrapper(): return func().upper() return wrapper @uppercase def greet(): """Return a friendly greeting.""" return 'Hello!' >>> greet.__name__ 'greet' >>> greet.__doc__ 'Return a friendly greeting.'
- As a best practice, I’d recommend that you use
functools.wraps
in all of the decorators you write yourself. It doesn’t take much time and it will save you (and others) debugging headaches down the road.
- As a best practice, I’d recommend that you use
*args
and**kwargs
let you write functions with a variable number of arguments in Python.*args
collects extra positional arguments as a tuple.**kwargs
collects the extra keyword arguments as a dictionary.
- Calling them args and kwargs is just a convention (and one you should stick to).
- Example usage with using a decorator
def trace(f): @functools.wraps(f) def decorated_function(*args, **kwargs): print(f, args, kwargs) result = f(*args, **kwargs) print(result) return decorated_function @trace def greet(greeting, name): return '{}, {}!'.format(greeting, name) >>> greet('Hello', 'Bob') <function greet at 0x1031c9158> ('Hello', 'Bob') {} 'Hello, Bob!'
- Putting a
*
before an iterable in a function call will unpack it and pass its elements as separate positional arguments to the called function.def add(a, b): return a + b >> nums = [1, 2] >>> add(*nums) 3
- Using the
*
operator on a generator consumes all elements from the generator and passes them to the function:>>> genexpr = (x * x for x in range(3)) >>> print(*genexpr) 0 1 4
- The
**
operator is used for unpacking keyword arguments from dictionaries- The function argument names need to match the dictionary keys
dict_vec = {'y': 0, 'z': 1, 'x': 1} def f(x, y, z): print(x, y, z) def g(x, y): print(x, y) >>> f(**dict_vec) 1 0 1 >>> g(**dict_vec) # error Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: g() got an unexpected keyword argument 'z'
- If you were to use the single asterisk
*
operator to unpack the dictionary, keys would be passed to the function in random order instead:>>> f(*dict_vec) y z x
- An
is
expression evaluates toTrue
if two variables point to the same (identical) object. • An == expression evaluates to True if the objects referred to by the variables are equal (have the same contents).
__repr__
and__str__
- You can control to-string conversion in your own classes using the
__str__
and__repr__
“dunder” methods. - The result of
__str__
should be readable. The result of__repr__
should be unambiguous. - Always add a
__repr__
to your classes. The default implementation for__str__
just calls__repr__
.
- Basic pattern - iterate through pairs
- Works same for odd/even lengths
- Always produces (length - 1) pairs
arr = [1, 2, 3, 4, 5]
for a, b in zip(arr, arr[1:]): # Pairs: (1,2), (2,3), (3,4), (4,5)
print(list(zip(arr, arr[1:]))) # [(1,2), (2,3), (3,4), (4,5)]
Common use cases:
diffs = [b - a for a, b in zip(arr, arr[1:])] # Differences
is_sorted = all(a <= b for a, b in zip(arr, arr[1:])) # Check if sorted
transitions = [(a,b) for a,b in zip(arr, arr[1:]) if a != b] # Find changes
Triple-wise using zip (looking at neighbors)
for a, b, c in zip(arr, arr[1:], arr[2:]): # Triplets: (1,2,3), (2,3,4), (3,4,5)
- Python provides three standard libraries for concurrency:
- threading
- asyncio
- multiprocessing
Explicitly create classes for data clumps, i.e., groups of values that do not have any methods on them. M*y programmers would use a generic Pair or Tuple class, but we have found that this leads to confusing and buggy programs.