.. _more_on_functions: More on Functions ================= From the materials you have covered up to this point you should have mastered the basics of writing functions. In particular, you know that functions can contain a chunk of code that can be written once, and used multiple times from other parts of the code. You know that you can pass values into the function, and that it can return values to the "calling" code. Now we will dig a bit deeper down into the specifics of functions in Python: Variable scope -------------- Defining a function: .. code-block:: python def fun(x, y): z = x + y return z ``x``, ``y``, ``z`` are *local* names. ``x`` and ``y`` because they are function *parameters* ``z`` because it was "bound" inside the function. Local vs. Global ................ Names bound in Python have a *scope* That *scope* determines where a name is visible, and what value it has in a given block. .. code-block:: ipython In [14]: x = 32 In [15]: y = 33 In [16]: z = 34 In [17]: def fun(y, z): ....: print(x, y, z) ....: In [18]: fun(3, 4) 32 3 4 ``x`` is global, while ``y`` and ``z`` are local to the function. But, did the value of ``y`` and ``z`` change in the *global* scope? .. code-block:: ipython In [19]: y Out[19]: 33 In [20]: z Out[20]: 34 No -- they did not. The "y" and "z" names *inside* the function are completely separate from the "y" and "z" outside the function. The ones outside the function are "global" names. When you use a name in Python, it first checks if it's a local name. Then, if that name is not in the local scope, it will look in the global scope for it. **NOTE:** "global" in Python means global to the module (generally a single file), not global to an entire program. Which is really good, as you have little way of knowing what names are being used in packages you are using, but are not writing yourself! In general, you should use global names mostly for constants. The Python convention is to designate global constants by typing the names we bind to them in ALL_CAPS: .. code-block:: python INSTALLED_APPS = ['foo', 'bar', 'baz'] CONFIGURATION_KEY = 'some secret value' ... This is just a convention, but it's a good one to follow. Global Gotcha ............. Take a look at this function definition: .. code-block:: ipython In [21]: x = 3 In [22]: def f(): ....: y = x ....: x = 5 ....: print(x) ....: print(y) ....: What is going to happen when we call ``f``? Try it and see: .. code-block:: ipython In [34]: f() --------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) in () ----> 1 f() in f() 1 def f(): ----> 2 y = x 3 x = 5 4 print(x) 5 print(y) UnboundLocalError: local variable 'x' referenced before assignment Because you are binding the symbol ``x`` locally, it becomes a local and masks the global value already bound. So in the line that caused the error: .. code-block:: python y = x Python knows that x is a local name, as it is assigned on the next line. But on this line, x has not yet been given a value -- hence the error. Globals are "read only" ....................... While you have access to the global names in side a function, you can't change what those names are bound to. Take a look at the previous examples -- when we set a new value to a name (using the equal sign), that makes the name local -- so it will not change what the global name refers to. Parameters ---------- So far we've seen simple parameter lists: .. code-block:: python def fun(x, y, z): print(x, y, z) These types of parameters are called *positional* When you call a function, you **must** provide arguments for all *positional* parameters *in the order they are listed*. Defaults for parameters: ........................ You can provide *default values* for parameters in a function definition: .. code-block:: ipython In [24]: def fun(x=1, y=2, z=3): ....: print(x, y, z) ....: When parameters are given with default values, they become *optional*. .. code-block:: ipython In [25]: fun() 1 2 3 You can provide arguments to a function call for *optional* parameters positionally: .. code-block:: ipython In [26]: fun(6) 6 2 3 In [27]: fun(6, 7) 6 7 3 In [28]: fun(6, 7, 8) 6 7 8 Or, you can use the parameter name as a *keyword* to indicate which you mean: .. code-block:: ipython In [29]: fun(y=4, x=1) 1 4 3 This allows you to specify only those optional parameters that you need to, and keep using the defaults for the rest. This is a very powerful feature of Python -- you'll find it's common to have a pretty long optional parameter list to functions. It allows a lot of flexibility (the hard stuff is possible), while in common use, it's easy to use (the easy stuff is easy). Once you've provided a *keyword* argument in this way, you can no longer provide any *positional* arguments: .. code-block:: ipython In [30]: fun(x=5, 6) File "", line 1 fun(x=5, 6) SyntaxError: non-keyword arg after keyword arg Recursion --------- You've seen functions that call other functions. If a function calls *itself*, we call that **recursion**. Like with other functions, a call within a call establishes a *call stack*. With recursion, if you are not careful, this stack can get *very* deep. Python has a maximum limit to how much it can recurse. This is intended to save your machine from running out of RAM. Recursion can be Useful ----------------------- Recursion is especially useful for a particular set of problems. For example, take the case of the *factorial* function. In mathematics, the *factorial* of an integer is the result of multiplying that integer by every integer smaller than itself down to 1. :: 5! == 5 * 4 * 3 * 2 * 1 We can use a recursive function nicely to model this mathematical function: .. code-block:: python def fact(n): """compute the factorial of the input value, n""" if n == 0: return 1 else: return n * fact(n-1) This is a typical structure for a recursive function: A) It starts with a check to see if the recursive process is "done" -- can it simply return a simple value. B) If not, then it does a computation using the same function with another value. It is critical that the first check is there, or the function will never terminate. Further Reading --------------- Here's a nice blog post about writting better functions: https://jeffknupp.com/blog/2018/10/11/write-better-python-functions/