๐ŸPython

Python Fundamentals

Welcome to the Python Fundamentals course. In this course, we will cover the basics of the Python programming language. We will start by discussing how to create variables, use conditions and loops, and import libraries. By the end of this course, you should have a solid understanding of the basics of Python and be ready to start writing your own programs.

Variables

In Python, a variable is a way to store a value or a reference to an object. To create a variable, you simply give it a name and assign it a value using the assignment operator (=). For example:

x = 5

This creates a variable named x and assigns it the value of 5.

Conditions

Conditions are used to make decisions in a program. The most common type of condition is the if-else statement. For example:

x = 5
if x > 0:
    print("x is positive")
else:
    print("x is not positive")

This code checks if the value of x is greater than 0. If it is, the program will print "x is positive", otherwise it will print "x is not positive".

Loops

Loops are used to repeat a block of code multiple times. The two most common types of loops in Python are the for loop and the while loop.

The for loop is used to iterate over a sequence of items, such as a list or a string. For example:

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

This code will print each fruit in the list, one at a time.

The while loop is used to repeat a block of code as long as a certain condition is true. For example:

x = 5
while x > 0:
    print(x)
    x -= 1

This code will print the numbers from 5 down to 1, because the condition (x > 0) is true for each iteration of the loop.

Importing Libraries

Python has a large number of libraries that can be imported to add additional functionality to your programs. To import a library, you use the import keyword followed by the name of the library. For example:

import math

This imports the math library, which provides mathematical functions such as sqrt() and sin().

Once a library is imported, you can use its functions by referencing them with the library name as a prefix. For example:

import math
x = math.sqrt(16)
print(x)

This code imports the math library and then uses the sqrt() function to calculate the square root of 16. The result is printed on the screen.

That's it for the basics of Python! You should now have a solid understanding of how to create variables, use conditions and loops, and import libraries in Python. You can now start writing your own programs and experimenting with the language.

Functions

Functions allow you to define a block of code that can be reused multiple times in your program. This makes your code more organized and easier to maintain.

def greet(name):
    print("Hello, " + name + "!")

greet("John")
greet("Jane")

This code defines a function named greet() that takes a single argument, "name", and prints a greeting using the value of that argument. The function is then called twice with different arguments.

Functions can also return a value using the return statement. For example:

def add(a, b):
    return a + b

result = add(3, 4)
print(result)

This code defines a function named add() that takes two arguments and returns their sum. The function is then called and the result is stored in a variable and printed.

Classes and Objects

Python is an object-oriented programming language, which means you can define classes to create objects. Classes are templates for objects that defines the properties and methods of an object.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print("Woof!")

dog1 = Dog("Fido", 3)
dog1.bark()

This code defines a class named Dog, which has two properties (name and age) and one method (bark()). An object of the class Dog is created with the name "Fido" and age 3, and the method bark() is called on the object.

Classes can also inherit from other classes using the class DerivedClass(BaseClass): syntax. This allows you to reuse existing code and add new functionality.

File Handling

File handling is an important concept in python, it allows you to read and write files. It's useful when you need to save the data or load it from a file.

f = open("file.txt", "r")
print(f.read())
f.close()

This code opens a file named "file.txt" in read mode, reads the contents of the file, and then closes the file. It's important to close the file after you finish working with it.

You can also write to a file using the write() method:

f = open("file.txt", "w")
f.write("Hello, world!")
f.close()

This code opens a file named "file.txt" in write mode, writes the string "Hello, world!" to the file, and then closes the file.

Exception Handling

Exception handling is a way to handle errors and unexpected situations that may occur in your program. It allows you to continue the execution of the program instead of it crashing.

try:
    x = 1/0
except ZeroDivisionError:
    print("Cannot divide by zero.")

This code tries to divide 1 by 0, which would normally cause a ZeroDivisionError. But the exception is caught by the except block, which prints a message instead of crashing the program.

You can also handle multiple exceptions in one block using the except ExceptionType1, ExceptionType2 syntax. And you can use a finally block to include code that should always be executed, regardless of whether an exception occurs or not.

try:
    x = 1/0
except (ZeroDivisionError, ValueError):
    print("Invalid operation.")
finally:
    print("Execution finished.")

