<jupyter_start><jupyter_text>Functions in Python You use functions in programming to bundle a set of instructions that you want to use repeatedly or that, because of their complexity, are better self-contained in a sub-program and called when needed. That means that a function is a piece of code written to carry out a specified task. To carry out that specific task, the function might or might not need multiple inputs. When the task is carried out, the function can or can not return one or more values.There are 3 types of functions in Python:1. **Built-in** functions, such as:     * `help()` - to ask for help    * `min()` - to get the minimum value    * `print()` - to print an object to the terminal, and so on2. **User-Defined** Functions (UDFs), which are functions that users create to help them out3. **Anonymous** functions, which are also called `lambda` functions because they are not declared with the standard `def` keyword. Functions vs MethodsA method refers to a function which is part of a `class`. You access it with an instance or object of the class. A function doesn’t have this restriction: it just refers to a standalone function. This means that **All methods are functions, but not all functions are methods**Consider this example, where you first define a function `plus()` and then a Summation `class` with a `sum()` method:<jupyter_code># Define a function `plus()`
def plus(a, b):
  return a + b
  
# Create a `Summation` class
class Summation(object):
  def sum_1(self, a, b):
    self.contents = a + b
    return self.contents<jupyter_output><empty_output><jupyter_text>If you want to call the `sum_1()` method that is part of the Summation `class`, you first need to define an instance or object of that class. So, let’s define such an object:<jupyter_code># Instantiate `Summation` class to call `sum()`
sum_instance = Summation()
print('sum_instance.sum_1:', sum_instance.sum_1(1,2))

# Just call `plus()` function
print('plus():', plus(1,2))<jupyter_output>sum_instance.sum_1: 3
plus(): 3<jupyter_text>Parameters vs Arguments**Parameters** - are the names used when defining a function or a method, and into which arguments will be mapped. In other words, **arguments** - are the things which are supplied to any function or method call, while the function or method code refers to the arguments by their parameter names. What happened to self?The 1st argument (`self`) of every class method is always a reference to the current instance of the `class` (Summation). By convention, this argument is called `self`.This all means that you don’t pass the reference to `self` in this case because `self` is the parameter name for an implicitly passed argument that refers to the instance through which a method is being invoked. It gets inserted implicitly into the argument list. How To Define A Function: User-Defined Functions (UDFs)The four steps to defining a function in Python are the following:1. Use the keyword `def` to declare the function and follow this up with the function name.2. Add parameters to the function: they should be within the parentheses `()` of the function. End your line with a colon `:`.3. Add statements that the functions should execute.4. End your function with a `return` statement if the function should output something. Without the `return` statement, your function will return an object `None`.<jupyter_code>def hello():
  print("Hello World") 
  return

print('hello() returns object:', hello())<jupyter_output>Hello World
hello() returns object: None<jupyter_text>Of course, your functions will get more complex as you go along: you can add for loops, flow control, … and more to it to make it more finegrained:<jupyter_code>def hello():
  name = str(input("Enter your name: "))
  if name:
    print ("Hello " + str(name))
  else:
    print("Hello World") 
  return 
  
hello()<jupyter_output>Enter your name:  Roman<jupyter_text>The return StatementNote that as you’re printing something in your UDF `hello()`, you don’t really need to return it. There won’t be any difference between the function above and this one:<jupyter_code>def hello():
  print("Hello World") 
  return("hello")

def hello_noreturn():
  print("Hello World")
  
# Multiply the output of `hello()` with 2 
hello() * 2

