LICENSE

MIT License

Copyright Β© 2018 Oleksii Trekhleb

Copyright Β© 2020 Samuel Huang

Copyright Β© 2023 jerry-git

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the β€œSoftware”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED β€œAS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

42.8. Python programming advanced#

# set up the env

import pytest
import ipytest
import unittest

ipytest.autoconfig()

42.8.1. Conditionals#

42.8.1.1. if-elif-else#

Fill missing pieces ____ of the following code such that prints make sense.

# Binary search algorithm
def binary_search(list, target):
    """Searches for a target element in a sorted list using binary search.

   Args:
       lst (list): The sorted list to be searched.
       target (any): The element to be searched for.

   Returns:
       int: The index of the target element if found, -1 otherwise.
   """
    # Initialize low and high indices
    # Initialize the left and right pointers
    left = 0
    right = len(list) - 1
    # Loop until the left pointer is greater than the right pointer
    while left <= right:
        # Find the middle index of the current range
        mid = (left + right) // 2
        # Compare the target with the middle element of the list
        if target == ____:
            # Return the index of the target if found
            return mid
        ____ target ____:
            # Narrow the search range to the left half if the target is smaller than the middle element
            ____ = mid - 1
        else:
            # Narrow the search range to the right half if the target is larger than the middle element
            ____ = mid + 1
    # Return -1 if the target is not found in the list
    return -1

lst = [1, 2, 3, 4, 5, 6, 7, 8, 9]
assert binary_search(lst, 5) == 4
assert binary_search(lst, 9) == 8
assert binary_search(lst, -1) == -1
assert binary_search(lst, 10) == -1
Check result by executing below... πŸ“
%%ipytest -qq

class TestBinarySearch:
    def test_existing_element(self):
        lst = [1, 3, 5, 7, 9]
        target = 5
        assert binary_search(lst, target) == 2

    def test_non_existing_element(self):
        lst = [1, 3, 5, 7, 9]
        target = 6
        assert binary_search(lst, target) == -1

    def test_empty_list(self):
        lst = []
        target = 5
        assert binary_search(lst, target) == -1

    def test_single_element_list(self):
        lst = [5]
        target = 5
        assert binary_search(lst, target) == 0

    def test_wrong_target_type(self):
        lst = [1, 3, 5, 7, 9]
        target = "a"
        with pytest.raises(TypeError):
            binary_search(lst, target)
πŸ‘©β€πŸ’» Hint

Compare the target value with the middle element during each iteration and narrow down the search range based on the comparison result.

42.8.2. For loops#

42.8.2.1. Fill the missing pieces#

Fill the ____ parts in the code below.

def factorial(n):
    """
    Calculate the factorial of a number.

    Args:
        n (int): The number to calculate the factorial of.

    Returns:
        int: The factorial of the input number.
    """
    result = 1
    ____ ____ in range(1, n + 1):
        result ____
    return result


assert factorial(3) == 6
Check result by executing below... πŸ“
%%ipytest -qq

class TestFactorial:
    def test_positive_number(self):
        assert factorial(5) == 120
        assert factorial(10) == 3628800
        assert factorial(0) == 1

    def test_float_number(self):
        with pytest.raises(TypeError):
            factorial(3.14)

    def test_string_input(self):
        with pytest.raises(TypeError):
            factorial("5")
πŸ‘©β€πŸ’» Hint

Use a loop to iterate through the numbers from 1 to n and multiply the result by each number to calculate the factorial.

42.8.2.2. range()#

def calculate_sum(n):
    """
    Calculate the sum of numbers from 1 to n.

    Args:
        n (int): The upper limit.

    Returns:
        int: The sum of the numbers.
    """
    total = 0
    for num in ____(1, ____):
        total += num
    return total

assert calculate_sum(4) == 10
Check result by executing below... πŸ“
%%ipytest -qq

class TestCalculateSum:
    def test_positive_number(self):
        assert calculate_sum(5) == 15
        assert calculate_sum(10) == 55
        assert calculate_sum(100) == 5050

    def test_zero(self):
        assert calculate_sum(0) == 0

    def test_negative_number(self):
        assert calculate_sum(-5) == 0

    def test_float_number(self):
        with pytest.raises(TypeError):
            calculate_sum(3.14)

    def test_string_input(self):
        with pytest.raises(TypeError):
            calculate_sum("5")
πŸ‘©β€πŸ’» Hint

Use a loop to iterate through the numbers starting from 1 up to and including n, and accumulate the sum by adding each number to the total variable.

42.8.2.3. Looping dictionaries#

my_dict = {'hacker': True, 'age': 72, 'name': 'John Doe'}
keys_list = []
values_list = []
# Your solution here:
____

42.8.2.4. Calculate the sum of dict values#

Calculate the sum of the values in magic_dict by taking only into account numeric values (hint: see isinstance).

magic_dict = dict(val1=44, val2='secret value', val3=55.0, val4=1)
# Your implementation
sum_of_values = ____
assert sum_of_values == 100

42.8.2.5. Create a list of strings based on a list of numbers#

The rules:

  • If the number is a multiple of five and odd, the string should be 'five odd'

  • If the number is a multiple of five and even, the string should be 'five even'

  • If the number is odd, the string is 'odd'

  • If the number is even, the string is 'even'

numbers = [1, 3, 4, 6, 81, 80, 100, 95]
# Your implementation
my_list = ____
assert my_list == ['odd', 'odd', 'even', 'even', 'odd', 'five even', 'five even', 'five odd']

42.8.3. While loops#

42.8.3.1. Fill the missing pieces#

Fill the ____ parts in the code below.

def count_digits(number):
    """
    Count the number of digits in a given number.

    Args:
        number (int): The input number.

    Returns:
        int: The count of digits in the number.

    Raises:
        TypeError: If the input is not an integer.
    """
    if not isinstance(number, int):
        raise TypeError("Input must be an integer.")

    count = 0
    ____ number != ____:
        number //= ____
        count += 1
    return count

assert count_digits(123) == 3
Check result by executing below... πŸ“
%%ipytest -qq

class TestCountDigits:
    def test_positive_number(self):
        assert count_digits(123) == 3

    def test_zero(self):
        assert count_digits(0) == 0

    def test_negative_number(self):
        assert count_digits(-123) == 3

    def test_float_number(self):
        with pytest.raises(TypeError):
            count_digits(3.14)

    def test_string_input(self):
        with pytest.raises(TypeError):
            count_digits("123")
πŸ‘©β€πŸ’» Hint

Use a loop to repeatedly divide the number by 10 and increment the count variable until the number becomes zero.

42.8.4. Break#

42.8.4.1. Fill the missing pieces using break statement#

def find_prime_factors(number):
    """
    Function to find and return the prime factors of a given number.
    
    Args:
    number (int): The number to find the prime factors of.

    Returns:
    list: A list containing the prime factors of the given number.
    """
    prime_factors = []  # Initialize an empty list to store the prime factors
    divisor = 2  # Start with the smallest prime number as the divisor

    while number > 1:  # Continue the loop until the number is reduced to 1
        if number % divisor == 0:  # Check if the current divisor is a factor of the number
            prime_factors.append(divisor)  # If it is, add it to the list of prime factors
            number = number ____ divisor  # Divide the number by the divisor to continue finding the remaining factors
        else:
            divisor ____ 1  # If the divisor is not a factor, increment it by 1
            if divisor * divisor > number:  # If the square of the divisor is greater than the number
                if number > 1:  # And the remaining number is greater than 1
                    ____.append(____)  # Add the remaining number as a prime factor
                ____  # Break out of the loop, as no further factors need to be checked

    return prime_factors  # Return the list of prime factors

assert find_prime_factors(18) == [2, 3, 3]
Check result by executing below... πŸ“
%%ipytest -qq

class TestFindPrimeFactors:
    def test_positive_number(self):
        assert find_prime_factors(10) == [2, 5]
        assert find_prime_factors(12) == [2, 2, 3]
        assert find_prime_factors(29) == [29]
        assert find_prime_factors(56) == [2, 2, 2, 7]
        assert find_prime_factors(100) == [2, 2, 5, 5]

    def test_zero(self):
        assert find_prime_factors(0) == []

    def test_negative_number(self):
        assert find_prime_factors(-10) == []
        assert find_prime_factors(-12) == []
        assert find_prime_factors(-29) == []
        assert find_prime_factors(-56) == []
        assert find_prime_factors(-100) == []

    def test_string_input(self):
        with pytest.raises(TypeError):
            find_prime_factors("123")
πŸ‘©β€πŸ’» Hint

Try using the divisor to decompose the number.

42.8.5. Continue#

42.8.5.1. Fill the missing pieces using continue statement#

def sieve_of_eratosthenes(n):
    """
    Finds all prime numbers less than or equal to n using the sieve of Eratosthenes method.

    Args:
        n (int): A positive integer to be the upper bound of the prime numbers.

    Returns:
        list: A list of all prime numbers less than or equal to n.
    """
    if n <= 1:
        return []
    # Initialize a list of booleans from 0 to n, where True means the number is prime
    is_prime = [True] * (n + 2)

    # Mark 0 and 1 as not prime
    is_prime[0] = False
    is_prime[1] = False

    # Loop from 2 to the square root of n
    for i in range(2, int(n ** 0.5) + 1):
        # If the current number is marked as prime
        if is_prime[i]:

            # Loop from i^2 to n with step size i
            for j in range(i * i, n + 1, i):
                # Mark all multiples of i as not prime
                is_prime[j] = ____
    # Initialize an empty list to store the prime numbers
    primes = []

    # Loop from 2 to n
    for i in range(2, n + 1):

        # If the current number is marked as prime
        if ____:
            # Append it to the list of prime numbers
            primes.____

        # Otherwise, continue to the next iteration
        else:
            ____
    # Return the list of prime numbers
    return primes

assert sieve_of_eratosthenes(10) == [2, 3, 5, 7]
Check result by executing below... πŸ“
%%ipytest -qq

class TestSieveOfEratosthenes:
    def test_positive_number(self):
        assert sieve_of_eratosthenes(10) == [2, 3, 5, 7]
        assert sieve_of_eratosthenes(20) == [2, 3, 5, 7, 11, 13, 17, 19]
        assert sieve_of_eratosthenes(30) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
        assert sieve_of_eratosthenes(50) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

    def test_zero(self):
        assert sieve_of_eratosthenes(0) == []

    def test_negative_number(self):
        assert sieve_of_eratosthenes(-10) == []

    def test_float_number(self):
        with pytest.raises(TypeError):
            sieve_of_eratosthenes(3.14)

    def test_string_input(self):
        with pytest.raises(TypeError):
            sieve_of_eratosthenes("123")
πŸ‘©β€πŸ’» Hint

In the inner loop, mark all multiples of the current prime number as not prime. To do this, set the corresponding indices in the is_prime list to False.

42.8.6. Functions#

42.8.6.1. Fill the missing pieces of the gcd function#

def gcd(a, b):
    """
    Finds the greatest common divisor of two positive integers using the Euclidean algorithm.

    Args:
        a (int): A positive integer.
        b (int): Another positive integer.

    Returns:
        int: The greatest common divisor of a and b.
    """
    if not isinstance(a, int) or not isinstance(b, int):
        raise TypeError("Inputs must be integers.")

    # Base case: if b is 0, return a
    if b == 0:
        return ____

    # Recursive case: if b is not 0, return the GCD of b and the remainder of a divided by b
    else:
        return gcd(b, ____)

assert gcd(12, 18) == 6
Check result by executing below... πŸ“
%%ipytest -qq

