What makes function composition so powerful? - python

I have read about functional programming and it's core concept seem to be the high use of immutable datastructures, which in turn lead to pure functions. These pure functions ought be easily composable like in this example:
def add(x, y):
return 3 + 3
def minus1(z):
return z - 1
print(minus1(add(3,3)))
I understand, that i can compose functions and build new functions by this method. In the following example, i have composed two impure functions:
def add_list_last(arr):
arr.append(1)
return arr
def insert_entry_at_start(arr):
arr.insert(0,1)
return arr
print(add_list_last(insert_entry_at_start([1,2,3])))
Why can't i just compose impure functions like i did in the second example and why is it so powerful? What is the disadvantage of the second example and why is the mutation of the array preventing composability?

T = TypeVar('T')
def split(v: T) -> Tuple[T, T]:
return v, v
def apply_to_first(f: Callable[[T], T], *xs: T) -> Sequence[T]:
return (f(xs[0]), *xs[1:])
apply_to_first(insert_entry_at_start, *split([1, 2]))
You would expect this result here:
([1, 2, 1], [1, 2])
But in fact you get:
([1, 2, 1], [1, 2, 1])
Because insert_entry_at_start is impure.

Here's a simplified example:
def prepend_impure(lst, element):
lst.insert(0, element)
return lst
def prepend_pure(lst, element):
cpy = lst[:]
cpy.insert(0, element)
return cpy
def combine(list1, list2):
return [i + j for i, j in zip(list1, list2)]
list1 = [2, 3, 4]
list2 = [2, 3, 4]
print(combine(prepend_impure(list1, 1), prepend_impure(list1, 1)))
print(combine(prepend_pure(list2, 1), prepend_pure(list2, 1)))
Output:
[2, 2, 4, 6, 8]
[2, 4, 6, 8]
The prepend functions insert an element at the beginning of a given list and return the list.
The combine function adds 2 lists together.
Personally, I would expect the code to both return lists of [1, 2, 3, 4] and add the 2 together.
Not only do you not get what you expect with the impure version.. In a large codebase, trying to be mindful of all the ways a function can mutate or modify your objects can produce a lot of cognitive overload.

Related

Python list comprehension with complex data structures

I'm trying to flatten some mixed arrays in Python using LC. I'm having some trouble figuring out how to structure it.
Here's the array's i'm trying to flatten
arr_1 = [1, [2, 3], 4, 5]
arr_2 = [1,[2,3],[[4,5]]]
I tried this methods for arr_1 but get "TypeError: 'int' object is not iterable"
print([item if type(items) is list else items for items in arr_1 for item in items])
So I decided to break it into parts to see where it's failing by using this
def check(item):
return item;
print([check(item) if type(items) is list else check(items) for items in [1, [2, 3], 4, 5] for items in arr_2])
Through the debugger I found that it's failing at the 2d array in
for items in [1, [2, 3], 4, 5]
I don't need the LC to be in one line but I just wanted to know how to do it in a single nested LC if its even possible.
Using an internal stack and iter's second form to simulate a while loop:
def flatten(obj):
return [x
for stack in [[obj]]
for x, in iter(lambda: stack and [stack.pop()], [])
if isinstance(x, int)
or stack.extend(reversed(x))]
print(flatten([1, [2, 3], 4, 5]))
print(flatten([1, [2, 3], [[4, 5]]]))
print(flatten([1, [2, [], 3], [[4, 5]]]))
Output (Try it online!):
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
To explain it a bit, here's roughly the same with ordinary code:
def flatten(obj):
result = []
stack = [obj]
while stack:
x = stack.pop()
if isinstance(x, int):
result.append(x)
else:
stack.extend(reversed(x))
return result
If the order doesn't matter, we can use a queue instead (inspired by 0x263A's comment), although it's less memory-efficient (Try it online!):
def flatten(obj):
return [x
for queue in [[obj]]
for x in queue
if isinstance(x, int) or queue.extend(x)]
We can fix the order if instead of putting each list's contents at the end of the queue, we insert them right after the list (which is less time-efficient) in the "priority" queue (Try it online!):
def flatten(obj):
return [x
for pqueue in [[obj]]
for i, x in enumerate(pqueue, 1)
if isinstance(x, int) or pqueue.__setitem__(slice(i, i), x)]

