© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
G. GutaPragmatic Python Programminghttps://doi.org/10.1007/978-1-4842-8152-9_2

2. The Function: Programs as a Series of Statements

Gabor Guta1  
(1)
Budapest, Hungary
 

“It is usual in mathematics—outside of mathematical logic—to use the word function imprecisely and to apply it to forms such as y2 + x. Because we shall later compute with expressions for functions, we need a distinction between functions and forms and a notation for expressing this distinction. This distinction and a notation for describing it, from which we deviate trivially is given by Church.”

John McCarthy et al.

The function is the simplest construct able to describe a behavior. Functions can behave similarly as in math: they compute a number from a given number or numbers. Functions in programming languages are of a somewhat more general construction, though: they generate a new object by executing statements from given objects. In this chapter, you will learn how functions work in programming languages and about related concepts such as blocks.

Calling a Function

Let’s start by looking at how to use the built-in functions that already exist in Python. Using a function usually means executing a function call. The result of the function is referred to as the returned value . The computer executes the statements assigned to the function name with the specified objects, and the result object will be obtained. This is expressed by placing a pair of parentheses after the function name, in which parameter objects are enumerated optionally. The enumerated objects are called the arguments of the functions. Calling the absolute value function visible in Listing 2-1 will result in a number object with a value of 1. This is exactly what you would expect from the |-1| expression.
abs(-1)
Listing 2-1

Calculation of an Absolute Value

The function call can be combined with expression notations: it can be present anywhere a reference to an object can be written, and so the arguments of the function can become arbitrarily complicated expressions. From among the built-in mathematical functions you saw examples for the absolute value function (abs()). In addition, there also exists the functions of raising to a power (pow()) and rounding (round()). You can see this in Listing 2-2. The result of price calculation of 3499 with tax (which is 10% in the United States, so the original number must be multiplied by 1.1) is 3848.9, which is rounded to an integer (to 3849) in the example.
round(3499 * 1.1)
Listing 2-2

Calculation of a Rounded Value

Tip

The built-in types of the Python language cannot always accurately store decimals. This is a consequence of the binary representation of decimals in memory. For example, the 3848.9 value shown in the previous example is stored as 3848.90000000000009. How to store best financial data in Python is described in detail in Chapter 6.

Listing 2-3 shows a more complicated expression . What we are calculating is the gross difference of two net prices. First the rounded gross values of the two prices are calculated in the expression; then these are subtracted from each other. It is visible in the example that the function calls can be embedded, and the result of the abs() function call is assigned to a variable name.
OLD_NET = 5000
NEW_NET = 6500
VAT_RATE = 1.1
difference_gross = abs(round(OLD_NET*1.1) - round(NEW_NET*1.1))
Listing 2-3

Calculation of a More Complicated Expression

Side Effects of Functions

The function concept in the programming languages is more general than that in the mathematical sense, as previously clarified. An important difference is that functions can have side effects. By side effects, we mean it not only calculates a value and that value will be returned, but it also changes something in its environment: changes the value of an object in the environment of a function, displays text to the screen, saves a file, sends an email on the network, etc.

The Python language even allows a function to not have a result object. In this case, its return value will be a None object . This makes sense, usually, when the function completes its task by causing a side effect (e.g., writing a string to display, writing to a file, etc.).

In Listing 2-4 the function writes Hello World to the screen. (This is a typical first example when teaching a program language.)
print('Hello World!')
Listing 2-4

Function with a Side Effect

Listing 2-5 shows a practical example, where the print() function has two arguments: one is the string containing the message, and the other is the total amount. These arguments will be printed on a single line separated by spaces . In Listing 2-6, you can see the reverse of the print() function, which reads a line instead of printing it. The line is a string closed by a linefeed; hence, the input() function reads characters until the Enter key is pressed. The result of reading a line does not contain the linefeed character. The input() function receives a parameter as well, a string, which is additionally written out before reading the line.
total_amount = 1000
print('Order total:', total_amount)
Listing 2-5

Function with Multiple Parameters

product_name = input('Please, enter the name of the product')
print('The name of the product:', product_name)
Listing 2-6

Input and Output Statements

Function Arguments

Arguments have so far been specified to functions by listing them in due order. This method of argument specification is referred to as positional parameter specification. However, it is possible to specify an argument not only according to position but also as a keyword argument.

There are two important arguments of the print() function that can be specified as keyword arguments: the sep and end arguments. These arguments can be strings: the first one specifies which separator character should be printed between the printed values, and the second one specifies what is printed at the end of the line. Among the arguments of the function, the positional arguments should always precede the keyword parameters.

The Python language allows positional or keyword arguments to be optional. In other words, the function will assign them a default value if they are not specified. The keyword arguments of print() can be omitted because they have default values. The default value for the sep parameter is a space, and the default value for the end parameter is a new line (linefeed) character. See Listing 2-7.
PRODUCT_NAME = 'Cube'
PRICE = 100
print('The product:', PRODUCT_NAME, end=' ')
print('(', PRICE, ' USD)', sep='')
Listing 2-7

Specification of Keyword Arguments

The other feature you can observe is that the print() function has any number of positional parameters. We speak of a variable-length parameter list, and at the function call you specify them as normal fixed parameters. This is useful in cases when it is not known in advance how many arguments will be specified.

Defining a Function

Now that you have seen how to call functions, you will learn how to define your own function. Defining a function always begins with the def keyword, followed by the name of the function, and the parentheses optionally with parameters; then the statements constituting the function come on new lines after a colon. Parameters are listed in parentheses separated by commas, and the values of these variables will be defined by the arguments during the call of the function.

Note

Function parameter or argument? When the variable names are listed in the function definition to be required at the function call, they are called parameters or formal arguments. When values are specified in the function call to be passed, they are called arguments or actual arguments. In practice, parameter and argument are often used imprecisely in an interchangeable way, and their exact meaning can be figured out from the context.

Listing 2-8 shows a function definition . It takes two parameters and returns the product of them. This function can be called as shown in Listing 2-9. The first argument (the value 1000) will be assigned to the price parameter, and the second argument (the value of 8) will be assigned to the amount parameter. The result of the function call shown in the example will be the value 8000.
def total_sum(price, quantity):
    return price * quantity
Listing 2-8

Function Definition

total_sum(1000, 8)
Listing 2-9

Call of the Defined Function

It is known from the indentation that statements belong to the function: they start with an indentation one level deeper as compared to the first line of the function definition. Lines after each other with the same indentation depth are referred to as blocks . Additionally, blocks can contain statements that require the following lines to be indented one more level deeper, i.e., so a novel, nested block is formed. The new block so formed is part of the block it is contained by. You can see a block in lines 2 and 3 in Listing 2-10, which are indented to the same level. This signals that it belongs to the definition in line 1. Line 4 appears on the top level, and actually lines 1 to line 4 are also considered as a block .
def total_sum(price, quantity):
    total = price * quantity
    return total
extra_price = 2000
Listing 2-10

Blocks

Caution

The space and the tab are two different characters. How many spaces are included in a tab depends on the settings. This is the reason mixed space and tab characters are not allowed to be used to indent blocks. The official recommendation is to use four spaces for every block. Compliance to this is assisted by development tools supporting the Python language: they replace tabs automatically with spaces.

A block corresponding to a function definition is called a function body . Statements forming the function can contain a return keyword and an expression after it. This statement indicates the end of the function and value of the expression will be the result of the function . When no such statement is present, the function has no results; i.e., its result is None as discussed earlier.

Keyword Arguments

If you want to pass arguments as keyword arguments , you can do it as shown in Listing 2-11. Parameters can be passed either as positional arguments or as keyword arguments, or as combination of the two, until the constraint of specifying positional arguments before keyword arguments is met. (There is no third variation in the example due to this constraint.)
total_sum(price=1000, quantity=8)
total_sum(1000, quantity=8)
Listing 2-11

Call of the Defined Function with Keyword Arguments

For the function definition in Listing 2-12, parameters get the default values. The parameters with default values are not allowed to be followed by parameters without default values . This is demonstrated in lines 1, 2, 3, and 4 of Listing 2-13, where for a default value the function can be called according to the previous one; this time arguments specified at the call will be passed. In lines 5, 6, 7, and 8, a single argument is specified, or no arguments are specified, and in these cases the not-specified parameters will be assigned with the default values. This is the way parameters modified on rare occasions can be made optional.
def total_sum_v2(price=500, quantity=5):
    return price * quantity
Listing 2-12

Function Definition with Default Parameters

total_sum_v2(1000, 8)
total_sum_v2(1000, quantity=8)
total_sum_v2(price=1000, quantity=8)
total_sum_v2(quantity=8, price=1000)
total_sum_v2(1000)
total_sum_v2(price=1000)
total_sum_v2(quantity=8)
total_sum_v2()
Listing 2-13