It's also possible to raise your own exceptions using the raise statement. This can be useful for signaling that a specific condition has been met or that an error has occurred.

def check_age(age):
    if age < 0:
        raise ValueError("Age must be greater than 0")
    if age > 120:
        raise ValueError("Age must be lower than 120")

try:
    check_age(-5)
except ValueError as e:
    print(e)

With these concepts, you should have a solid understanding of how to create functions, classes, and handle files and exceptions in Python. You can now start writing your own programs and experimenting with these advanced features of the language.

Upgrade

The pip3 install --upgrade python command is used to upgrade the Python interpreter on your system. This command uses the Python Package Index (PyPI) to find and download the latest version of Python and its associated packages. The pip3 command is a package manager for Python and is used to install and manage packages and modules. By specifying the --upgrade flag, pip3 will upgrade any packages that are outdated or require updating to the latest version available on PyPI. It is important to note that upgrading Python may cause compatibility issues with existing code, so it is recommended to thoroughly test any applications after upgrading to ensure they still function correctly.

pip3 install --upgrade python

The pip3 install --upgrade pip command is used to upgrade the version of pip that is installed on your system. Pip is a package manager for Python that makes it easy to install and manage Python packages. When you run this command, pip will check for a newer version of itself and then download and install it if one is available. This will ensure that you have the latest version of pip, which may include bug fixes, security updates, and new features. It's generally a good idea to keep your tools up-to-date to take advantage of these improvements and avoid potential issues caused by outdated software.

pip3 install --upgrade pip

The pip install --upgrade certifi command is used to upgrade the Certifi package, which is a collection of root certificates for validating the trustworthiness of SSL/TLS connections. This command will search for the latest version of the Certifi package available on the Python Package Index (PyPI) and upgrade the currently installed package to that version if it exists. The pip install --upgrade command ensures that the package is upgraded to the latest version and all dependencies are also updated if necessary. This command is useful for ensuring that your Python environment has the most up-to-date root certificates to ensure secure and trusted connections over SSL/TLS.

pip3 install --upgrade certifi

Decorators

Introduction to Decorators

Decorators are a powerful feature in Python that allow you to modify or extend the behavior of functions or methods without changing their code. They are typically used to add functionality to existing functions, such as logging, caching, measuring execution time, etc.

In this chapter, we will explore how to create and use decorators using a practical example. We will implement a simple tictoc decorator that measures the execution time of two functions, do_this and do_that.

Understanding the Code

Before we delve into creating the decorator, let's first understand the code you provided. The code defines two functions, do_this and do_that, and a tictoc decorator. The tictoc decorator wraps a function and measures the time it takes to execute.

The tictoc decorator works as follows:

import time