How do I write instructions inside map in Python?

The input is a list: ["asdf", "ghjk", "lmnop", "zyx"].
I specifically need to apply a function over all elements of the list (so, I should use map). The function should do something like f(x, k) = x + k, where k is a constant equal to 1, and x is a counter (hence, x will be 0 at first, and when gradually iterating over the list, it should increment somehow, but how?).
Assuming these, the expected output should be: [1, 2, 3, 4].
from functools import partial
def f(x, k):
return x + k
def with_partial(my_list, k=1):
func = partial(f, k=k) # partial object which acts as a function when called
x = 0 # counter
# map -> applies a funciton over all elements of my_list
rez = list(map(lambda it: (func(x)), my_list))
# if I want to increment it, it should look like: rez = list(map(lambda it: x = (func(x)), my_list))
print("Result with partial: " + str(rez))
if __name__ == "__main__":
with_partial([5, 6, 7])
with_partial(["asdf", "ghjk", "lmnop", "zyx"])
But the output is:
Result with partial: [1, 1, 1]
Result with partial: [1, 1, 1, 1]
But I want it to be like:
Result with partial: [1, 2, 3]
Result with partial: [1, 2, 3, 4]
I can use list comprehension quite easily for that type of problem, but I need to understand how can I write instructions inside map, so that I can increment my counter effectively. Or... Is there anything about partial function I should know already, but I don't? How do I solve this type of problem using functional programming?

Python : how to make 1-D array from 2-D array

I have array2D = [[1,2,3],[4,5,6]]. What I want is a function which takes an index and returns the elements in 1D array.
Example: fn(0) -> returns [1,4]
fn{1) -> returns [2,5]
I need a fast way to do this.
you can use lambda and list comprehension:
array2D = [[1,2,3],[4,5,6]]
fn = lambda x: [item[x] for item in array2D]
print(fn(0)) # [1, 4]
print(fn(1)) # [2, 5]
print(fn(2)) # [3, 6]
as suggested in the comments, you may apply the same concept with a function definition:
def fn(x): return [item[x] for item in array2D]
print(fn(0)) # [1, 4]
print(fn(1)) # [2, 5]
print(fn(2)) # [3, 6]
Lambda functions are pretty useful, and let you define operation in a really clear way.
In our example, our lambda accept a variable x, which represent the index we want of each item in array2D
Then you have list comprehension, similarly to lambda function, they are a really powerful tool and a must in python
In this situation you should prefear the function definiton, as suggested by PEP-8.
The following list comprehension will work:
def fn(i, lst):
return [sublst[i] for sublst in lst]
>>> array2D = [[1, 2, 3], [4, 5, 6]]
>>> fn(0, array2D)
[1, 4]
>>> fn(1, array2D)
[2, 5]
You can use operator.itemgetter:
array2D = [[1,2,3],[4,5,6]]
from operator import itemgetter
def fn(x, k):
return list(map(itemgetter(k), x))
fn(array2D, 0) # [1, 4]
If you want to define new functions for retrieving a specific index, you can do so via functools.partial:
from functools import partial
def fn(x, k):
return list(map(itemgetter(k), x))
get_zero_index = partial(fn, k=0)
get_zero_index(array2D) # [1, 4]
Here are my two cents using slicing (I have to use additional np.array() for this because your original data was a list):
array2D = np.array([[1,2,3],[4,5,6]])
def fn(n): return (list(array2D[:,n]))
print (fn(0), fn(1), fn(2))
How about a generator?
We could use zip to pack them, then create a empty list to store the generated data:
class myZip(object):
__slots__ = ('zipData', 'interList')
def __init__(self, *args):
self.zipData = zip(*args)
self.interList = []
def __call__(self, index):
try:
return self.interList[index]
except IndexError:
try:
if index == 0:
self.interList.append(next(self.zipData))
return self.interList[index]
for i in range(index-(len(self.interList)-1)):
self.interList.append(next(self.zipData))
return self.interList[index]
except StopIteration:
raise IndexError("index out of range")
def __iter__(self):
for i in self.interList:
yield i
for i in self.zipData:
yield i
array2D = [[1,2,3],[4,5,6]]
a = myZip(*array2D)
print(a(2))
print(a(1))
print(a(0))
---
(3, 6)
(2, 5)
(1, 4)
The benefits of this is we do not need to produce all data at once.

Pythonic way to append output of function to several lists

I have a question that I haven't quite found a good solution to. I'm looking for a better way to append function output to two or more lists, without using temp variables. Example below:
def f():
return 5,6
a,b = [], []
for i in range(10):
tmp_a, tmp_b = f()
a.append(tmp_a)
b.append(temp_b)
I've tried playing around with something like zip(*f()), but haven't quite found a solution that way.
Any way to remove those temp vars would be super helpful though, thanks!
Edit for additional info:
In this situation, the number of outputs from the function will always equal the number of lists that are being appended to. The main reason I'm looking to get rid of temps is for the case where there are maybe 8-10 function outputs, and having that many temp variables would get messy (though I don't really even like having two).
def f():
return 5,6
a,b = zip(*[f() for i in range(10)])
# this will create two tuples of elements 5 and 6 you can change
# them to list by type casting it like list(a), list(b)
First solution: we make a list of all results, then transpose it
def f(i):
return i, 2*i
# First make a list of all your results
l = [f(i) for i in range(5)]
# [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]
# then transpose it using zip
a, b = zip(*l)
print(list(a))
print(list(b))
# [0, 1, 2, 3, 4]
# [0, 2, 4, 6, 8]
Or, all in one line:
a, b = zip(*[f(i) for i in range(5)])
A different solution, building the lists at each iteration, so that you can use them while they're being built:
def f(i):
return 2*i, i**2, i**3
doubles = []
squares = []
cubes = []
results = [doubles, squares, cubes]
for i in range(1, 4):
list(map(lambda res, val: res.append(val), results, f(i)))
print(results)
# [[2], [1], [1]]
# [[2, 4], [1, 4], [1, 8]]
# [[2, 4, 6], [1, 4, 9], [1, 8, 27]]
print(cubes)
# [1, 8, 27]
Note about list(map(...)): in Python3, map returns a generator, so we must use it if we want the lambda to be executed.list does it.
For your specific case, the zip answers are great.
Using itertools.cycle and itertools.chain is a different approach from the existing answers that might come in handy if you have a lot of pre-existing lists that you want to append to in a round-robin fashion. It also works when your function returns more values than you have lists.
>>> from itertools import cycle, chain
>>> a, b = [], [] # new, empty lists only for demo purposes
>>> for l, v in zip(cycle([a, b]), (chain(*(f() for i in range(10))))):
... l.append(v)
...
>>> a
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
>>> b
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6]
I'd do
tmp = f()
a.append(tmp[0])
b.append(tmp[1])
Not sure how pythonic it is for you though.

Using for loops to create multiple lists

Currently I have a function.
def create1(n):
output = []
for i in range(n):
output.append(int(i)+1)
return output
It returns [1,2,3] whenever enter create(3). However, I want it to return [[1],[1,2],[1,2,3]].
I know there's a problem with something in my for loop but I can't figure it out.
Use range() to create lists of numbers quickly:
def create1(n):
output = []
for i in range(n):
output.append(range(1, i + 2))
return output
or, using a list comprehension:
def create1(n):
return [range(1, i + 2) for i in range(n)]
If you are using Python 3, turn the iterator returned by range() into a list first:
for i in range(n):
output.append(list(range(1, i + 2)))
Quick demo:
>>> def create1(n):
... return [range(1, i + 2) for i in range(n)]
...
>>> create1(3)
[[1], [1, 2], [1, 2, 3]]
This works in Python 2 and Python 3:
>>> def create1(n):
... return [list(range(1,i+1)) for i in range(1,n+1)]
...
>>> create1(5)
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
Try this:
def create(n):
output = []
for i in range(n):
output.append(range(1,i+2))
return output
print create(3)
What you really want is a list appended at every stage. So try this
def create1(n):
output = []
for i in range(n):
output.append(range(1,i+2)) # append a list, not a number.
return output
range(n) gives you a list of integers from 0 to n-1. So, at each stage (at each i), you're appending to the output a list from 0 to i+1.

Categories