Python

09 Jun 2025 - cohlem

Unpacking over indexing

why?

# 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), {})]