def tictoc(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        func(*args, **kwargs)
        t2 = time.time() - t1
        print(f'{func.__name__} ran in {t2} seconds')

    return wrapper

@tictoc
def do_this():
    # Simulating running code..
    time.sleep(1.3)

@tictoc
def do_that():
    # Simulating running code..
    time.sleep(.4)

if __name__ == '__main__':
    do_this()
    do_that()
    print('Done')

Output:

do_this ran in 1.305107831954956 seconds
do_that ran in 0.40225887298583984 seconds
Done

When the script is executed, do_this and do_that functions are executed. Since they are decorated with tictoc, their execution time is measured and printed to the console.

Creating the Decorator

Now, let's create a chapter that explains the implementation of the tictoc decorator step by step.

Step 1: Understanding Decorators

In Python, functions are first-class objects, which means you can pass functions as arguments to other functions. This property allows us to create decorators. A decorator is a function that takes another function as an argument, adds some functionality, and returns the modified function.

Step 2: Defining the Decorator

To create the tictoc decorator, we need to define a function that takes another function func as an argument. Inside the decorator, we will create a new function wrapper, which will measure the execution time of func and print the result.

import time

def tictoc(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        func(*args, **kwargs)
        t2 = time.time() - t1
        print(f'{func.__name__} ran in {t2} seconds')

    return wrapper

Step 3: Applying the Decorator

Now that we have defined the tictoc decorator, we can apply it to the functions we want to measure.

@tictoc
def do_this():
    # Simulating running code..
    time.sleep(1.3)

@tictoc
def do_that():
    # Simulating running code..
    time.sleep(.4)

By using the @tictoc syntax above each function, we apply the tictoc decorator to do_this and do_that.

Step 4: Executing the Functions

Finally, we execute the functions do_this() and do_that() in the if __name__ == '__main__' block.

if __name__ == '__main__':
    do_this()
    do_that()
    print('Done')

When the script is executed, the decorated functions will print their execution times to the console.

Decorators are a powerful tool in Python that allow you to add extra functionality to functions without modifying their code directly. In this chapter, we explored how to create a simple tictoc decorator to measure the execution time of functions. This is just one example of what decorators can do, and they can be used for various other purposes, making them an essential concept to master in Python.

*Args & **Kwargs

In Python, *args and **kwargs are special syntax that allow a function to accept a variable number of arguments and keyword arguments, respectively. They are commonly used when you want to create flexible functions that can take different numbers of arguments without having to define each one explicitly.

The *args Syntax

The *args syntax allows a function to accept a variable number of positional arguments. When you use *args as a parameter in a function definition, it means you can pass any number of arguments to that function, and they will be packed into a tuple. Inside the function, you can then access these arguments using the args tuple.

Let's take a look at the example code you provided to understand how *args works:

def order_pizza(size, *args, **kwargs):
    print(f'Ordered a {size} pizza with the following toppings:')
    for topping in args:
        print(f'- {topping}')
    print('\nDetails of the order are:')
    for key, value in kwargs.items():
        print(f'- {key}: {value}')

order_pizza("large", "pepperoni", 'olives', delivery=True, tips=8)

In the order_pizza function, *args is used as a parameter, which means it can accept a variable number of toppings as positional arguments. In the function call order_pizza("large", "pepperoni", 'olives', delivery=True, tips=8), "pepperoni" and 'olives' are passed as additional arguments and will be packed into the args tuple.

The function then prints the size of the pizza and the toppings using a loop that iterates over the args tuple.

The **kwargs Syntax

The **kwargs syntax allows a function to accept a variable number of keyword arguments. When you use **kwargs as a parameter in a function definition, it means you can pass any number of keyword arguments to that function, and they will be packed into a dictionary. Inside the function, you can then access these arguments using the kwargs dictionary.

Let's take a closer look at the example code to see how **kwargs works:

def order_pizza(size, *args, **kwargs):
    print(f'Ordered a {size} pizza with the following toppings:')
    for topping in args:
        print(f'- {topping}')
    print('\nDetails of the order are:')
    for key, value in kwargs.items():
        print(f'- {key}: {value}')

order_pizza("large", "pepperoni", 'olives', delivery=True, tips=8)

In the order_pizza function, **kwargs is used as a parameter, which means it can accept a variable number of keyword arguments. In the function call order_pizza("large", "pepperoni", 'olives', delivery=True, tips=8), delivery=True and tips=8 are passed as keyword arguments and will be packed into the kwargs dictionary.

The function then prints the details of the order using a loop that iterates over the kwargs dictionary.

Combining *args and **kwargs

You can use both *args and **kwargs in the same function definition. In that case, you can pass both positional and keyword arguments to the function, and they will be appropriately packed into the args tuple and kwargs dictionary, respectively.

def example_function(arg1, arg2, *args, kwarg1='default', **kwargs):
    print(f'arg1: {arg1}')
    print(f'arg2: {arg2}')
    print(f'args: {args}')
    print(f'kwarg1: {kwarg1}')
    print(f'kwargs: {kwargs}')

# Example call
example_function('A', 'B', 'C', 'D', kwarg1='custom', key1='value1', key2='value2')

In this example, arg1 and arg2 are regular positional arguments, *args collects additional positional arguments, kwarg1 is a default keyword argument, and **kwargs collects any extra keyword arguments. The output of the function call will show how the arguments are packed:

arg1: A
arg2: B
args: ('C', 'D')
kwarg1: custom
kwargs: {'key1': 'value1', 'key2': 'value2'}

In this chapter, you learned about *args and **kwargs in Python, which allow you to create functions that can accept a variable number of positional and keyword arguments. By using these features, you can make your functions more flexible and versatile, as they can handle different numbers of inputs without explicitly defining them in the function signature. Understanding *args and **kwargs is essential for writing more generic and reusable functions in Python.

Last updated