>>> from env_helper import info; info()
页面更新时间: 2024-04-04 19:57:28
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

9.3. Functional Programming

Functional programming is a programming paradigm that emphasizes writing functions that perform calculations without modifying global vari- ables or any external state (such as files on the hard drive, internet connec- tions, or databases). Some programming languages, such as Erlang, Lisp, and Haskell, are heavily designed around functional programming con- cepts. Although not shackled to the paradigm, Python has some functional programming features. The main ones that Python programs can use are side-effect-free functions, higher-order functions, and lambda functions. ## Side Effects Side effects are any changes a function makes to the parts of the program that exist outside of its own code and local variables. To illustrate this, let’s create a subtract() function that implements Python’s subtraction operator ( - ):

>>> def subtract(number1, number2):
>>>
>>>     return number1 - number2
>>>
>>> subtract(123, 987)
-864

This subtract() function has no side effects. That is, it doesn’t affect any- thing in the program that isn’t a part of its code. There’s no way to tell from the program’s or the computer’s state whether the subtract() function has been called once, twice, or a million times before. A function might modify local variables inside the function, but these changes remain isolated from the rest of the program.

Now consider an addToTotal() function, which adds the numeric argu- ment to a global variable named TOTAL :

>>> TOTAL = 0
>>> def addToTotal(amount):
>>>
>>>     global TOTAL
>>>
>>>     TOTAL += amount
>>>
>>>     return TOTAL
>>>
>>> addToTotal(10)
10
>>> addToTotal(10)
20
>>> addToTotal(9999)
10019
>>> TOTAL
10019

The addToTotal() function does have a side effect, because it modifies an element that exists outside of the function: the TOTAL global variable. Side effects can be more than changes to global variables. They include updating or deleting files, printing text onscreen, opening a database con- nection, authenticating to a server, or making any other change outside of the function. Any trace that a function call leaves behind after returning is a side effect.

Side effects can also include making in-place changes to mutable objects referred to outside of the function. For example, the following removeLastCatFromList() function modifies the list argument in-place:

>>> def removeLastCatFromList(petSpecies):
>>>
>>>     if len(petSpecies) > 0 and petSpecies[-1] == 'cat':
>>>
>>>         petSpecies.pop()
>>> myPets = ['dog', 'cat', 'bird', 'cat']
>>> removeLastCatFromList(myPets)
>>> myPets
['dog', 'cat', 'bird']

In this example, the myPets variable and petSpecies parameter hold ref- erences to the same list. Any in-place modifications made to the list object inside the function would also exist outside the function, making this modi- fication a side effect.

A related concept, a deterministic function, always returns the same return value given the same arguments. The subtract(123, 987) function call always returns − 864 . Python’s built-in round() function always returns 3 when passed 3.14 as an argument. A nondeterministic function won’t always return the same values when passed the same arguments. For example, calling random.randint(1, 10) returns a random integer between 1 and 10 . The time. time() function has no arguments, but it returns a different value depend- ing on what your computer’s clock is set to when the function was called. In the case of time.time() , the clock is an external resource that is effectively an input into the function the same way an argument is. Functions that depend on resources external to the function (including global variables, files on the hard drive, databases, and internet connections) are not consid- ered deterministic.

One benefit of deterministic functions is that you can cache their val- ues. There’s no need for subtract() to calculate the difference of 123 and 987 more than once if it can remember the return value from the first time it’s called with those arguments. Therefore, deterministic functions allow us to make a space-time trade-off, quickening the runtime of a function by using space in memory to cache previous results.

A function that is deterministic and free of side effects is called a pure function. Functional programmers strive to create only pure functions in their programs. In addition to those already noted, pure functions offer several benefits:

  • They’re well suited for unit testing, because they don’t require you to set up any external resources.

  • It’s easy to reproduce bugs in a pure function by calling the function with the same arguments.

  • Pure functions can call other pure functions and remain pure.

  • In multithreaded programs, pure functions are thread-safe and can safely run concurrently. (Multithreading is beyond the scope of this book.)

  • Multiple calls to pure functions can run on parallel CPU cores or in a multithreaded program because they don’t have to depend on any exter-nal resources that require them to be run in any particular sequence.

You can and should write pure functions in Python whenever possible. Python functions are made pure by convention only; there’s no setting that causes the Python interpreter to enforce purity. The most common way to make your functions pure is to avoid using global variables in them and ensure they don’t interact with files, the internet, the system clock, random numbers, or other external resources. ## Higher-Order Functions Higher-order functions can accept other functions as arguments or return functions as return values. For example, let’s define a function named callItTwice() that will call a given function twice:

>>> def callItTwice(func, *args, **kwargs):
>>>
>>>     func(*args, **kwargs)
>>>
>>>     func(*args, **kwargs)
>>>
>>> callItTwice(print, 'Hello, world!')
Hello, world!
Hello, world!

The callItTwice() function works with any function it’s passed. In Python, functions are first-class objects, meaning they’re like any other object: you can store functions in variables, pass them as arguments, or use them as return values. ## Lambda Functions Lambda functions, also known as anonymous functions or nameless functions, are simplified functions that have no names and whose code consists solely of one return statement. We often use lambda functions when passing func- tions as arguments to other functions.

For example, we could create a normal function that accepts a list con- taining a 4 by 10 rectangle’s width and height, like this:

>>> def rectanglePerimeter(rect):
>>>
>>>     return (rect[0] * 2) + (rect[1] * 2)
>>>
>>> myRectangle = [4, 10]
>>> rectanglePerimeter(myRectangle)
28

The equivalent lambda function would look like this:

>>> lambda rect: (rect[0] * 2) + (rect[1] * 2)
<function __main__.<lambda>(rect)>

To define a Python lambda function, use the lambda keyword, followed by a comma-delimited list of parameters (if any), a colon, and then an expression that acts as the return value. Because functions are first-class objects, you can assign a lambda function to a variable, effectively replicat- ing what a def statement does:

>>> rectanglePerimeter = lambda rect: (rect[0] * 2) + (rect[1] * 2)
>>> rectanglePerimeter([4, 10])

We assigned this lambda function to a variable named rectanglePerimeter , essentially giving us a rectanglePerimeter() function. As you can see, func- tions created by lambda statements are the same as functions created by def statements.

NOTE

In real-world code, use def statements rather than assigning lambda functions to a constant variable. Lambda functions are specifically made for situations in which a function doesn’t need a name.

The lambda function syntax is helpful for specifying small functions to serve as arguments to other function calls. For example, the sorted() function has a keyword argument named key that lets you specify a func- tion. Instead of sorting items in a list based on the item’s value, it sorts them based on the function’s return value. In the following example, we pass sorted() a lambda function that returns the perimeter of the given rectangle. This makes the sorted() function sort based on the calculated perimeter of its [width, height] list rather than based directly on the [width, height] list:

>>> rects = [[10, 2], [3, 6], [2, 4], [3, 9], [10, 7], [9, 9]]
>>> sorted(rects, key=lambda rect: (rect[0] * 2) + (rect[1] * 2))
[[2, 4], [3, 6], [10, 2], [3, 9], [10, 7], [9, 9]]

Rather than sorting the values [10, 2] or [3, 6] , for example, the func- tion now sorts based on the returned perimeter integers 24 and 18 . Lambda functions are a convenient syntactic shortcut: you can specify a small one- line lambda function instead of defining a new, named function with a def statement. ## Mapping and Filtering with List Comprehensions In earlier Python versions, the map() and filter() functions were common higher-order functions that could transform and filter lists, often with the help of lambda functions. Mapping could create a list of values based on the values of another list. Filtering could create a list that contained only the values from another list that match some criteria.

For example, if you wanted to create a new list that had strings instead of the integers [8, 16, 18, 19, 12, 1, 6, 7] , you could pass that list and lambda n: str(n) to the map() function:

>>> mapObj = map(lambda n: str(n), [8, 16, 18, 19, 12, 1, 6, 7])
>>> list(mapObj)
['8', '16', '18', '19', '12', '1', '6', '7']

The map() function returns a map object, which we can get in list form by passing it to the list() function. The mapped list now contains string values based on the original list’s integer values. The filter() function is similar, but here, the lambda function argument determines which items in the list remain (if the lambda function returns True ) or are filtered out (if it returns False ). For example, we could pass lambda n: n % 2 == 0 to filter out any odd integers:

>>> filterObj = filter(lambda n: n % 2 == 0, [8, 16, 18, 19, 12, 1, 6, 7])
>>> list(filterObj)
[8, 16, 18, 12, 6]

The filter() function returns a filter object, which we can once again pass to the list() function. Only the even integers remain in the filtered list. But the map() and filter() functions are outdated ways to create mapped or filtered lists in Python. Instead, you can now create them with list comprehensions. List comprehensions not only free you from writing out a lambda function, but are also faster than map() and filter() . Here we replicate the map() function example using a list comprehension:

>>> [str(n) for n in [8, 16, 18, 19, 12, 1, 6, 7]]
['8', '16', '18', '19', '12', '1', '6', '7']

Notice that the str(n) part of the list comprehension is similar to lambda n: str(n) . And here we replicate the filter() function example using a list comprehension:

>>> [n for n in [8, 16, 18, 19, 12, 1, 6, 7] if n % 2 == 0]
[8, 16, 18, 12, 6]

Notice that the if n % 2 == 0 part of the list comprehension is similar to lambda n: n % 2 == 0 .

Many languages have a concept of functions as first-class objects, allow- ing for the existence of higher-order functions, including mapping and filtering functions.