# (Try to) multiply the output of `hello_noreturn()` with 2 
hello_noreturn() * 2<jupyter_output>Hello World
Hello World<jupyter_text>However, if you want to continue to work with the result of your function and try out some operations on it, you will need to use the `return` statement to actually return a value, such as a String, an integer, e.t.c. Consider the following scenario, where `hello()` returns a String "hello", while the function `hello_noreturn()` returns `None`:The second function gives you an error because you can’t perform any operations with a `None`. You’ll get a `TypeError` that says that you can’t do the multiplication operation for `NoneType` (the `None` that is the result of `hello_noreturn()`) and `int` (2).**NOTE:** functions immediately exit when they come across a `return` statement, even if it means that they won’t return any value:<jupyter_code>def run():
  for x in range(10):
     if x == 2:
       return
  print("Run!")
  
print('run() returns object:', run())<jupyter_output>run() returns object: None<jupyter_text>Another thing that is worth mentioning when you’re working with the `return` statement is the fact that you can use it to return multiple values. To do this, you make use of tuples.**Remember:** that this data structure is very similar to that of a `list`: it can contain multiple values. However, tuples are immutable, which means that you can’t modify any amounts that are stored in it! You construct it with the help of double parentheses `()`. You can unpack tuples into multiple variables with the help of the comma `,` and the assignment operator `=`.<jupyter_code>def plus(a, b):
  total_sum = a + b
  return (total_sum, a)

# Call `plus()` and unpack variables 
total_sum, a = plus(3,4)

print('total_sum: ', total_sum, '\na: ', a, sep='')<jupyter_output>total_sum: 7
a: 3<jupyter_text>Note that the `return` statement return sum, a would have the same result as return `(sum, a)`: the former actually packs sum and a into a tuple under the hood! How To Add Docstrings To A Python Function**Docstrings** - describe what your function does, such as the computations it performs or its return values. These descriptions serve as documentation for your function so that anyone who reads your function’s docstring understands what your function does, without having to trace through all the code in the function definition.Function docstrings are placed in the immediate line after the function header and are placed in between triple quotation marks `""" ... """`. An appropriate Docstring for your `hello()` function is ‘Prints “Hello World”’.<jupyter_code>def hello():
    """ 
    Prints "Hello World".

    Returns:
        None
    """
    print("Hello World") 
    return<jupyter_output><empty_output><jupyter_text>Function Arguments in PythonThere are 4 types of arguments that Python UDFs can take:* Default arguments* Required arguments* Keyword arguments* Variable number of arguments Default Arguments**Default arguments** - are those that take a default value if no argument value is passed during the function call. You can assign this default value by with the assignment operator `=`:<jupyter_code>def plus(a, b = 2):
  return a + b
  
# Call `plus()` with only `a` parameter
print('plus(a=1):', plus(a=1))

# Call `plus()` with `a` and `b` parameters
print('plus(a=1, b=3):', plus(a=1, b=3))<jupyter_output>plus(a=1): 3
plus(a=1, b=3): 4<jupyter_text>Required ArgumentsAs the name kind of gives away, the required arguments of a UDF are those that have to be in there. These arguments need to be passed during the function call and in precisely the right order:<jupyter_code>def plus(a, b):
  return a + b

print('plus(a=1):', plus(a=1))<jupyter_output><empty_output><jupyter_text>You need arguments that map to the `a` as well as the `b` parameters to call the function without getting any errors. If you switch around `a` and `b`, the result won’t be different, but it might be if you change `plus()` to the following:<jupyter_code>def plus(a, b):
  return a / b

plus(3, 4)<jupyter_output><empty_output><jupyter_text>Keyword ArgumentsIf you want to make sure that you call all the parameters in the right order, you can use the keyword arguments in your function call. You use these to identify the arguments by their parameter name:<jupyter_code>def divide(a,b):
  return a / b
  
# Call `plus()` function with parameters 
print('divide(2,3):', divide(2,3))
print('divide(3,2):', divide(3,2), '\n----------')

# Call `plus()` function with keyword arguments
print('divide(a=1, b=2):', divide(a=1, b=2))
print('divide(b=2, a=1):', divide(b=2, a=1))<jupyter_output>divide(2,3): 0.6666666666666666
divide(3,2): 1.5 
----------
divide(a=1, b=2): 0.5
divide(b=2, a=1): 0.5<jupyter_text>Note that by using the keyword arguments, you can also switch around the order of the parameters and still get the same result when you execute your function Variable Number of ArgumentsIn cases where you don’t know the exact number of arguments that you want to pass to a function, you can use the following syntax with `*args`:<jupyter_code>def plus(*args):
    
    print('type(args):', type(args), '\nargs:', args)

    return sum(args)

plus(1,4,5)<jupyter_output>type(args): <class 'tuple'> 
args: (1, 4, 5)<jupyter_text>The asterisk `*` is placed before the variable name that holds the values of all non-keyword variable arguments. **NOTE:** you might as well have passed `*varint`, `*var_int_args` or any other name to the `plus()` function.The special syntax `**kwargs` in function definitions in python is used to pass a keyworded, variable-length argument list. We use the name `kwargs` with the double star `**`. The reason is because the double star allows us to pass through keyword arguments (and any number of them).A keyword argument is where you provide a name to the variable as you pass it into the function.One can think of the kwargs as being a `dictionary` that maps each keyword to the value that we pass alongside it. That is why when we iterate over the kwargs there doesn’t seem to be any order in which they were printed out.<jupyter_code>def myFun(**kwargs):
    
    print('type(kwards):', type(kwargs), '\nkwards:', kwargs, '\n----------')
    
    for key, value in kwargs.items():
        print ("%s == %s" % (key, value))
 
myFun(first ='1', mid ='0', last='1')   

# Python program to illustrate  **kwargs for
# variable number of keyword arguments with
# one extra argument.
 
def myFun(arg1, **kwargs):
    
    print('----------\n', 
          'arg1: ', arg1, 
          '\ntype(kwards): ', type(kwargs), 
          '\nkwards: ', kwargs, 
          '\n----------', sep = '')
    
    for key, value in kwargs.items():
        print ("%s == %s" %(key, value))
 
myFun("Hi", first ='Geeks', mid ='for', last='Geeks')<jupyter_output>type(kwards): <class 'dict'> 
kwards: {'first': '1', 'mid': '0', 'last': '1'} 
----------
first == 1
mid == 0
last == 1
----------
arg1: Hi
type(kwards): <class 'dict'>
kwards: {'first': 'Geeks', 'mid': 'for', 'last': 'Geeks'}
----------
first == Geeks
mid == for
last == Geeks<jupyter_text>Global vs Local VariablesIn general, variables that are defined inside a function body have a **local scope**, and those defined outside have a **global scope**. That means that local variables are defined within a function block and can only be accessed inside that function, while global variables can be obtained by all functions that might be in your script:<jupyter_code># Global variable `init`
init = 1

def plus(*args):
    # Local variable `total`
    total = 0
  
    for i in args:
        total += i
    
    return total
  
# Access the global variable
print("This is the initialized value " + str(init))

# (Try to) access the local variable
print("This is the sum " + str(total))<jupyter_output>This is the initialized value 1<jupyter_text>You’ll see that you’ll get a `NameError` that says that the name `total` is not defined when you try to print out the local variable `total` that was defined inside the function body. The `init` variable, on the other hand, can be printed out without any problems. Anonymous Functions in Python**Anonymous functions** - are also called `lambda` functions in Python because instead of declaring them with the standard `def` keyword, you use the `lambda` keyword:<jupyter_code># Anonymous function
double = lambda x: x*2

double(5)<jupyter_output><empty_output><jupyter_text>`lambda x: x*2` - is the anonymous or `lambda` function:* `x` is the argument* `x*2` is the expression or instruction that gets evaluated and returnedWhat’s special about this function is that it has no name. If you had to write the above function in a UDF, the result would be the following:<jupyter_code>def double(x):
    return x*2

double(5)<jupyter_output><empty_output><jupyter_text>Let’s consider another example of a lambda function where you work with two arguments:<jupyter_code># `sum()` lambda function
sum = lambda x, y: x + y;

# Call the `sum()` anonymous function
print('The `sum()` anonymous function:', sum(4,5))

# "Translate" to a UDF
def sum(x, y):
    return x + y

print('The `sum()` UDF function:', sum(4,5))<jupyter_output>The `sum()` anonymous function: 9
The `sum()` UDF function: 9<jupyter_text>You use anonymous functions when you require a nameless function for a short period of time, and that is created at runtime. Specific contexts in which this would be relevant is when you’re working with `filter()`, `map()` and `reduce()`:<jupyter_code>from functools import reduce

my_list = [1,2,3,4,5,6,7,8,9,10]

# Use lambda function with `filter()`
# The filter() function filters (as the name suggests) the original
# Input list `my_list` on the basis of a criterion `value > 10`
filtered_list = list(filter(lambda x: (x*2 > 10), my_list))

# Use lambda function with `map()`
# With map() you apply a function to all items of the list `my_list`. 
# In this case, you multiply all elements with 2.
mapped_list = list(map(lambda x: x*2, my_list))

# Use lambda function with `reduce()`
# You use `reduce()` function cumulatively to the items of the `my_list` list, 
# From left to right and reduce the sequence to a single value, 55, in this case.
reduced_list = reduce(lambda x, y: x+y, my_list)

print(filtered_list)
print(mapped_list)
print(reduced_list)<jupyter_output>[6, 7, 8, 9, 10]
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
55<jupyter_text>**NOTE:** that the `reduce()` function is part of the `functools` library. Using main() as a FunctionIf you have any experience with other programming languages such as Java, you’ll know that the `main()` function is required to execute functions. As you have seen in the examples above, this is not necessarily needed for Python. However, including a `main()` function in your Python program can be handy to structure your code logically - all of the most important components are contained within this `main()` function.You can easily define a `main()` function and call it just like you have done with all of the other functions above:<jupyter_code>def main():
    print("This is a main function")

main()<jupyter_output>This is a main function<jupyter_text>However, as it stands now, the code of your `main()` function will be called when you import it as a `module`. To make sure that this doesn’t happen, you call the `main()` function when `__name__ == '__main__'`.That means that the code of the above code chunk becomes:<jupyter_code>def main():
    print("This is a main function")
  
if __name__ == '__main__':
    main()<jupyter_output>This is a main function<jupyter_text>__name__ == '__main__' explanationPython files are called **modules** and they are identified by the `.py` file extension. A `module` can define:* Functions* Classes* VariablesSo when the interpreter runs a module: * The `__name__` variable will be set as `__main__` if the module that is being run is the **main program**, * If the code is importing the module from another module, then the `__name__`  variable will be set to that **module’s name**.Let's take a look at an example. Create a Python module named file_one.py and paste this top level code inside:<jupyter_code>print("File one __name__ is set to: " + __name__)<jupyter_output>File one __name__ is set to: __main__<jupyter_text>By running this file you will see exactly what we were talking about. The variable `__name__` for this module is set to `__main__`. Python File Naming ConventionsThe usual way of using `__name__` and `__main__` looks like this:<jupyter_code>if __name__ == "__main__":
    # Here should be code to be executed when running the file as a main program (script)
    ...

print(__name__)<jupyter_output>__main__
{'fraction_non_alphanumeric': 0.10202038051481416, 'fraction_numerical': 0.010249160629086412, 'mean_word_length': 4.6237164624047695, 'pattern_counts': {'":': 1, '<': 79, '<?xml version=': 0, '>': 81, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '23', 'dir': 'jupyter-structured-clean-dedup', 'id': '399317', 'license': 'MIT', 'n_tokens_mistral': 5019, 'n_tokens_neox': 4822, 'n_words': 2344, 'path': 'python/functions.ipynb', 'repo_name': 'RenZorRUS/apollo-lib', 'size': '16977'}