A way to write programs in Python so that there are no data structure modifying side effects. It makes programs easier to reason about, and also shorter and easier to read (depending, of course, on your particular code reading habits).
Use data structures that are immutable: int, float, string, etc.
For data structures that are not immutable (list, dict, set), use operations that return new data structures instead of modifying old.
Here are the non-destructive variants (returning new data structures instead of modifying the existing one) of the builtin operations:
Destructive Non-destructive ls.append(x) ls + [x] ls.clear() [] ls.copy() ls ls.extend(ls2) ls + ls2 ls.pop(i) # or del ls[i] ls[:i] + ls[i+1:] ls.reverse() reversed(ls) ls.sort() sorted(ls) ls.insert(i, x) [*ls[:i], x, *ls[i:]] ls[i] = x [*ls[:i], x, *ls[i+1:]] ls[i:j] = ls2 ls[:i] + ls2 + ls[j:]
Don't use these in a loop! Instead, make a comprehension / generator that creates the result in one go.
No:
for x in ls: # this also breaks the "single assignment" rule result = result + [frobnicate(x)]
Yes:
result = [frobnicate(x) for x in ls]
No:
def geom_descend(x, divisor): if x <= 1: return [x] return geom_descend(x / divisor, divisor) + [x]
Yes:
def geom_descend(x, divisor): if x > 1: yield from geom_descend(x / divisor, divisor) yield x
Here are the non-destructive variants of builtin operations for sets:
Destructive Non-destructive s.copy() s s.update(s2) s | s2 s.intersection_update(s2) s & s2 s.difference_update(s2) s - s2 s.add(x) s | {x} s.remove(x) s - {x} s.clear() set()
Here are the non-destructive variants of builtin operations for dictionaries:
Destructive Non-destructive d[key] = x {**d, key: x} d.clear() {} d.copy() d d.update(d2) {**d, **d2} # or d | d2
One of the pros of using immutable data types is that they can be used as dictionary keys. Because of this, you might consider working with only immutable data types:
Mutable types Immutable types x = [... for ...] x = tuple(... for ...) x = {... for ...} x = frozenset(... for ...) x = {...: ... for ...} x = frozendict((..., ...) for ...)
It's not a good idea to build collections bit by bit, i.e. adding one element at a time. Python's built-in data types don't have good support for doing that in a functional way, causing a lot of memory & processing overhead. Instead, aim for a style where you build collections all at once, and read them whichever way you want.
If you need to build the collection gradually (usually this is the case if you need to read the collection to put more stuff into it, i.e. the result has a dependency on itself), switch to imperative style locally.
The most common way of building collections is to make them from earlier collections by operators (such as s1&s2 for the intersection between s1 and s2). Where this isn't applicable, use comprehensions when you're building the collections from earlier collections, and generators when you're making them from scratch.
Example 1, building a map of 2nd degree relatives from immediate relatives:
indirect_relatives = {person: relatives[direct_relative] for person, direct_relative in relatives.items() if direct_relative in relatives}
Example 2, building a list of pairs of numbers with common divisors:
common_divisor_pairs = [(x, y) for x in ls for y in ls if lcd(x, y) > 1]
Example 3, rewriting the keys of a dictionary with a common prefix:
new_dict = {prefix + key: value for key, value in old_dict.items()}
Example 4, creating a set of primes up to 100:
set(range(2,100)) - set(x*y for x in range(2,8) for y in range(2,50))
Replace these by comprehensions&aggregates, generators, or recursion. This takes a lot of iterator and builtin function vocabulary.
for e in mylist: if safe(e): x += turn(e)
->
x = sum(turn(e) for e in mylist if safe(e))
More complicated example:
is_ascending = True for i in range(len(mylist) - 1): if mylist[i] > mylist[i+1]: is_ascending = False
->
is_ascending = all(e1 <= e2 for e1, e2 in zip(mylist, mylist[1:]))
or
is_ascending = sorted(mylist) == mylist
Pikalinkit: