Python Advanced Guide (Easy Advanced Programming): 10. Write Efficient Functions

Original: http://inventwithpython.com/beyond/chapter10.html

Functions are like mini-programs within programs, allowing us to break down code into smaller units. This saves us from having to write duplicate code, which introduces bugs. But writing efficient functions requires making many decisions about naming, size, parameters, and complexity.

This chapter explores the different ways we can write functions and the pros and cons of the different tradeoffs. We'll delve into the trade-offs between small and large functions, how the number of parameters affects function complexity, and how to write functions with variable number of parameters using the and *operator **. We'll also explore the functional programming paradigm and the benefits of writing functions according to it.

function name

Function names should follow the identifier conventions we described in Chapter 4. But they should usually contain a verb, since functions usually perform some action. You can also use a noun to describe what is happening. For example, the name refreshConnection(), setPassword()and extract_version()clarify what the function does and what it does.

For methods that are part of a class or module, you probably don't need nouns. SatelliteConnectionA method in a class reset()or a function webbrowserin a module open()already provides the necessary context. You can tell that the satellite connection is the item that is being reset, and the web browser is the item that is being opened.

It is better to use long, descriptive names rather than abbreviated or too short names. A mathematician might immediately understand that gcd()a function named returns the greatest common denominator of two numbers, but others will find it getGreatestCommonDenominator()more informative.

Remember not to use any of Python's built-in function or module names such as all, any, date, email, , file, format, , , hash, id, input, list, min, max, object, open, random, set, and .strsumtesttype

function size tradeoff

Some programmers say that functions should be as short as possible, no longer than one screen can fit. A function of only a dozen lines is relatively easy to understand, at least compared to a function of a few hundred lines. However, there is also a downside to shortening functions by splitting the code into multiple smaller functions. Let's look at some advantages of small functions:

  • The code for this function is easier to understand.
  • The function may require fewer arguments.
  • The function is unlikely to have side effects, as described in “Functional Programming” on page 172.
  • This function is easier to test and debug.
  • The function may raise fewer exceptions of different types.

But short functions also have some disadvantages:

  • Writing short functions usually means more functions in the program.
  • Having more functions means more complex programs.
  • Having more functions also means having to come up with more descriptive, accurate names, which is a daunting task.
  • Using more functions requires you to write more documentation.
  • The relationship between functions becomes more complex.

Some people take the "shorter is better" guideline to an extreme, claiming that all functions are three or four lines of code at most. This is crazy. For example, here's the function from the Towers of Hanoi game in Chapter 14 getPlayerMove(). The details of how these codes work are not important. Just look at the general structure of this function:

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""

    while True:  # Keep asking player until they enter a valid move.
        print('Enter the letters of "from" and "to" towers, or QUIT.')
        print("(e.g. AB to moves a disk from tower A to tower B.)")
        print()
        response = input("> ").upper().strip()

        if response == "QUIT":
            print("Thanks for playing!")
            sys.exit()

        # Make sure the user entered valid tower letters:
        if response not in ("AB", "AC", "BA", "BC", "CA", "CB"):
            print("Enter one of AB, AC, BA, BC, CA, or CB.")
            continue  # Ask player again for their move.

        # Use more descriptive variable names:
        fromTower, toTower = response[0], response[1]

        if len(towers[fromTower]) == 0:
            # The "from" tower cannot be an empty tower:
            print("You selected a tower with no disks.")
            continue  # Ask player again for their move.
        elif len(towers[toTower]) == 0:
            # Any disk can be moved onto an empty "to" tower:
            return fromTower, toTower
        elif towers[toTower][-1] < towers[fromTower][-1]:
            print("Can't put larger disks on top of smaller ones.")
            continue  # Ask player again for their move.
        else:
            # This is a valid move, so return the selected towers:
            return fromTower, toTower

This function is 34 lines long. While it covers multiple tasks, including allowing the player to enter a move, checking to see if the move is valid, and if the move is invalid, asking the player to enter a move again, these tasks fall under the scope of getting the player's move. On the other hand, if we're committed to writing short functions, we can getPlayerMove()break the code in into smaller functions, like this:

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""

    while True:  # Keep asking player until they enter a valid move.
        response = askForPlayerMove()
 terminateIfResponseIsQuit(response)
        if not isValidTowerLetters(response):
            continue # Ask player again for their move.

        # Use more descriptive variable names:
        fromTower, toTower = response[0], response[1]

        if towerWithNoDisksSelected(towers, fromTower):
            continue  # Ask player again for their move.
        elif len(towers[toTower]) == 0:
            # Any disk can be moved onto an empty "to" tower:
            return fromTower, toTower
        elif largerDiskIsOnSmallerDisk(towers, fromTower, toTower):
            continue  # Ask player again for their move.
        else:
            # This is a valid move, so return the selected towers:
            return fromTower, toTower

def askForPlayerMove():
    """Prompt the player, and return which towers they select."""
    print('Enter the letters of "from" and "to" towers, or QUIT.')
    print("(e.g. AB to moves a disk from tower A to tower B.)")
    print()
    return input("> ").upper().strip()

def terminateIfResponseIsQuit(response):
    """Terminate the program if response is 'QUIT'"""
    if response == "QUIT":
        print("Thanks for playing!")
        sys.exit()

def isValidTowerLetters(towerLetters):
    """Return True if `towerLetters` is valid."""
    if towerLetters not in ("AB", "AC", "BA", "BC", "CA", "CB"):
        print("Enter one of AB, AC, BA, BC, CA, or CB.")
        return False
    return True

def towerWithNoDisksSelected(towers, selectedTower):
    """Return True if `selectedTower` has no disks."""
    if len(towers[selectedTower]) == 0:
        print("You selected a tower with no disks.")
        return True
    return False

def largerDiskIsOnSmallerDisk(towers, fromTower, toTower):
    """Return True if a larger disk would move on a smaller disk."""
    if towers[toTower][-1] < towers[fromTower][-1]:
        print("Can't put larger disks on top of smaller ones.")
        return True
    return False

These six functions are 56 lines long, almost double the number of lines of original code, but they perform the same task. While each function is easier to understand than the original getPlayerMove(), their combination means increased complexity. Readers of your code may have a hard time understanding how it all fits together. getPlayerMove()function is the only one called by the rest of the program; the other five functions are called only once, from getPlayerMove(). But the quality of the function doesn't convey this fact.

I also had to come up with new names and docstrings ( deftriple-quoted strings under each statement, explained further in Chapter 11) for each new function. This leads to confusing function names, such as getPlayerMove()and askForPlayerMove(). Plus, getPlayerMove()it's still longer than three or four lines, so if I'm following the "shorter is better" rule, I need to break it up into smaller functions!

In this case, the strategy of only allowing very short functions may lead to simpler functions, but the overall complexity of the program increases dramatically. In my opinion, functions should preferably be less than 30 lines, never more than 200 lines. Keep your functions as short as possible, but no longer.

Function parameters and actual parameters

The formal parameters of a function are defthe variable names between the parentheses of the function's statement, while the actual parameters are the values ​​between the parentheses of the function call. The more parameters a function has, the easier its code is to configure and generalize. But more parameters also mean more complexity.

A good rule to follow is that zero to three arguments is fine, but more than five or six is ​​probably too much. Once functions get too complex, it's a good idea to think about how to break them down into smaller functions with fewer parameters.

default parameters

One way to reduce the complexity of function parameters is to provide default parameters for the parameters. A default argument is a value that is used as an argument if the function call does not specify an argument. If most function calls use a specific parameter value, we can make that value a default parameter to avoid typing it repeatedly in function calls.

We defspecified a default parameter in the statement, following the parameter name and the equals sign. For example, in this introduction()function, greetingthe parameter named has a value 'Hello'if the function call does not specify it:

>>> def introduction(name, greeting='Hello'):
...    print(greeting + ', ' + name)
...
>>> introduction('Alice')
Hello, Alice
>>> introduction('Hiro', 'Ohiyo gozaimasu')
Ohiyo gozaimasu, Hiro

When the function is called without a second argument introduction(), it defaults to a string 'Hello'. Note that arguments with default arguments must always follow arguments without default arguments.

Recall from Chapter 8 that you should avoid using mutable objects as default values, such as empty lists []or empty dictionaries {}. “Don't use mutable values ​​as default parameters” on page 143 explains the problems this approach causes and their solutions.

Using *and **passing arguments to functions

You can pass groups of arguments to functions individually using *the and **syntax (often pronounced star and double star ). *The syntax allows you to pass items in an iterable object such as a list or tuple. **The syntax allows you to pass in key-value pairs from a map object (such as a dictionary) as separate arguments.

For example, print()functions can accept multiple parameters. By default, it puts a space between them, as shown in the code below:

>>> print('cat', 'dog', 'moose')
cat dog moose

These parameters are called positional parameters because their position in the function call determines which parameter is assigned to which parameter. However, if you store those strings in a list, and try to pass that list, print()the function thinks you're trying to print out the list as a single value:

>>> args = ['cat', 'dog', 'moose']
>>> print(args)
['cat', 'dog', 'moose']

Passing a list to print()will display the list, including brackets, quotes, and comma characters.

One way to print a single item in a list is to split the list into multiple arguments by passing the index of each item separately to the function, which results in hard-to-read code:

>>> # An example of less readable code:
>>> args = ['cat', 'dog', 'moose']
>>> print(args[0], args[1], args[2])
cat dog moose

There is an easier way to pass these items to print(). You can use *the syntax to interpret the items in a list (or any other iterable data type) as individual positional arguments. Enter the following example into the interactive shell.

>>> args = ['cat', 'dog', 'moose']
>>> print(*args)
cat dog moose

*The syntax allows you to pass list items individually to a function, no matter how many items are in the list.

You can pass **mapped data types, such as dictionaries, as separate keyword arguments using the syntax. Keyword arguments are preceded by the argument name and an equals sign. For example, print()a function has a sepkeyword argument that specifies a string to put between the arguments it displays. By default it is set to a string of spaces ' '. You can use assignment statements or **syntax to assign keyword arguments to different values. To see how this works, enter the following into an interactive shell:

>>> print('cat', 'dog', 'moose', sep='-')
cat-dog-moose
>>> kwargsForPrint = {
    
    'sep': '-'}
>>> print('cat', 'dog', 'moose', **kwargsForPrint)
cat-dog-moose

Note that these instructions produce the same output. In this example, we set up the dictionary in just one line of code kwargsForPrint. But for more complex cases, you may need more code to build a dictionary of keyword arguments. **The syntax allows you to create custom dictionaries of configuration settings to pass to function calls. This is especially useful for functions and methods that accept a large number of keyword arguments.

*You can use the and **syntax to provide a variable number of arguments to a function call by modifying the list or dictionary at runtime .

*Create variadic functions using

You can also defuse the syntax in a statement to create variadic or variadic* functions that receive varying numbers of positional arguments . For example, is a variadic function because you can pass it any number of strings: for example, or . Note that although we used the syntax in the function call in the previous section, we use the syntax in the function definition in this section.print()print('Hello!')print('My name is', name)**

Let's product()see an example by creating a function that takes any number of arguments and multiplies them:

>>> def product(*args):
...    result = 1
...    for num in args:
...        result *= num
...    return result
...
>>> product(3, 3)
9
>>> product(2, 1, 2, 3)
12

Inside the function, argsjust a regular Python tuple containing all positional arguments. Technically, you can name this parameter anything you want as long as it *starts with an asterisk ( ), but by convention it is usually named args.

Knowing when to use *it takes some thinking. After all, the alternative to making a variadic function is to take a single argument that accepts a list (or other iterable data type) containing a varying number of items. This is what the built-in sum()function does:

>>> sum([2, 1, 2, 3])
8

sum()The function expects an iterable argument, so passing it multiple arguments will result in an exception:

>>> sum(2, 1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() takes at most 2 arguments (4 given)

Meanwhile, the built-in min()sum max()function can find the minimum or maximum of several values, accepting a single iterable argument or multiple separate arguments:

>>> min([2, 1, 3, 5, 8])
1
>>> min(2, 1, 3, 5, 8)
1
>>> max([2, 1, 3, 5, 8])
8
>>> max(2, 1, 3, 5, 8)
8

All of these functions take a different number of arguments, so why are their arguments designed differently? When should we use *syntax to design functions that accept an iterable argument or multiple independent arguments?

How we design parameters depends on how we predict how programmers will use our code. print()A function accepts multiple parameters because programmers more often pass it a series of strings or variables containing strings, as in print('My name is', name). print()It's uncommon to collect these strings into a list in a few steps and then pass the list to . Also, if you print()pass a list to , the function will print the full list value, so you cannot use it to print individual values ​​in the list.

There's no reason to call it with a separate argument sum(), since Python already uses +the operator for that. Because you can 2 + 4 + 8write code like that, you don't need to be able to sum(2, 4, 8)code like that. It makes sense that you'd have to pass a varying number of arguments to sum().

min()and max()functions allow both flavors. If the programmer passes an argument, the function assumes it is a list or tuple of values ​​to check. If the programmer passes multiple parameters, it assumes these are the values ​​to check. These two functions typically process lists of values ​​at program runtime, such as function calls min(allExpenses). They also deal with independent parameters that the programmer chooses when writing the code, such as in max(0, someNumber). Therefore, these functions are designed to accept both types of arguments. Below myMinFunction(), my own min()implementation of the function, demonstrates this:

def myMinFunction(*args):
    if len(args) == 1:
    1 values = args[0]
    else:
    2 values = args

    if len(values) == 0:
    3 raise ValueError('myMinFunction() args is an empty sequence')

 4 for i, value in enumerate(values):
        if i == 0 or value < smallestValue:
            smallestValue = value
    return smallestValue

myMinFunction()Use *the syntax to accept varying numbers of arguments as tuples. If this tuple contains only one value, we assume it is a sequence of values ​​to check 1 . Otherwise, we assume argsa tuple of values ​​to check 2. Either way, valuesthe variable will contain a sequence of values ​​for the rest of the code to check. Like actual min()functions, we raise if the caller passes no arguments or passes an empty sequence3 ValueError. The rest of the code loops through the values ​​and returns the smallest value found, 4. To keep this example simple, myMinFunction()only sequences such as lists or tuples are accepted, not any iterable values.

You might wonder why we don't always write functions to accept two ways of passing different numbers of arguments. The answer is that it's best to keep your functions as simple as possible. Unless both ways of calling a function are common, choose one of them. If a function normally deals with data structures created when the program runs, it's better to have it accept a single argument. If a function normally deals with arguments that the programmer specifies when writing the code, it is better to use *syntax to accept a variable number of arguments.

**Create variadic functions using

Variadic functions can also use **the syntax. Although the syntax defin a statement *indicates a different number of positional arguments, **the syntax indicates a different number of optional keyword arguments.

If you define a function that can **take many optional keyword arguments without using the syntax, your defstatement can become unwieldy. Consider a hypothetical formMolecule()function with parameters for all 118 known elements:

>>> def formMolecule(hydrogen, helium, lithium, beryllium, boron, `--snip--`

Pass as hydrogenparameter 2, oxygenpass as parameter 1, return 'water', which is also cumbersome and hard to understand, since you have to set all irrelevant elements to zero:

>>> formMolecule(2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 `--snip--`
'water'

Functions can be made more manageable by using named keyword arguments that each have a default argument so that you don't have to pass arguments to it in the function call.


Note

Although the terms formal parameter and actual parameter are well defined, programmers tend to use keyword formal parameter and keyword actual parameter (generally referred to as keyword parameter in Chinese) interchangeably.


For example, this defstatement has default arguments for each keyword argument 0:

>>> def formMolecule(hydrogen=0, helium=0, lithium=0, beryllium=0, `--snip--`

This makes calling formMolecule()much easier, since you only need to specify parameter values ​​for parameters that differ from the default parameters. You can also specify keyword arguments in any order:

>>> formMolecule(hydrogen=2, oxygen=1)
'water'
>>> formMolecule(oxygen=1, hydrogen=2)
'water'
>>> formMolecule(carbon=8, hydrogen=10, nitrogen=4, oxygen=2)
'caffeine'

But still an awkward defstatement with 118 parameter names. What if new elements are discovered? You must update the function's defstatement and any documentation for the function's parameters.

Instead, you can use **the syntax of keyword arguments to collect all arguments and their arguments as key-value pairs into a dictionary. You can technically **name the parameter anything you want, but by convention it is usually named kwargs:

>>> def formMolecules(**kwargs):
...    if len(kwargs) == 2 and kwargs['hydrogen'] == 2 and 

kwargs['oxygen'] == 1:

 ...        return 'water'
...    # (rest of code for the function goes here)
...
>>> formMolecules(hydrogen=2, oxygen=1)
'water'

**The syntax indicates that kwargsparameters can handle all keyword arguments passed in a function call. They will be stored as key-value pairs in kwargsthe dictionary assigned to the parameter. As new chemical elements are discovered, you need to update the code of the function, not its defstatement, because all keyword arguments are put in kwargs:

>>> def formMolecules(**kwargs): # 1
...    if len(kwargs) == 1 and kwargs.get('unobtanium') == 12: # 2
...        return 'aether'
...    # (rest of code for the function goes here)
...
>>> formMolecules(unobtanium=12)
'aether'

As you can see, defstatement 1 is the same as before, only code 2 of the function needs to be updated. When you use **syntax, defstatements and function calls become easier to write and still produce readable code.

Using *and **creating wrapper functions

def*A common use case for the and **syntax in statements is to create wrapper functions that pass arguments to another function and return the return value of that function. You can forward any and all arguments to the wrapped function using *the and syntax. **For example, we can create a printLowercase()function that wraps a built-in print()function. It relies on print()to do the actual work, but converts the string argument to lowercase first:

>>> def printLower(*args, **kwargs): # 1
...    args = list(args) # 2
...    for i, value in enumerate(args):
...        args[i] = str(value).lower()
...    return print(*args, **kwargs) # 3
...
>>> name = 'Albert'
>>> printLower('Hello,', name)
hello, albert
>>> printLower('DOG', 'CAT', 'MOOSE', sep=', ')
dog, cat, moose

printLower()Function 1 uses a syntax that accepts a varying number of positional arguments in a tuple assigned *to arguments, while a syntax that assigns any keyword arguments to a dictionary in arguments. If the function uses both and , the argument must precede the argument. We pass these to the wrapped function, but first our function modifies some arguments, so we create a tuple2 in list form.args**kwargs*args**kwargs*args**kwargsprint()args

After argschanging the strings in to lowercase, we use *and **syntax 3 to pass argsthe items in and kwargsthe key-value pairs in as separate arguments to print(). print()The return value of is also printLower()returned as the return value of . These steps effectively wrap print()the function.

functional programming

Functional programming is a programming paradigm that emphasizes writing functions that perform computations without modifying global variables or any external state such as files on your hard drive, internet connections, or databases. Some programming languages, such as Erlang, Lisp, and Haskell, are largely designed around functional programming concepts. Despite not being bound by a paradigm, Python has some functional programming features. The main functions that Python programs can use are side-effect-free functions, higher-order functions, and Lambda functions.

side effect

Side effects are any changes a function makes to parts of the program that exist outside of its own code and local variables. To illustrate this, let's create a function that implements Python's subtraction operator ( -) :subtract()

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

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

Now consider a addToTotal()function that adds a numeric argument to TOTALa global variable named:

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

addToTotal()A function does have side effects because it modifies an element that exists outside of the function: the global variable TOTAL. Side effects are not just changes to global variables. They include updating or deleting a file, printing text to the screen, opening a database connection, authenticating to a server, or making any other changes outside of a function. Any traces left after the function call returns are side effects.

Side effects also include in-place changes to mutable objects referenced outside 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, myPetsthe variable and petSpeciesparameter hold references to the same list. Any in-place modifications to the list object inside the function will also exist outside the function, making such modifications a side effect.

A related concept, a deterministic function , always returns the same return value given the same arguments. subtract(123, 987)Function calls always return −864. Python built-in round()functions always return when passed 3.14as arguments 3. Non-deterministic functions do not always return the same value when passed the same arguments . For example, the call random.randint(1, 10)will return a random integer between 1and . The function has no parameters, but it returns different values ​​depending on the setting of the computer's clock when the function is called. In the example, the clock is an external resource, which is actually an input to the function, just like an argument. Functions that depend on resources external to the function (including global variables, files on hard disk, databases, and Internet connections) are not considered deterministic.10time.time()time.time()

One benefit of deterministic functions is that their values ​​can be cached. It doesn't need to calculate the difference of sums multiple times if subtract()it can remember the return value the first time it was called with these parameters . Thus, deterministic functions allow us to make a space-time trade-off , speeding up the running time of the function by using memory space to cache previous results.123987

A function that is deterministic and has no side effects is called a pure function . Functional programmers strive to create only pure functions in their programs. Apart from those already mentioned, pure functions offer several benefits:

  • They are great for unit testing because they don't require you to set up any external resources.
  • It's easy to reproduce the bug 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 run safely concurrently. (Multithreading is beyond the scope of this book.)
  • Multiple calls to pure functions can run on parallel CPU cores or in multithreaded programs because they don't depend on any external resources that require them to run in any particular order.

Whenever possible, you can and should write pure functions in Python. Python functions are pure by convention; no setting causes the Python interpreter to guarantee purity. The most common approach is to avoid using global variables in functions, and make sure 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 callItTwice()function called that will call the given function twice:

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

callItTwice()A function works with whatever function it is passed. In Python, functions are first-class objects , which means they are just like any other object: you can store functions in variables, pass them as arguments, or use them as return values.

Lambda function

Lambda functions , also known as anonymous functions or unnamed functions , are simplified functions without a name whose code returnconsists of only one statement. We often use Lambda functions when passing functions as parameters to other functions.

For example, we could create a generic function that takes a list containing the width and height of a 4 by 10 rectangle as follows:

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

The equivalent Lambda function looks like this:

lambda rect: (rect[0] * 2) + (rect[1] * 2)

To define a Python Lambda function, use lambdathe keyword, followed by a comma-separated list of arguments (if any), a colon, and then an expression that acts as the return value. Because functions are first-class objects, you can assign Lambda functions to variables, effectively replicating defthe functionality of statements:

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

We assign this Lambda function to a rectanglePerimetervariable named , effectively giving us a rectanglePerimeter()function. As you can see, lambdaa function created by a statement defis the same as a function created by a statement.


Note

In real-world code, use defstatements instead of assigning Lambda functions to constant variables. Lambda functions are specifically designed for situations where the function does not need a name.


Lambda function syntax facilitates specifying small functions as arguments to other function calls. For example, sorted()functions have a keykeyword argument named , which allows you to specify a function. Instead of sorting the items in the list based on their values, it sorts them based on the return value of the function. In the example below, we sorted()pass a Lambda function that returns the perimeter of a given rectangle. This makes the function sort sorted()based on the computed girth of its list, rather than directly on the list:[width, height][width, height]

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

For example, the function now sorts on the returned integer sum of perimeters 24, rather than sorting on the values ​​or . Lambda functions are a convenient syntactic shortcut: instead of defining a new named function with one statement, you can specify a small, one-line Lambda function .18[10, 2][3, 6]def

Using list comprehensions for mapping and filtering

In earlier versions of Python, map()and filter()functions were common higher-order functions that could transform and filter lists, usually with the help of Lambda functions. A map can create a list of values ​​based on the values ​​of another list. Filtering creates a list that contains only values ​​from another list that meet certain criteria.

For example, if you want to create a new list that contains strings instead of integers [8, 16, 18, 19, 12, 1, 6, 7], you can pass this list lambda n: str(n)sum to map()the 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']

map()The function returns an object and we can get it as a list mapby passing it to the function. list()Mapped lists now contain string values ​​based on the integer values ​​of the original list. filter()Functions are similar, but here, the Lambda function parameter determines which items in the list are kept (if the Lambda function returns True) or filtered out (if it returns False). For example, we can lambda n: n % 2 == 0filter out any odd numbers by:

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

filter()The function returns a filter object, which we can pass to list()the function again. Only even integers remain in the filtered list.

But map()and filter()functions are an obsolete way of creating maps or filtering lists in Python. Instead, you can now create them with list comprehensions. List comprehensions not only save you from writing Lambda functions, but are also faster than map()and filter().

Here we replicate map()the function example using list comprehensions:

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

Note that the list comprehension str(n)part is lambda n: str(n)similar to .

Here we replicate filter()the function example using list comprehensions:

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

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

Many languages ​​have the concept of functions as first-class objects, allowing for higher-order functions, including mapping and filtering functions.

The return value should always have the same data type

Python is a dynamically typed language, which means that Python functions and methods are free to return values ​​of any data type. But to make your functions more predictable, you should strive to make them return only values ​​of a single data type.

For example, the following function returns an integer value or a string value based on a random number:

>>> import random
>>> def returnsTwoTypes():
...    if random.randint(1, 2) == 1:
...        return 42
...    else:
...        return 'forty two'

When you're writing code that calls the function, it's easy to forget that you have to deal with several possible data types. Continuing with the example, suppose we call and want to convert the numberreturnsTwoTypes() it returns to hexadecimal:

>>> hexNum = hex(returnsTwoTypes())
>>> hexNum
'0x2a'

Python's built-in hex()function returns a hexadecimal string of the integer value it is passed. This code works fine as long as returnsTwoTypes()it returns an integer, giving us the impression that this code is error-free. But when returnsTwoTypes()returning a string, it throws an exception:

>>> hexNum = hex(returnsTwoTypes())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object cannot be interpreted as an integer

Of course, we should always remember to handle every possible data type that the return value may have. But in the real world, it's easy to forget this. To prevent these mistakes, we should always try to have functions return values ​​of a single data type. This is not a strict requirement, and sometimes it is not possible to have a function return a value of a different data type. But the closer you get to returning only one type, the simpler and less error-prone your function will be.

There is one case to be especially careful about: don't return from functions Noneunless your function always returns None. NoneValue is NoneTypethe only value in the data type. NoneIt's tempting to have functions return to indicate that an error occurred (a practice I discuss in the next section, "Throwing Exceptions vs. Returning Error Codes"), but you should reserve returns for functions that don't have a meaningful return value None.

The reason is that returns indicating errors are a common source of Noneuncaught exceptions:'NoneType' object has no attribute

>>> import random
>>> def sometimesReturnsNone():
...    if random.randint(1, 2) == 1:
...        return 'Hello!'
...    else:
...        return None
...
>>> returnVal = sometimesReturnsNone()
>>> returnVal.upper()
'HELLO!'
>>> returnVal = sometimesReturnsNone()
>>> returnVal.upper()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'upper'

The error message is rather vague, and it may take some effort to trace back to a function that usually returns the expected result, but may also return when the error occurs None. The problem arises because sometimesReturnsNone()we return Noneand then we assign it to returnVala variable. But the error message would make you think the problem is in upper()the call to the method.

At a conference in 2009, computer scientist Tony Hall Noneapologized for the invention of null references (values ​​similar to Python's) in 1965, saying "I call it my billion-dollar mistake. [… ] I couldn't resist the temptation to put in a null reference simply because it's so easy to do. This has led to untold bugs, bugs, and system crashes that have probably cost billions of dollars over the last 40 years Pain and loss." You can autbor.com/billiondollarmistakewatch his full speech online.

Throwing exceptions and returning error codes

In Python, the terms exception and error mean roughly the same thing: an unusual condition in a program that usually indicates a problem. Exceptions became popular as a programming language feature in the 1980s and 1990s with the advent of C++ and Java. They replace the use of error codes , which are values ​​returned from functions that indicate a problem. The nice thing about exceptions is that the return value is only relevant to the purpose of the function, not to indicate that there was an error.

Error codes can also cause problems with programs. For example, Python's find()string methods typically return the index at which the substring was found, or -1as an error code if the substring was not found. But since we can also use -1to specify an index from the end of the string, inadvertently using it -1as an error code could introduce a bug. Type the following in an interactive shell to see how this works.

>>> print('Letters after b in "Albert":', 'Albert'['Albert'.find('b') + 1:])
Letters after b in "Albert": ert
>>> print('Letters after x in "Albert":', 'Albert'['Albert'.find('x') + 1:])
Letters after x in "Albert": Albert

Part of the code 'Albert'.find('x')evaluates to an error code -1. This makes the expression 'Albert'['Albert'.find('x') + 1:]evaluate to 'Albert'[-1 + 1:], which further evaluates to 'Albert'[0:], and then evaluates to 'Albert'. Obviously, this is not the expected behavior of the code. Calling index()instead find(), as in 'Albert'['Albert'.index('x') + 1:], raises an exception, making the problem too obvious to ignore.

String methods , on the other hand, index()raise an exception when the substring cannot be found ValueError. If you don't handle this exception, it will crash the program - this behavior is usually better than not noticing the error.

ValueErrorThe name of the exception class usually ends with "error", as in , NameErroror , when the exception indicates an actual error SyntaxError. Exception classes that represent exceptional conditions that are not necessarily errors include StopIteration, KeyboardInterruptor SystemExit.

Summarize

Functions are a common way of putting our program code together, and they require you to make certain decisions: what to name them, how big they should be, how many parameters they should have, and how many you should pass for those parameters parameters. The and syntax defin statements allows functions to receive varying numbers of arguments, making them variadic functions.***

Although Python is not a functional programming language, it has many features used by functional programming languages. Functions are first-class objects, which means you can store them in variables and pass them as arguments to other functions (called higher-order functions in this context). Lambda functions provide a short syntax for specifying unnamed, anonymous functions as arguments to higher-order functions. The most common higher-order functions in Python are map()and filter(), although the functionality they provide can be performed much faster using list comprehensions.

The return value of a function should always be the same data type. You should not use return values ​​as error codes: exceptions are used to indicate errors. In particular Nonevalues ​​are often incorrectly used as error codes.

Guess you like

Origin blog.csdn.net/wizardforcel/article/details/130030501