Just as with functions, we can execute statements in the bodies of loops.
We can use everything we learned in the loop, like conditional constructs. Imagine a function that counts the number of times a letter appears in a sentence. We see an example of how it works here:
count_chars('Fear cuts deeper than swords.', 'e') # 4
# If nothing is found, the result is 0 matches
count_chars('Sansa', 'y') # 0
Before you look at the contents of a function, think about this:
- Is this an aggregate operation?
- What will be the test condition to determine if we should include the character?
Now, we look at the contents of a function:
def count_chars(string, char):
index = 0
count = 0
while index < len(string):
if string[index] == char:
# We only count the matching characters
count = count + 1
# The counter is incremented anyway
index = index + 1
return count
It is an aggregation task. It doesn't count all the characters to calculate the amount, but you still should analyze each character. The main difference between this loop and the ones we looked at before is that there's a condition in the body.
We only increment the count
variable if the character in question is the same as the expected one. Otherwise, it's a typical aggregate function, returning the number of characters you want.
Edge cases
The my_substr()
function you implemented in a previous lesson contains many bugs. It passed the test because it had no edge cases.
The function worked with regular arguments. Now let's imagine that we passed these length options:
0
- Negative number
- A number that exceeds the actual size of the string
The creators of Python did not design the my_substr()
function for this. The code will run in different situations with different combinations of conditions and data. You can't be sure that the arguments passed to it will always be correct, so you must consider all cases.
Edge cases are a common source of logic errors in programs. There is always something that programmers forget to consider. These errors often don't immediately manifest. They do not cause visible problems for a long time.
The program may continue to work, but eventually, someone will notice an error in the results. It can often be a result of Python's dynamic typing.
As you get more experience, you'll learn how to deal with these errors.
Here is an extended version of the my_substr()
function. It takes three arguments: a string, an index, and the length of the substring to extract. The function returns a substring of the specified length starting from the given index.
Let us observe some calling examples:
string = 'If I look back I am lost'
print(my_substr(string, 0, 1)) # => 'I'
print(my_substr(string, 3, 6)) # => 'I look'
What edge cases should we consider:
- The extracted substring has a negative length
- The index given is negative
- The index given exceeds the boundary of the entire string
- The length of the substring in the sum with the given index exceeds the boundary of the whole string
When implementing the function, we write each edge case as a separate code piece, most likely with if
.
It is worth implementing a separate function that checks the arguments are valid if you want to write my_substr()
in a way that protects it against these cases.
Returning from loops
Dealing with loops usually comes down to two cases.
We can:
- Aggregate data during iterations and work with it after the loop (for example, string reversals)
- Execute the loop until we reach the desired result and exit (for example, prime number calculations)
Consider an algorithm that checks whether a number is a prime. We divide the number x
by all numbers from two to x - 1
and look at the remainder. We know we have a prime number if we cannot find a divisor in this range that divides x
without a remainder.
In this case, we don't need to go to x - 1
- half of x
is enough.
For example, 11
isn't divisible by 2
, 3
, 4
, or 5
. When we know it, we can be sure that 11
won't be divisible by numbers above its half.
So we can optimize the algorithm and check the divisions only up to x / 2
:
def is_prime(number):
if number < 2:
return False
divider = 2
while divider <= number / 2:
if number % divider == 0:
return False
divider += 1
return True
print(is_prime(1)) # => False
print(is_prime(2)) # => True
print(is_prime(3)) # => True
print(is_prime(4)) # => False
Imagine that the algorithm that divides numbers consecutively by numbers up to x / 2
finds one that divides without a remainder.
It means the argument passed is not a prime number, and we don't need further calculations. It is where it returns False
.
If you have run the whole loop and have not found a number that divides without a remainder, it means the number is prime.