class TestGCD:
    def test_positive_numbers(self):
        assert gcd(10, 25) == 5
        assert gcd(14, 28) == 14
        assert gcd(21, 14) == 7
        assert gcd(48, 18) == 6
        assert gcd(17, 29) == 1

    def test_zero(self):
        assert gcd(0, 10) == 10
        assert gcd(10, 0) == 10
        assert gcd(0, 0) == 0

    def test_negative_numbers(self):
        assert gcd(-10, 25) == 5
        assert gcd(14, -28) == 14
        assert gcd(-21, -14) == 7

    def test_same_number(self):
        assert gcd(10, 10) == 10
        assert gcd(0, 0) == 0

    def test_one_as_input(self):
        assert gcd(1, 25) == 1
        assert gcd(14, 1) == 1
        assert gcd(1, 1) == 1

    def test_large_numbers(self):
        assert gcd(123456789, 987654321) == 9
        assert gcd(2**64, 2**32) == 2**32

    def test_float_numbers(self):
        with pytest.raises(TypeError):
            gcd(3.14, 2.71)

    def test_string_input(self):
        with pytest.raises(TypeError):
            gcd("123", "456")
πŸ‘©β€πŸ’» Hint

Use the Euclidean algorithm recursively, updating the values of a and b.

42.8.6.2. Searching for wanted people#

Implement find_wanted_people function which takes a list of names (strings) as argument. The function should return a list of names which are present both in WANTED_PEOPLE and in the name list given as argument to the function.

WANTED_PEOPLE = ['John Doe', 'Clint Eastwood', 'Chuck Norris']
# Your implementation here
____
people_to_check1 = ['Donald Duck', 'Clint Eastwood', 'John Doe', 'Barack Obama']
wanted1 = find_wanted_people(people_to_check1)
assert len(wanted1) == 2
assert 'John Doe' in wanted1
assert 'Clint Eastwood' in wanted1

people_to_check2 = ['Donald Duck', 'Mickey Mouse', 'Zorro', 'Superman', 'Robin Hood']
wanted2 = find_wanted_people(people_to_check2)

assert wanted2 == []

42.8.6.3. Counting average length of words in a sentence#

Create a function average_length_of_words which takes a string as an argument and returns the average length of the words in the string. You can assume that there is a single space between each word and that the input does not have punctuation. The result should be rounded to one decimal place (hint: see round).

# Your implementation here
____
assert average_length_of_words('only four lett erwo rdss') == 4
assert average_length_of_words('one two three') == 3.7
assert average_length_of_words('one two three four') == 3.8
assert average_length_of_words('') == 0

42.8.7. Lambda#

42.8.7.1. Fill the missing pieces#

def map_function(nums, func):
    """
    Applies a function to each element in a list of numbers and returns a new list of results.

    Args:
        nums (list): A list of numbers to be processed.
        func (function): A function to be applied to each element in the list.

    Returns:
        list: A new list of numbers where each element is the result of applying the function to the corresponding element in the original list.
    """
    # Use the map function and a lambda expression to apply the function to each element in the list and convert the result to a list
    return list(map(____ x: ____, ____))


# Define a list of numbers
nums = [1, 2, 3, 4, 5]


# Define a function that squares a number
def square(x):
    return ____

# Call the map_function with the list of numbers and the square function
assert map_function(nums, square) == [1, 4, 9, 16, 25]
Check result by executing below... πŸ“
%%ipytest -qq

class TestMapFunction:
    def test_square_function(self):
        assert map_function([1, 2, 3, 4, 5], square) == [1, 4, 9, 16, 25]
        assert map_function([0, -1, 2, -3, 4], square) == [0, 1, 4, 9, 16]
        assert map_function([], square) == []

    def test_negative_numbers(self):
        assert map_function([-1, -2, -3], abs) == [1, 2, 3]
        assert map_function([-1, -2, -3], square) == [1, 4, 9]

    def test_float_numbers(self):
        assert map_function([1.5, 2.5, 3.5], int) == [1, 2, 3]
        assert map_function([0.5, 1.5, 2.5], round) == [0, 2, 2]

    def test_string_numbers(self):
        with pytest.raises(TypeError):
            map_function(["1", "2", "3"], square)
πŸ‘©β€πŸ’» Hint

Use the map() function with a lambda expression to apply the given function to each element in the list and convert the result to a list.

42.8.8. Classes#

42.8.8.1. Fill the missing pieces of the Stack class#

Fill ____ pieces of the Stack implemention in order to pass the assertions.

class Stack:
    """
    A class to represent a stack data structure.

    Attributes:
        items (list): A list of items in the stack.

    Methods:
        push(item): Adds an item to the top of the stack.
        pop(): Removes and returns the top item from the stack.
        peek(): Returns the top item from the stack without removing it.
        is_empty(): Returns True if the stack is empty, False otherwise.
    """

    # Initialize an empty list to store the items
    def _____(____):
        ____.items = []

    # Add an item to the top of the stack
    def push(self, ____):
        self.items.append(____)

    # Remove and return the top item from the stack
    def pop(self):
        return self.items.____

    # Check if the stack is empty
    def is_empty(self):
        return ____ == 0

stack = Stack()
stack.push(1)

assert stack.pop() == 1
assert stack.is_empty() == True
Check result by executing below... πŸ“
%%ipytest -qq

class TestStack:
    def test_push(self):
        stack = Stack()
        stack.push(1)
        stack.push(2)
        stack.push(3)
        assert stack.items == [1, 2, 3]

    def test_pop(self):
        stack = Stack()
        stack.push(1)
        stack.push(2)
        stack.push(3)
        assert stack.pop() == 3
        assert stack.pop() == 2
        assert stack.pop() == 1
        assert stack.is_empty()

    def test_is_empty(self):
        stack = Stack()
        assert stack.is_empty()
        stack.push(1)
        assert not stack.is_empty()
        stack.pop()
        assert stack.is_empty()
πŸ‘©β€πŸ’» Hint

Initialize the list in init, use list operations to push and pop items, and check the list length to determine if it’s empty.

42.8.8.2. Fill the missing pieces of the Complex class#

class Complex:
    """
    A class to represent a complex number.

    Attributes:
        real (float): The real part of the complex number.
        imag (float): The imaginary part of the complex number.

    Methods:
        __add__(other): Returns the sum of two complex numbers.
        __sub__(other): Returns the difference of two complex numbers.
        __mul__(other): Returns the product of two complex numbers.
        __truediv__(other): Returns the quotient of two complex numbers.
        __abs__(): Returns the absolute value of the complex number.
        __eq__(): Returns whether two complex numbers are equal.
        conjugate(): Returns the conjugate of the complex number.
    """

    # Initialize the real and imaginary parts of the complex number
    def ____(self, real, imag):
        ____ = ____
        ____ = ____

    # Define the string representation of the complex number
    def __str__(self):
        if self.imag >= 0:
            return f"{____} + {____}i"
        else:
            return f"{____} - {____}i"

    # Define the addition of two complex numbers
    def __add__(self, other):
        return ____

    # Define the subtraction of two complex numbers
    def __sub__(self, other):
        return ____

    # Define the multiplication of two complex numbers
    def __mul__(self, other):
        return ____

    # Define the division of two complex numbers
    def __truediv__(self, other):
        denominator = other.real ____ + other.imag ____
        return Complex((self.real * ____ + self.imag * ____) ____ denominator,
                       (self.imag * ____ - self.real * ____) ____ denominator)

    # Define the absolute value of the complex number
    def __abs__(self):
        return (self.real ____ + self.imag ____) ____

    # Define the equality of two complex numbers
    def __eq__(self, other):
        return self.real == other.real ____ self.imag == other.imag

    # Define the conjugate of the complex number
    def conjugate(self):
        return ____


# Create two complex numbers
z1 = Complex(3, 4)
z2 = Complex(1, -2)

assert z1 + z2 == Complex(4, 2)
assert z1 - z2 == Complex(2, 6)
assert z1 * z2 == Complex(11, -2)
assert z1 / z2 == Complex(-1, 2)
assert abs(z1) == 5
assert abs(z2) == 2.23606797749979
assert z1.conjugate() == Complex(3, -4)
assert z2.conjugate() == Complex(1, 2)
Check result by executing below... πŸ“
%%ipytest -qq

class TestComplex:
    def test_addition(self):
        c1 = Complex(1, 2)
        c2 = Complex(3, 4)
        result = c1 + c2
        assert result.real == 4
        assert result.imag == 6

    def test_subtraction(self):
        c1 = Complex(1, 2)
        c2 = Complex(3, 4)
        result = c1 - c2
        assert result.real == -2
        assert result.imag == -2

    def test_multiplication(self):
        c1 = Complex(1, 2)
        c2 = Complex(3, 4)
        result = c1 * c2
        assert result.real == -5
        assert result.imag == 10

    def test_division(self):
        c1 = Complex(1, 2)
        c2 = Complex(3, 4)
        result = c1 / c2
        assert result.real == 0.44
        assert result.imag == 0.08

    def test_absolute_value(self):
        c = Complex(3, 4)
        result = abs(c)
        assert result == 5

    def test_equality(self):
        c1 = Complex(1, 2)
        c2 = Complex(1, 2)
        c3 = Complex(3, 4)
        assert c1 == c2
        assert c1 != c3

    def test_conjugate(self):
        c = Complex(1, 2)
        result = c.conjugate()
        assert result.real == 1
        assert result.imag == -2
πŸ‘©β€πŸ’» Hint

Implement arithmetic operations using appropriate operators and utilize mathematical operations and string formatting for other methods.

42.8.9. Exceptions#

42.8.9.1. Dealing with exceptions#

Fill ____ parts of the implementation below. sum_of_list function takes a list as argument and calculates the sum of values in the list. If some element in the list can not be converted to a numeric value, it should be ignored from the sum.

def sum_of_list(values):
    ____ = 0
    for val in values:
        ____:
        numeric_val = float(val)
    ____
    ____ as e:
    ____


____ += numeric_val
return ____
list1 = [1, 2, 3]
list2 = ['1', 2.5, '3.0']
list3 = ['', '1']
list4 = []
list5 = ['John', 'Doe', 'was', 'here']
nasty_list = [KeyError(), [], dict()]

assert sum_of_list(list1) == 6
assert sum_of_list(list2) == 6.5
assert sum_of_list(list3) == 1
assert sum_of_list(list4) == 0
assert sum_of_list(list5) == 0
assert sum_of_list(nasty_list) == 0

42.8.9.2. Dealing with exceptions#

def square_root(x):
    """
    Finds the square root of a non-negative number using the Newton's method.

    Args:
        x (float): A non-negative number to be the input.

    Returns:
        float: The square root of x.

    Raises:
        ValueError: If x is negative.
    """
    # Check if x is negative
    if x < 0:
        # Raise a ValueError exception with a message
        raise ValueError("x must be non-negative")

    # Use a try block to attempt the square root calculation
    ____:
        # Initialize a guess value as half of x
        guess = x / 2

        # Loop until the guess is close enough to the actual square root
        while abs(____ - x) > 0.000001:
            # Update the guess value using the Newton's formula
            guess = (guess + x ____ guess) / 2

        # Return the guess value as the square root of x
        return guess

    # Use an except block to handle possible ZeroDivisionError exceptions
    ____ ZeroDivisionError as ____:
        # Print the exception message
        print(____)

        return ____


import math

assert math.isclose(square_root(25), 5, rel_tol=0.000001)
import pytest

with pytest.raises(ValueError):
    square_root(-9)
Check result by executing below... πŸ“
%%ipytest -qq

class TestSquareRoot:
    def test_positive_number(self):
        result = square_root(16)
        assert result == pytest.approx(4)

    def test_zero(self):
        result = square_root(0)
        assert result == 0

    def test_negative_number(self):
        with pytest.raises(ValueError):
            square_root(-16)

    def test_large_number(self):
        result = square_root(123456789)
        assert result == pytest.approx(11111.11109, rel=1e-5)
πŸ‘©β€πŸ’» Hint

Try-except for ZeroDivisionError, initialize guess as x/2, update using Newton’s formula until desired precision.

42.8.10. Acknowledgments#

Thanks to below awesome open source projects for Python learning, which inspire this chapter.