This article describes how to use Python for basic functional programming. in addition to object-oriented programming, Python can also perform simple functional programming without relying on external variables, this article describes some of the basics. For more information, see the following functional documents: abstract functional technologies such as combination, pipelines, and high-order functions. Different in this article, it shows the imperative and non-functional code examples that people write every day, and converts these examples into functional styles.
In the first part of this article, we will rewrite some short data conversion cycles into functional maps and CES. The second part selects a long loop, breaks them down into units, and then changes each unit to a function type. The third part selects a long continuous data conversion loop and breaks it down into a functional pipeline.
The examples are all written in Python, because many people think Python is easy to read. To prove that functional technology is the same for many languages, many examples avoid using the Python-specific syntax: map, reduce, and pipeline.
Guidance
When people talk about functional programming, they will mention many "functional" features. Mentioned immutable data 1, first class object 2 and last call optimization 3. These are the language features that help functional programming. Mapping, contracting, piplining, recursing, currying4, and use of higher-order functions. These are programming techniques used to write functional code. Parallel 5, inert Computing 6, and certainty are mentioned. These are attributes conducive to functional programming.
Ignore all of these. One sentence can be used to describe the features of functional code: avoid side effects. It does not depend on or change data other than the current function. All other functional items are derived from this. Use it as a guide when you study.
This is a non-functional method:
a = 0def increment1(): global a a += 1
This is a functional method:
def increment2(a): return a + 1
Do not iterate over lists. Use map and reduce.
Map)
Map accepts a method and a set as parameters. It creates a new empty set, calls the input method using the elements in each set as parameters, and inserts the returned value into the newly created set. The new set is returned.
This is a simple map that accepts a list containing the name and returns a list containing the name length:
name_lengths = map(len, ["Mary", "Isla", "Sam"]) print name_lengths# => [4, 4, 3]
Next, this map performs the Square operation on each element in the imported collection:
squares = map(lambda x: x * x, [0, 1, 2, 3, 4]) print squares# => [0, 1, 4, 9, 16]
This map does not use a naming method. It uses an anonymous and inline method defined using lambda. Lambda parameters are defined on the left side of the colon. The method subject is defined on the right side of the colon. The returned value is the result of running the method body.
The following non-functional code accepts a real name list, and then replaces the real name with a random specified code name.
import random names = ['Mary', 'Isla', 'Sam']code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde'] for i in range(len(names)): names[i] = random.choice(code_names) print names# => ['Mr. Blonde', 'Mr. Blonde', 'Mr. Blonde']
(As you can see, this algorithm may give multiple agents the same secret code. Hopefully it will not be confused in the task .)
This can be rewritten using map:
import random names = ['Mary', 'Isla', 'Sam'] secret_names = map(lambda x: random.choice(['Mr. Pink', 'Mr. Orange', 'Mr. Blonde']), names)
Exercise 1. try to rewrite the following code with map. It accepts a list composed of real names as parameters, and then uses a more stable policy to generate a code to replace these names.
names = ['Mary', 'Isla', 'Sam'] for i in range(len(names)): names[i] = hash(names[i]) print names# => [6306819796133686941, 8135353348168144921, -1228887169324443034]
(I hope that the password will have a good memory. do not forget the code when you execute the task .)
My solutions:
names = ['Mary', 'Isla', 'Sam'] secret_names = map(hash, names)
Reduce (iteration)
Reduce accepts a method and a set as parameters. Returns the result of iteration of all elements in the container.
This is a simple reduce. Returns the sum of all elements in the set.
sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4]) print sum# => 10
X is the current element of iteration. A is the sum and also the value returned by executing lambda on the previous element. Reduce () traversal element. Each iteration executes lambda on the current a and x and returns the result as the of the next iteration.
What is a in the first iteration? No iteration results were passed in before. Reduce () uses the first element in the set as a for the first iteration, and then starts iteration from the second element. That is to say, the first x is the second element.
This code records the frequency of the word 'Sam 'in the string list:
sentences = ['Mary read a story to Sam and Isla.', 'Isla cuddled Sam.', 'Sam chortled.'] sam_count = 0for sentence in sentences: sam_count += sentence.count('Sam') print sam_count# => 3
The following is written in reduce:
sentences = ['Mary read a story to Sam and Isla.', 'Isla cuddled Sam.', 'Sam chortled.'] sam_count = reduce(lambda a, x: a + x.count('Sam'), sentences, 0)
How does this code initialize? The starting point of 'Sam 'cannot be 'Mary read a story to Sam and Isla.'. the initial sum is specified by the third parameter. In this way, the element types in the set can be different from those in the accumulators.
Why is map and reduce better?
First, most of them are code lines.
2. the most important part of iteration: Set, operation, and return value are always in the same position in all maps and reduce.
3. the code in the loop may change the previously defined variables or the variables to be used later. As shown in the example, map and reduce are functional.
4. map and reduce are element operations. Every time someone reads the for loop, they have to read the logic line by line. There is almost no regular structure to help you understand the code. On the contrary, map and reduce both create code blocks to organize complex algorithms, and readers can quickly understand elements and abstract them in their minds. "Well, every element of the code in the conversion set. Then combine the processed data into an output ."
5. map and reduce are many "good friends" who provide convenience. they are revisions of basic behaviors. For example, filter, all, any, and find.
Exercise 2. Use map, reduce, and filter to overwrite the following code. Filter accepts a method and a set. Returns the elements in the collection that make the method return true.
people = [{'name': 'Mary', 'height': 160}, {'name': 'Isla', 'height': 80}, {'name': 'Sam'}] height_total = 0height_count = 0for person in people: if 'height' in person: height_total += person['height'] height_count += 1 if height_count > 0: average_height = height_total / height_count print average_height # => 120
If this is tricky, try not to consider data operations. Consider the status of the data to pass, from the people Dictionary list to the average height. Do not bind multiple transformations together. Put each row in an independent row and save the result in a named variable. After the code can be run, it will be concise immediately.
My solution:
people = [{'name': 'Mary', 'height': 160}, {'name': 'Isla', 'height': 80}, {'name': 'Sam'}] heights = map(lambda x: x['height'], filter(lambda x: 'height' in x, people)) if len(heights) > 0: from operator import add average_height = reduce(add, heights) / len(heights)
Write declarative code instead of imperative
The following program demonstrates three car competitions. Every time you move, each car may move or not move. The path of all vehicles will be printed each time the time is moved. Five times later, the competition is over.
The following is an output:
----- ------- -------- ----------- -------------
This is a program:
from random import random time = 5car_positions = [1, 1, 1] while time: # decrease time time -= 1 print '' for i in range(len(car_positions)): # move car if random() > 0.3: car_positions[i] += 1 # draw car print '-' * car_positions[i]
The code is imperative. A function version should be declarative. You should describe what to do, not how to do it.
Usage
By binding code snippets to methods, you can make the program more declarative.
from random import random def move_cars(): for i, _ in enumerate(car_positions): if random() > 0.3: car_positions[i] += 1 def draw_car(car_position): print '-' * car_position def run_step_of_race(): global time time -= 1 move_cars() def draw(): print '' for car_position in car_positions: draw_car(car_position) time = 5car_positions = [1, 1, 1] while time: run_step_of_race() draw()
To understand this code, you only need to look at the main loop ." If time is not 0, run run_step_of_race and draw and check the time. "If you want to learn more about run_step_of_race or draw in this code, you can read the code in the method.
The comment is missing. The code is self-described.
It is a very good and simple way to improve the readability of the code.
This technology uses methods, but it is only used as a conventional sub-method, but simply package the code. According to the instructions, the code is not functional. The method in the code uses the status instead of passing in the parameter. The method affects the code in the vicinity by changing the external variable, rather than the return value. To find out what the method has done, you must read each line carefully. If you find an external variable, you must find its source and find the methods to modify it.
Removal status
The following is a functional version:
from random import random def move_cars(car_positions): return map(lambda x: x + 1 if random() > 0.3 else x, car_positions) def output_car(car_position): return '-' * car_position def run_step_of_race(state): return {'time': state['time'] - 1, 'car_positions': move_cars(state['car_positions'])} def draw(state): print '' print 'n'.join(map(output_car, state['car_positions'])) def race(state): draw(state) if state['time']: race(run_step_of_race(state)) race({'time': 5, 'car_positions': [1, 1, 1]})
Code is still segmented and extracted into the method, but this method is functional. Function methods have three signatures. First, there is no shared variable. Time and car_positions are directly transmitted to method race. Second, the method accepts parameters. Third, there is no instantiated variable in the method. All data changes are completed in the return value. Rece () uses the result of run_step_of_race () for recursion. Each step produces a status, which is directly transferred to the next step.
Now there are two methods: zero () and one ():
def zero(s): if s[0] == "0": return s[1:] def one(s): if s[0] == "1": return s[1:]
Zero () accepts a string s as the parameter. if the first character is '0', the method returns other parts of the string. If not, None is returned. the default return value of Python is returned. One () does the same thing, except that the first character must be '1 ′.
Imagine the next method called rule_sequence. Accept a string and a list of Rule methods used to store the zero () and one () modes. Call the first rule on string. Unless None is returned, it will continue to accept the returned value and call the second rule on string. Unless None is returned, it accepts the returned value and calls the third rule. And so on. If any rule returns None, the rule_sequence () method is stopped, and None is returned. Otherwise, return the return value of the last rule method.
The following is an example output:
print rule_sequence('0101', [zero, one, zero])# => 1 print rule_sequence('0101', [zero, zero])# => None
This is the imperative version of rule_sequence ():
This is an imperative version:
def rule_sequence(s, rules): for rule in rules: s = rule(s) if s == None: break return s
Exercise 3. The above code uses loops to complete functions. Recursive rewriting makes it more declarative.
My solution:
def rule_sequence(s, rules): if s == None or not rules: return s else: return rule_sequence(rules[0](s), rules[1:])
Pipeline
In the previous chapter, some imperative loops are rewritten into recursive forms and used to call auxiliary methods. In this section, we will use the pipline technology to override another type of imperative loop.
The following is a list of three typical sub-data. each dictionary stores three key-value pairs related to a band: name, inaccurate nationality, and activation status. The format_bands method processes this list cyclically.
bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False}, {'name': 'women', 'country': 'Germany', 'active': False}, {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}] def format_bands(bands): for band in bands: band['country'] = 'Canada' band['name'] = band['name'].replace('.', '') band['name'] = band['name'].title() format_bands(bands) print bands# => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'},# {'name': 'Women', 'active': False, 'country': 'Canada' },# {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}]
Worried about the method name ." Format is a fuzzy word. Looking at the code carefully, these worries become crazy. Do three things in a loop. The value of 'country' is set to 'Canada '. The punctuation marks in the name are removed. The first letter of the name is capitalized. However, it is hard to see what the purpose of this code is and whether it looks like it has been done. In addition, the code is difficult to reuse, and it is difficult to test and parallel.
Compare it with the following code:
print pipeline_each(bands, [set_canada_as_country, strip_punctuation_from_name, capitalize_names])
This code is easy to understand. It removes side effects, and the auxiliary methods are functional because they seem to be linked together. The previous output forms the input of the next method. If these methods are functional, it is easy to verify. They are easy to reuse, test, and parallel.
The operation of pipeline_each () is to pass bands and pass one at a time to a conversion method such as set_cannada_as_country. After all bands call this method, pipeline_each () collects the converted bands. Then, input the following method in sequence.
Let's take a look at the conversion method.
def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d[key] = value return d def set_canada_as_country(band): return assoc(band, 'country', "Canada") def strip_punctuation_from_name(band): return assoc(band, 'name', band['name'].replace('.', '')) def capitalize_names(band): return assoc(band, 'name', band['name'].title())
Each of them associates a key of the band with a new value. It is difficult to do so without changing the original value. Assoc () solves this problem by using deepcopy () to generate a copy based on the passed dictionary. Modify the copy for each conversion method, and then return the copy.
It seems like this is good. The original Band dictionary no longer worries about changing a key value because it needs to be associated with a new value. However, the above code has two potential side effects. In method strip_punctuation_from_name (), the name without punctuation is generated by calling the replace () method on the original value. In the capitalize_names () method, the first letter of the name is capitalized by calling title () on the original value. If replace () and title () are not functional, strip_punctuation_from_name () and capitalize_names () are not functional.
Fortunately, replace () and title () do not change the strings they operate on. The strings in Python is unchangeable. For example, when replace () is used to operate the band name string, the original string is copied first, and then the copied string is modified. Zookeeper.
The variability of string and dictionaries in Python compares the attractiveness of languages like Clojure. Programmers never have to worry about variable data. Data is immutable.
Exercise 4. Try to override the pipeline_each method. Consider the operation sequence. Each time you extract a bands from the array and pass it to the first conversion method. And then pass it to the second method in a similar way. And so on.
My solution:
My solution:
def pipeline_each(data, fns): return reduce(lambda a, x: map(x, a), fns, data)
All three conversion methods are used to modify the specified fields of the passed band. Call () can be used to extract this function. Call accepts a method as a parameter and a value key as a parameter of the method.
set_canada_as_country = call(lambda x: 'Canada', 'country')strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name')capitalize_names = call(str.title, 'name') print pipeline_each(bands, [set_canada_as_country, strip_punctuation_from_name, capitalize_names])
Or, if we want to make it readable in a concise way, we can:
print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),call(lambda x: x.replace('.', ''), 'name'),call(str.title, 'name')])
Call () Code:
def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d[key] = value return d def call(fn, key): def apply_fn(record): return assoc(record, key, fn(record.get(key))) return apply_fn
There is a lot going on here. Let's take it piece by piece.
This code has done a lot. Let's see it.
1. call () is a high-level function. A high-order function accepts a function as a parameter or returns a function. Or call.
2. apply_fn () looks like the three conversion functions. It accepts a record (a band), finds the value at the record [key] position, calls fn with this value as the parameter, and returns the result of the specified fn to the copy of the record, then return the copy.
3. call () does not do any practical work. When the call is called, apply_fn () will do the actual work. In the above example using pipeline_each (), an apply_fn () instance will change the country value of the incoming band to "Canada". The other instance will capital the first letter of the incoming band name.
4. When an apply_fn () instance is running, fn and key are no longer in scope. They are neither apply_fn () parameters nor local variables. But they are still accessible. When a method is defined, the method saves the reference of the variables contained in the method: the variables defined outside the scope of the method but used in the method. When the method runs and the code references a variable, Python looks for the variables in the local and parameter fields. If not, you can find the variables saved in the closure. That is where the fn and key are located.
5. bands is not mentioned in the call () code. Because no matter what the topic is, call () can generate a pipeline for any program. Functional programming aims to build a universal, reusable, and composite function library.
Pretty. Closures, higher-order functions, and variable scopes are included in paragraphs. Have a cup of lemonade.
You also need to do some processing on the band. It is to remove things except name and country on the band. Extract_name_and_country () can pull such information.
def extract_name_and_country(band): plucked_band = {} plucked_band['name'] = band['name'] plucked_band['country'] = band['country'] return plucked_band print pipeline_each(bands, [call(lambda x: 'Canada', 'country'), call(lambda x: x.replace('.', ''), 'name'), call(str.title, 'name'), extract_name_and_country]) # => [{'name': 'Sunset Rubdown', 'country': 'Canada'},# {'name': 'Women', 'country': 'Canada'},# {'name': 'A Silver Mt Zion', 'country': 'Canada'}]
Extract_name_and_country () can be written as a common function called pluck. Pluck () can be used as follows:
print pipeline_each(bands, [call(lambda x: 'Canada', 'country'), call(lambda x: x.replace('.', ''), 'name'), call(str.title, 'name'), pluck(['name', 'country'])])
Exercise 5. Pluck () accepts a series of key values and extracts data from record based on these key values. Try to write. High-order functions are required.
My solution:
def pluck(keys): def pluck_fn(record): return reduce(lambda a, x: assoc(a, x, record[x]), keys, {}) return pluck_fn
What now?
Is there anything else to do?
Function code can be used with other code styles. The converter in this article can be implemented in any language. Try using your code to implement it.
Think about Mary, Isla, and Sam. Convert the list iteration into maps and CES ces operations.
Think about the car competition. Break down the code into methods. Change those methods to functional ones. Converts loop processing to recursion.
Think about the band. Rewrite a series of operations into pipelines.
Note:
1. an unchangeable piece of data refers to data that cannot be changed. Some languages are like Clojure. by default, all values are immutable. Any variable operation is to copy the value, modify the copied value, and return it. In this way, the bugs caused by incomplete access in the program is eliminated.
2. languages that support first-class functions allow function processing like other types of values. This means that the method can be created, passed to other methods, returned from the method, and stored in other data structures.
3. tail call optimization is a programming language feature. Each method recursion creates a stack. Stack is used to store the parameters and local values required for the current method. If a method has many recursion times, it is very likely that the compiler or interpreter will consume all the memory. Languages with end-to-end call optimization support the entire recursive call sequence by reusing the same stack. Languages such as Python do not support tail call optimization. Generally, the number of recursive methods is limited to thousands. The race () method is only five times, so it is safe.
4. Currying means decomposing a method that accepts multiple parameters into a method that accepts only the first parameter and returns a method that accepts the next parameter until all parameters are accepted.
5. parallelism means running the same piece of code simultaneously without synchronization. These concurrent operations often run on different processors.
6. inert computing is a compiler technology. to avoid code execution before results are required.
7. processing is deterministic only when the same results can be obtained for each repeat.