Calling the Defined Function with Various Arguments

Visibility of Names

After a variable name has been defined in the earlier examples, it can be referred to from the point of the definition. In the functions, in turn, if a variable name is defined, it cannot be referred to from “outside.” If the defined total value variable name is referred to from outside the definition of the function (in our case after the definition in the line where the statements are not indented) in Listing 2-14, an error will be raised. Visibility can be summarized in three points.
  • A name defined outside but before the function definition (in the block containing the definition of the function or any of the blocks containing the particular block) is visible from the function (only to access the object referenced by the name).

PRICE = 2000
def total_sum_v3(quantity):
    return PRICE * quantity
total_sum_v3(100)
Listing 2-14

Function Referencing to an Outer Variable Name

  • A name is assigned to an object in the function body (inside the block belongs to the function), and a reference can be made to the name only inside the function after the assignment; the name is not visible outside the function. If the name is identical to a name defined outside the function, this new assignment will be in effect, but it does not change the assignment outside the function. In other words, by leaving the function, the object assigned to the name outside the function will be reachable with the name (it will not be overwritten; it only will be shadowed within the function). In Listing 2-15, the result of the total_sum_v4 function call will be 30000.

PRICE = 2000
def total_sum_v4(quantity):
    PRICE = 3000
    return PRICE * quantity
total_sum_v4(100)
Listing 2-15

Function Referencing a Shadowed Outer Variable Name

  • The previous two use cases cannot be mixed. That is, if a value that was also defined outside the function is assigned at any point in the function body, the reference preceding the definition in the function body will refer to the name inside the function body (an error message is given in this case indicating that no object assignments took place to the name yet, instead of accessing the value outside the function).

Functions as Parameters

Functions are objects as well, like with numbers. Therefore, a function can be given as a value of a variable name, and it can be invoked. This is useful when there is behavior to be transferred. In Listing 2-16, three methods are defined: the first and second functions calculate the discount rate from the unit price and quantity, and the third function calculates the reduced price from the unit price, quantity, and discount rate calculation function. The last two lines show examples of how the reduced_price_p can be called. The discount_30 and the discount_4p1 return a 30 percent discount value if the unit price is higher than 500 and a rate that makes one item out of free free, respectively. The reduce_price_p function calculates the total_value from the price and quantity as a first step. Then it calls its discount parameter, which is a function, to retrieve the rate of the discount. Finally, it calculates the reduced price from the total_value and discount_rate. In the first and second examples, the results are 3500 and 4000, respectively.
def discount_30(price, quantity):
    return 0.3 if price>500 else 0
