Python
09 Jun 2025 - cohlem
Unpacking over indexing
why?
- less noisy
# do this
a,b = somehting
# over
a = something[0]
b = somehting[1]
another example
# don't do this
snacks = [('bacon', 350), ('donut', 240), ('muffin', 190)]
for i in range(len(snacks)):
item = snacks[i]
name = item[0]
calories = item[1]
print(f'#{i+1}: {name} has {calories} calories')
# do this
for rank, (name, calorie) in enumerate(snacks,1):
print(rank, name, calorie)
Unpacking can be applied to any iterables (dict, lists, tuples)
Slicing
slicing creates a reference to original list, but changing sliced object won’t reflect in original list
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
b = a[:3]
b[0]= 10
print(b) # [10, 'b', 'c']
print(a) # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
Striding
Avoid using striding (step) i.e start:stop:step altogether, mostly avoid them while using negative strides.
a[::-1] # outputs ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a']
a[-2::-2] # ['g', 'e', 'c', 'a'] # its confusing, so avoid it
Starred Expression
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
car_ages_descending = sorted(car_ages, reverse=True)
oldest, second_oldest = car_ages_descending
# Instead of doing this
oldest = car_ages_descending[0]
second_oldest = car_ages_descending[1]
others = car_ages_descending[2:]
print(oldest, second_oldest, others)
# Do this
oldest, second_oldest, *others = car_ages_descending
print(oldest, second_oldest, others)
But it can only be used once in an expression, but it should alway pair with at least one normal unpacking. i.e only doing * others = car_ages_descending will not work.
Sorting
.sort() can be called on to any built-in types that has ordering, i.e int, float, str. Defaults to sorting in ascending order, use reverse=True for descending.
numbers = [93, 86, 11, 68, 70]
numbers.sort()
print(numbers) # [11, 68, 70, 86, 93]
class Tool:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __repr__(self):
return f'Tool({self.name!r}, {self.weight})'
tools = [
Tool('level', 3.5),
Tool('hammer', 1.25),
Tool('screwdriver', 0.5),
Tool('chisel', 0.25),
]
#simply calling tools.sort() will result in error, cause Tool class doesn't implement comparision operator by default.
# sort it using key parameter.
# keys expects a function, and the should return by which we want our list to be ordered i.e by name
tools.sort(key = lambda x: x.name)
Tuples have built in order defined in them, i.e
(4, 'drill') < (4, 'sander')
in this case, 4 is compared with 4, and since its true ‘drill’ is compared with ‘sander’, we can take advantage of this tuple within our key function to defined the order i.e
tools.sort(key = lambda x: (x.weight, x.name))
In this case, it sort by ascending order, but first weight is compared AND if any two items have the same weight their name attribute is compared i.e drill and sander have same weights, so d < s
#output
[Tool('drill', 4),
Tool('sander', 4),
Tool('circular saw', 5),
Tool('jackhammer', 40)]
list multiplication vs list comprehension
use multiplication carefully.
a = [[1,2]] * 2 # replicates the *same* inner list reference twice
b = [[1,2] for _ in range(2)] # makes two distinct inner lists
Closures and decorators
A simple problem. We need to keep track of every object that is printed. In the code below, how do we access data list inside inner function? doing something like below will have global data, which any other functions can access. We need some list that can keep track of every object printed, but is also only accessible to that specific function.
data = []
def inner(obj):
data.append(obj)
print(obj)
We use closures.
def print_with_memory(func):
data = []
def inner(obj):
data.append(obj)
func(obj)
return inner
print_ = print_with_memory(print)
print_('yoo')
print_('whats up')
print_.__closure__[0].cell_contents
## outputs
yoo
whats up
['yoo', 'whats up']
The drawback with closure function above is that inner func only takes one object, whereas it should be able to take in more than one object as parameters. If so, make use of _ args and _ kwargs.
def print_with_memory(func):
data = []
def inner(*args, **kwargs):
data.append((args, kwargs)) # args is a tuple, and kwargs is a dict
func(*args, **kwargs)
return inner
print_ = print_with_memory(print)
print_('yoo', 'haha', 1,2,3)
print_('whats up', sep=':')
print_.__closure__[0].cell_contents
# output
yoo haha 1 2 3
whats up
[(('yoo', 'haha', 1, 2, 3), {}), (('whats up',), {'sep': ':'})]
One more thing, our inner function doesn’t return anything, lets add return to it, and it becomes a decorator.
def print_with_memory(func):
data = []
def inner(*args, **kwargs):
data.append((args, kwargs)) # args is a tuple, and kwargs is a dict
return func(*args, **kwargs)
return inner
our print_with_memory() function now can take in any function, i.e print, max, min and so on. We can also do something like this. The whole point of decorators is that we are decorating the function, with additional functionality, i.e adding every parameter passed to that function to data list.
`print = print_with_memory(print)` # we deocorate with somehting like this
How to use @ in decorator
def store_arguments(func):
data = []
def wrapper(*args, **kwargs):
data.append((args, kwargs))
return func(*args, **kwargs)
return wrapper
# or we can do this
@store_arguments
def my_special_function(name, repeat):
return name.upper() * repeat
print(my_special_function('yolo', 2))
print(my_special_function('haha', 3))
my_special_function.__closure__[0].cell_contents
# outputs
YOLOYOLO
HAHAHAHAHAHA
[(('yolo', 2), {}), (('haha', 3), {})]