(toiminnot)

hwechtla-tl: How to write functional python

Kierre.png

Mikä on WikiWiki?
nettipäiväkirja
koko wiki (etsi)
viime muutokset


What is it?

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).

Basic rules

  1. Don't modify data structures. No .append().
  2. Don't modify variables. You can assign to a variable that doesn't exist yet.
  3. If you break the above rules, do it only within a function. Especially, don't ever modify data that you got as an argument, gave as an argument, or got as a return value of a subcall. You can, however, return values that you modified earlier, and give as argument values that you modified earlier.
  4. Don't refer inherently stateful data structures (such as iterators) multiple times. The easiest way to achieve this is not to give them a name (i.e. not assign them anywhere). If you need to put them into data structures, convert them to something rereadable, such as list.

Basic data structures

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.

lists

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

sets and dicts

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 ...)

building collections

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))

for & while loops

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:


kommentoi (viimeksi muutettu 18.08.2022 11:15)