def discount_4p1(price, quantity):
    return ((quantity//5) / quantity)
def reduced_price_p(price, quantity, discount):
    total_value = price * quantity
    discount_rate = discount(price, quantity)
    return total_value * (1-discount_rate)
print(reduced_price_p(1000, 5, discount_30))
print(reduced_price_p(1000, 5, discount_4p1))
Listing 2-16

Function as Argument

Definitions of Nested Functions

In the Python language , the definition of a function is considered an ordinary statement. Therefore, it may be present in a block of any depth. This is the reason why you are allowed to define a function within a function if you want to use it only in the block. Listing 2-17 shows that two short functions being defined within the functions to calculate the discounted price.
def reduced_price_e(price, quantity, discount, limit = 5000):
    def total_sum():
         return price * quantity
    def d_available(total_value):
        return total_value >= limit
    multiplier = 1.0 - (discount
                    if d_available(total_sum())
                    else 0.0)
    return round(total_sum() * multiplier)
reduced_price_e(1000, 5, 0.3)
Listing 2-17

Nested Function Definition

Functions in Practice

Three types of notations can help you when defining functions: define the parameter and return types, specify the preconditions with respect to the parameters, and include a detailed documentation string. These three notations are completely optional and primarily do not affect the behavior of a function.

Similar to the way type hints were defined for variable names in Chapter 1, the types of function parameters can be written after the name, separated by a colon. The return type of the function can be written after the function parameters, separated from it by an arrow (->).

Function can begin with a so-called documentation string (docstring) , which provides a description of the function, that can be queried. The documentation string is a multiline comment that contains the task of the function briefly, on one line; its detailed description separated by one empty line; and finally, a description of the parameters after the Args: word.

After the documentation string, the preconditions can be represented to describe the conditions necessary for the function to work properly. These can be described with the assert statement : an assert keyword followed by a Boolean expression and optionally by an error message separated by a comma. If the preconditions expressed by the Boolean expression is not fulfilled, an error is given.

By convention, there is no space between the function name and the parentheses when calling the functions, as you saw in the examples. As to the commas, it is recommended to put a space only after the comma.

Listing 2-18 describes the process shown in Figure 2-1. Each step in the figure corresponds to one line of the example. This example demonstrates in practice how the source code can differ from the documentation. The gap can be narrowed by inserting comments into the source code identical to the ones in the figure and giving the function a name consistent with the documentation. The example uses the definitions of Listing 2-19.
def total_sum(price: int, quantity: int) -> int:
    """Calculates the total sum
    The total sum is the product of the price and quantity.
    Args:
        price: the unit price
        quantity: the quantity of the product
    Returns:
        the result of the computation, which is the value of the product
    """
    assert price >= 0, "the price cannot be negative"
    assert quantity >= 0, "the quantity cannot be negative"
    total_sum = price * quantity
    return total_sum
def discount_available(value: int, limit: int = 5000) -> bool:
    """Checks whether any discount is available
    Based on the limit it decides whether the discount can be applied
    Args:
        value: the total price of the product
        limit: a new limit if it differs from 5000
    Returns:
        True if discount is available, otherwise False
    """
    assert value >= 0, "the value cannot be negative"
    assert limit >= 0, "the limit cannot be negative"
    return value >= limit
def reduced_price(value: int, discount: float) -> int:
    """Calculate the discounted price
    Calculates the final price according to value and discount variables,
    which will be the discounted price if discount is available
    Args:
        value:     the total price of the product
        discount:  amount of the discount in fraction
                   (e.g.  0.5 equals 50%)
    Returns:
        The discounted price if discount is available, otherwise the original value
    """
    assert value >= 0, "the value cannot be negative"
    assert 1 >= discount >= 0, "discount is not in the valid range"
    multiplier = 1.0 - (discount
                    if discount_available(value)
                    else 0.0)
    return round(value * multiplier)
Listing 2-18

Function Definition

A flowchart of a Discount calculator. It begins with the start followed by Defines the unit price, 5500, Defines the quantity, 5, calculating the total sum, printing the discounted price and the last step being print whether any discount is available and this marks the end.

Figure 2-1

Discount calculator

PRICE: int = 5500 # unit price
QUANTITY: int = 5 # quantity
total_value = total_sum(PRICE, QUANTITY)
print('Total sum: ', reduced_price(total_value, 0.5))
print('5%' if discount_available(total_value) else 'None')
Listing 2-19

Discount Calculator

Advanced Details

This section describes some technical details in reference manual style and advanced concepts that may need more technical background.

Namespace and Scope

A namespace is an object that stores the mapping of names (variable names, function names, etc.) to objects. The outermost user-accessible namespace of the program is called the global namespace. In addition, there are so-called built-in namespaces that include the names of the built-in objects. The definition of the function creates a new namespace called a local namespace . Functions can contain further function definitions; thus, an arbitrary number of nested local namespaces can be constructed. This way, namespaces form a tree-like hierarchy, the root of which is the built-in namespace.

The scope of a variable name is part of the source code where a name of a namespace can refer. Resolution of a referenced name takes place by finding the assignment in the relevant local or global namespace. If this is unsuccessful, an attempt is made to find it in the next namespace outside the referenced one. The last namespace in the hierarchy is the built-in namespace. If it cannot be resolved even in this namespace, an error will be raised. Namespaces can “shadow name” each other in case an identical name is defined. This shadowing behavior can be changed by two statements: a local namespace outside of the local and global namespaces can be referenced by a nonlocal statement and a global statement (which consist of the keyword and the variable name), respectively.

Positional-Only and Keyword-Only Parameters

The function in Listing 2-20 has strange characters as parameters: / and *. These characters are not real parameters, but they have special meaning: all parameters that are preceding the / are positional-only parameters , and all parameters that are following the * signs are keyword-only arguments . The two function calls at the end of the listing are the only valid way to call the function f.
def f(a, /, b, *, c):
    print('positional-only parameter:', a)
    print('positional or keyword parameter:', b)
    print('keyword-only parameter:', c)
f(1, 2, c=3)
f(1, b=2, c=3)
Listing 2-20

Function with Positional-Only and Keyword-Only Parameters

Variable Number of Arguments

You saw in the examples of the function call in Listings 2-4 and 2-5 that functions can have a variable number of arguments . The notation that can be used for this is a star put in front of the parameter’s name. This parameter will then be a special parameter that does not behave like a simple positional parameter, but an object that will contain all the positional arguments that were not assigned to the preceding parameters. If there are two stars, the parameter will contain the keyword arguments that were not assigned to the preceding parameters.

This notation also works the other way around too. If such an object containing collected parameters is transferred with a star or with two stars, the contained values will be expanded. In Listing 2-21, the function f call prints “1, 2, 3” as positional parameters and then the “a: 1, b: 2, c: 3” pairs as keyword parameters .
def f(*args, **kwargs):
    print('positional parameters', args)
    print('keyword parameters', kwargs)
f(1, 2, 3, a=1, b=2, c=3)
Listing 2-21

Variable Number of Arguments

Lambda Expression

For functions expecting another function as their parameter, the definition of the function to be passed as a parameter is frequently clumsy. The lambda function can solve this problem; it’s a simple anonymous function definition containing a single expression. Listing 2-22 shows a discount_50() function that is transferred as an argument in line 4 to the reduced_price_p function, as shown in Listing 2-16. Listing 2-23 shows the replacement of the function calculating the previous discount by a lambda expression. A lambda expression in this example has two parameters: the price and the quantity to maintain the compatibility with the earlier function definition conventions, but only the quantity parameter used in the expression. As the lambda expression returns a function object, it can be written directly as a function argument of the reduced_price_p function.
def discount_50(price, quantity):
    return 0.5 if quantity>10 else 0
print(reduced_price_p(1000, 15, discount_50))
Listing 2-22

Simple Function as Parameter

print(reduced_price_p(1000, 15, lambda price, quantity: 0.5 if quantity>10 else 0))
Listing 2-23

Lambda Expression as Parameter

Decorator

A decorator is a special notation to modify the behavior of the function. The decorator is in fact a function receiving a function as a parameter and returning a function as a result. It can be used in two typical ways: it records the obtained function into some global data structure and returns with the function originally received, or it returns with another function instead of that received as a parameter. Depending on the purpose of this new function, it can carry out operations before and after calling the original function.

A decorator is presented in Listing 2-24, which checks if the functions calculating the discount function do not calculate too high of a discount rate. If they calculate too high of a discount rate, the decorator intervenes and returns the maximum allowed discount rate. Listing 2-16 shows the definition of the function reduced_price_p used in the example.
def limit_discount(discount):
    def check(price, quantity):
        pre_calculation = discount(price, quantity)
        return pre_calculation if pre_calculation<0.75 else 0.75
    return check
@limit_discount
def discount_40(price, quantity):
    rate = 0.4 if price*quantity > 500 else 0
    return rate
@limit_discount
def sell_for_free(price, quantity):
    rate = 1.0 if quantity == 1 else 0
    return rate
print(reduced_price_p(1000, 1, discount_40))
print(reduced_price_p(1000, 1, sell_for_free))
Listing 2-24

Decorators

Yield Statement and Asynchronous Functions

Functions that contain yield statements behave differently than the functions discussed in this chapter. They are called generator functions or co-routines, and you can find more information on them at the end of Chapter 4. These kinds of functions are the core building blocks of so-called asynchronous functions. We discuss them separately in Appendix C.

Key Takeaways

  • A function call is an expression that can be denoted by writing parentheses (this is called a function call operator) after the name of the function. The parentheses can contain further expressions that are the arguments of the function. Calling a function means executing the statements assigned to the function. The function usually describes some calculations or changes its environment. The function call usually returns a value that can be used.

  • Defining a function requires nothing more than assigning a block (list of statements) to a name. In the definition of a function, the parameters expected by the function can be listed. These parameters will be assigned to values during the call. The statements assigned to the defined function may contain a return statement, which can prescribe what object must be returned to the caller.

  • It is important to note that the variables defined inside the function cannot be accessed from outside (they form a new namespace). The external variables can be accessed from the function if they meet certain criteria. If you define a variable inside the function definition with the name identical to an external variable, then this variable will be shadowed by the new variable and changing it won’t have any effect on the external variable.

  • The functions are also objects , like objects of the other types shown so far. Although they are defined in a different way than other objects assigned to variable names, a function is actually a variable name. Because of this, a function name can be assigned to another variable name or can be used as an argument.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset