Skip to content

Commit

Permalink
More content to functional
Browse files Browse the repository at this point in the history
  • Loading branch information
jag1g13 committed Oct 6, 2019
1 parent e4804b6 commit 094df6c
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 14 deletions.
9 changes: 6 additions & 3 deletions _episodes/02-functional.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ It belongs to a different branch in the history of paradigms, the Declarative br

## Side Effect and Pure Functions

There's nothing particularly special about the behaviour of functions in Python, almost any valid code can be put inside a function to be called from elsewhere.
This includes defining
There's nothing particularly special about the behaviour of functions in Python, almost any valid code can be put inside a function to be called from elsewhere (even defining classes or other functions).
This means that each function can do anything the Python language can do.

In well designed code, a function should only be responsible for one task (see: Single Responsibility Principle).
Expand Down Expand Up @@ -254,6 +253,9 @@ print((lambda x: x + 1)(1))
One of the main applications of functional programming currently is the Map, Filter, Reduce model of data processing, usually refered to as **MapReduce**.
This model is particularly useful for the processing and analysis of **Big Data** using tools such as Spark or Hadoop.
Note that the `map` and `filter` functions in Python are use **lazy evaluation**.
This means that values in an iterable collection are not actually calculated until you need them.
We'll explain some of the implications of this a little later, but for now, we'll just use `list()` to convert the results to a normal list.
~~~
l = [1, 2, 3]
Expand Down Expand Up @@ -351,7 +353,7 @@ print(reduce((lambda a, b: a + b), l))
> {: .solution}
>
> Now let's assume we're reading in these numbers from an input file, so they arrive as a list of strings.
> Extend your function so that, as well as the previous tests, it also passes:
> Modify your function so that it passes the following tests:
>
> ~~~
> print(sum_of_squares(['1', '2', '3']))
Expand All @@ -376,6 +378,7 @@ print(reduce((lambda a, b: a + b), l))
> > return reduce(lambda a, b: a + b, squares)
> > ~~~
> > {: .language-python}
> >
> {: .solution}
>
> Finally, like comments in Python, we'd like it to be possible for users to comment out numbers in the input file the give to our program.
Expand Down
115 changes: 104 additions & 11 deletions _episodes/04-functional-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ keypoints:

## Comprehensions

Comprehensions are a more **Pythonic** way to do some of these operations.
Comprehensions are a more **Pythonic** way to structure map and filter operations.
They serve exactly the same purpose, but are more concise and can be easier to structure in more complex cases, such as mapping over a 2d data structure.
Using comprehensions also gives us control over which data structures we end up with, rather than always getting back a `map` or `filter` iterable.

### List Comprehensions

List comprehensions
The **list comprehension** is probably the most commonly used comprehension type.
As you might expect from the name, list comprehensions produce a list from some other iterable type.
In effect they are the same as using `map` and/or `filter` and using `list()` to cast the result to a list, as we did previously.

All comprehension types are structured in a similar way, using the syntax for a literal of that type (in this case a list literal) containing what looks like the top of a for loop.
To the left of the `for` we put the equivalent of the map operation we want to use:

~~~
print([i for i in range(5)])
Expand All @@ -33,6 +40,8 @@ print([2 * i for i in range(5)])
~~~
{: .output}

We can also use list comprehensions to perform the equivalent of a filter operation, by putting the filter condition at the end:

~~~
print([2 * i for i in range(5) if i % 2 == 0])
~~~
Expand All @@ -45,34 +54,34 @@ print([2 * i for i in range(5) if i % 2 == 0])

### Dictionary and Set Comprehensions

Exactly the same as list comprehensions, but using the dictionary or set literal syntax.
Dictionary and set comprehensions are fundamentally the same as list comprehensions but use the dictionary or set literal syntax:

~~~
print({i: 2 * i for i in range(5)})
print({2 * i for i in range(5)})
~~~
{: .language-python}

~~~
{0: 0, 1: 2, 2: 4, 3: 6, 4: 8}
{0, 2, 4, 6, 8}
~~~
{: .output}

~~~
print({2 * i for i in range(5)})
print({i: 2 * i for i in range(5)})
~~~
{: .language-python}

~~~
{0, 2, 4, 6, 8}
{0: 0, 1: 2, 2: 4, 3: 6, 4: 8}
~~~
{: .output}

## Generators

### Generator Comprehensions

Generator comprehensions look very similar to the comprehensions we've seen previously.
What happens if we try to use them in the same was as we did?
Generator comprehensions look very similar to list comprehensions, but behave slightly differently.
What happens if we try to use them in the same was as we did list comprehensions?

~~~
print((2 * i for i in range(5)))
Expand All @@ -84,7 +93,7 @@ print((2 * i for i in range(5)))
~~~
{: .output}

Generator expressions are not evaluated until you iterate over them.
Like the `map` and `filter` functions, generator expressions are not evaluated until you iterate over them.

~~~
for i in (2 * i for i in range(5)):
Expand Down Expand Up @@ -185,15 +194,99 @@ for x in l:
~~~
{: .output}

Since the list comprehension produces an actual list, whereas the generator comprehension doesn't produce any values until they are required, we also see a large difference in memory use between the two:

~~~
%%memit
# Uses about 4GB RAM
for x in [2*x for x in range(100000000)]:
pass
~~~
{: .language-python}

~~~
peak memory: 4786.71 MiB, increment: 3862.02 MiB
~~~
{: .output}

~~~
%%memit
for x in (2*x for x in range(100000000)):
pass
~~~
{: .language-python}

~~~
peak memory: 924.94 MiB, increment: 0.00 MiB
~~~
{: .output}

Performance in memory and time - use iPython timeit magic
## Generator Functions

There's one final common place where we see lazy evaluation - a **generator function**.
Generator functions are similar to generator comprehensions, but in using a function.

Instead of using `return` to return a single value from a function, a generator function **yields** multiple values:

~~~
def count_to_n(n):
for i in range(n):
yield i + 1
for i in count_to_n(5):
print(i)
~~~
{: .language-python}

~~~
1
2
3
4
5
~~~
{: .output}

Counting papers from academics

Sorting papers by title, day

Needs to end day with 2d array of papers published per person per day - run a simulation of people publishing on random days

~~~
class Person:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
class Academic(Person):
def __init__(self, name):
super().__init__(name)
self.papers = []
self.staff = []
def write_paper(title, day):
new_paper = Paper(title, day)
self.papers.append(new_paper)
return new_paper
def add_staff(academic):
if academic not in self.staff:
self.staff.append(academic)
academics = [Academic(name) for name in ['Alice', 'Bob', 'Carol', 'David']]
alice = academics[0]
bob = academics[1]
alice.add_staff(bob)
~~~
{: .language-python}


## Decorators
Function that accepts a function and returns a (different) function

Expand Down

0 comments on commit 094df6c

Please sign in to comment.