Recap

In this part of the beginner series, we will build upon the code from the last post, introducing more layers and moving parts. We will discuss the Python namespace and how Python keeps track of all the objects we have defined in our program. Additionally, we will learn a little bit about libraries and how we can use them to import code into our namespace (although we won't be importing anything just yet) and built-in objects (which we've actually already used). We will also cover user-defined functions and functions in general.

Built-in objects and the namespace

Built-in functions are functions (we touched briefly on this last time) that exist naturally in the Python namespace. The namespace sounds very arcane, but essentially it is a collection of already defined names for things that we can use in our code.

Namespaces

I asked chatGPT to explain the namespace, to which it replied:

Python namespace is a collection of objects (variable, function, class, etc.) that can be accessed by a unique name. It's basically a way for Python to keep track of all the different objects that you have defined in your program, and make sure that each object has a unique name. This helps to prevent confusion and name collisions between different objects in your program.

We can import things in to our namespace, as we'll see in later posts, to bring in more items (functions and classes) to write our code. Importing external code will be useful when we start using libraries like pandas, Python's data manipulation library. A library is a bunch of code already written for you so you don't have to reinvent the wheel. When you import those libraries into your code you are then able to use them once you get them into the namespace.

But there are certain objects that already exist without any necessary importing. These are called "built-in" objects.

One such built-in function here is the print function, which we used in the last post.

print('Hello World!')
Hello World!

We touched only briefly on functions last time, but essentially in Python, functions are blocks of code that are defined once and can be used repeatedly throughout a program. Functions allow for efficient and organized code by breaking down tasks into smaller, reusable chunks. They can accept arguments (inputs) and return values (outputs). Functions can also be used to create modules, which are collections of related functions that can be imported into other Python scripts (as discussed, we'll be exploring some of these modules/libraries in later posts like pandas). Functions are a key part of Python programming and are used to make code more modular, maintainable, and readable.

User-defined Functions

User defined functions in Python are functions that are created and defined by the user, rather than being pre-defined in the language itself. Using a fantasy football example, a user may define a function that calculates the total number of points scored by a player in a given week. This function would take in parameters such as the player's name, their position, and the number of rushing yards, receiving yards, etc. they had, and would return the total number of points scored. This user defined function could then be called and used multiple times throughout the program to calculate points for different players.

To write a user-defined function in Python using an NFL example, you could follow these steps:

1. Define the function by using the keyword def followed by the function name and any parameters that will be passed to the function. For example, a function that calculates a player's average yards per carry might be defined like this:

def avg_yards_per_carry(yards, carries):

2. Inside the function, write the code that will perform the desired calculation. In the example above, this would be the code that divides the player's total yards by their number of carries:

avg = yards / carries

3. Return the result of the calculation by using the return keyword. In the example above, this would be the player's average yards per carry:

return avg

4. Call the function in your code by using the function name followed by any necessary arguments. For example, to calculate the average yards per carry for a player who has rushed for 100 yards on 20 carries, you would write:

avg_yards = avg_yards_per_carry(100, 20)

5. Print the result of the function call to see the result of the calculation. In the example above, this would be the player's average yards per carry:

print(avg_yards)  # Output: 5.0

Overall, a user-defined function in Python using an NFL example might look like this:

def avg_yards_per_carry(yards, carries):
    avg = yards / carries
    return avg

avg_yards = avg_yards_per_carry(100, 20)
print(avg_yards) 
5.0

Now let's take all of this new information and use it to make our example from the last post even better. How can we modularize our code in a way that makes it better? The answer is that we can write a function that takes in a player's name, number of catches and targets and outputs a human-readable string that tells us that player's catch rate.

def calculate_catch_rate(player_name, catches, targets):
        catch_rate = catches / targets
        catch_rate_rounded = round(catch_rate, 2)
        print(player_name, 'had a catch rate of ', catch_rate_rounded)
        
calculate_catch_rate('Justin Jefferson', catches=10, targets=12)
Justin Jefferson had a catch rate of  0.83

This is our simple Python function that calculates the catch rate of a player. The function takes in three arguments:

  • player_name , which is a string representing the name of the player;
  • catches , which is an integer representing the number of catches the player had; and
  • targets , which is an integer representing the number of times the player was targeted.
  • The function first calculates the catch rate by dividing the number of catches by the number of targets. It then rounds the catch rate to two decimal places and prints out a string that includes the player's name and their catch rate.

    In the example provided, the function is called with the player name "Justin Jefferson", 10 catches, and 12 targets. This means that Justin Jefferson had a catch rate of 83.33%, which gets rounded to 0.83 and gets printed out as part of the string.

    Positional and Keyword Arguments

    Here you can also see that we explicitly defined arguments when passing them in. "Justin Jefferson" was a positional argument and catches and targets were keyword arguments.

    In programming, a keyword argument is a type of argument that is passed to a function or method in which the name of the argument is specified in the function call. This allows for greater clarity and readability of the code, as the name of the argument clearly indicates its purpose.

    As an example, consider a function that calculates the average rushing yards per game for a given NFL player. This function might have a keyword argument called "player_name" that specifies the name of the player for whom the average rushing yards per game should be calculated. The function could then be called like this:

    calculate_avg_rushing_yards_per_game(player_name="Austin Ekeler")

    In this example, the "player_name" keyword argument is used to specify the player for whom the average rushing yards per game should be calculated. The use of a keyword argument makes the code more readable and easier to understand, as it is clear that the "player_name" argument is used to specify the player in question. Our example above is no different.

    Let's circle back to introducing this new idea of user-defined functions in to our code.

    players = [
        {
        "name": "Tyreek Hill",
        "catches": 81,
        "targets": 106
        },
        {
        "name": "Stefon Diggs",
        "catches": 72,
        "targets": 99
        },
        {
        "name": "Justin Jefferson",
        "catches": 69, # nice
        "targets": 100
        }
    ]
    
    for player in players:
        calculate_catch_rate(player['name'], catches=player['catches'], targets=player['targets'])
    Tyreek Hill had a catch rate of  0.76
    Stefon Diggs had a catch rate of  0.73
    Justin Jefferson had a catch rate of  0.69
    

    As you can see here, we bring in our list of dictionary objects, each containing information about 3 NFL WR's - Tyreek Hill, Stefon Diggs, and Justin Jefferson.

    We then iterate through each of our player objects and pass in their data to the function, which provides the desired output! Compare this to our previous post and you could probably notice that the code is a lot cleaner now.

    Concluding Thoughts

    We covered a decent amount of theory in this post. To recap, we:

  • Expanded our knowledge of functions
  • Acknowledged that some of these functions are built-in to the namespace
  • Some of them can be imported via external libraries (which we will cover in the future), and
  • Some of these can be written by us as user-defined functions
  • We also covered in-depth what functions are and some other details like positional and keyword arguments.

    Using all of this, we took our code from last time and made it slightly more Pythonic (emphasis on slightly because at the moment, our code isn't all that more Pythonic yet, but we'll touch on this topic more in future posts. "Pythonic" code is essentially Python code that is written with good style/convention).

    No groundbreaking analysis yet, but we're one step closer to do some real analysis in Python!

    Thanks for reading - You guys are all awesome and happy coding!