Chapter 17
IN THIS CHAPTER
Understanding Python bugs
Considering bug sources
Locating and describing Python errors
Squashing Python bugs
Chapter 16 discusses errors in code from a Haskell perspective, and some of the errors you encounter in Haskell might take you by surprise. Oddly enough, so might some of the coding techniques used in other languages that would appear as errors. (Chapter 16 also provides a good reason not to compare the bug mitigation properties of various languages in the “Reducing the number of bugs” sidebar.) Python is more traditional in its approach to errors. For example, dividing a number by zero actually does produce an error, not a special data type designed to handle the division using the value Infinity
. Consequently, you may find the discussion (in the first section of this chapter) of what constitutes a bug in Python a little boring if you have worked through coding errors in other procedural languages. Even so, reading the material is a good idea so that you can better understand how Python and Haskell differ in their handling of errors in the functional programming environment.
The next section of the chapter goes into the specifics of Python-related errors, especially those related to the functional features that Python provides. Although the chapter does contain a little general information as background, it focuses mostly on the functional programming errors.
Finally, the chapter tells you about techniques that you can use to fix Python functional programming errors a little faster. You'll find the same sorts of things that you can do when using Python for procedural programming, such as step-by-step debugging. However, fixing functional errors sometimes requires a different thought process, and this chapter helps you understand what you need to do when such cases arise.
As with Haskell, Python bugs occur when an application fails to work as anticipated. Both languages also view errors that create bugs in essentially the same manner, even though Haskell errors take a functional paradigm's approach, while those in Python are more procedural in nature. The following sections help you understand what is meant by a bug in Python and provide input on how using the functional approach can affect the normal view of bugs.
You might be able to divine the potential sources of error in your application by reading tea leaves, but that's hardly an efficient way to do things. Errors actually fall into well-defined categories that help you predict (to some degree) when and where they’ll occur. By thinking about these categories as you work through your application, you’re far more likely to discover potential errors' sources before they occur and cause potential damage. The two principal categories are
The following sections discuss these two categories in greater detail. The overall concept is that you need to think about error classifications in order to start finding and fixing potential errors in your application before they become a problem.
Errors occur at specific times. However, no matter when an error occurs, it causes your application to misbehave. The two major time frames in which errors occur are
Runtime: A runtime error occurs after Python compiles the code that you write and the computer begins to execute it. Runtime errors come in several different types, and some are harder to find than others. You know you have a runtime error when the application suddenly stops running and displays an exception dialog box or when the user complains about erroneous output (or at least instability).
Not all runtime errors produce an exception. Some runtime errors cause instability (the application freezes), errant output, or data damage. Runtime errors can affect other applications or create unforeseen damage to the platform on which the application is running. In short, runtime errors can cause you quite a bit of grief, depending on precisely the kind of error you’re dealing with at the time.
You can distinguish errors by type, that is, by how they’re made. Knowing the error types helps you understand where to look in an application for potential problems. Exceptions work like many other things in life. For example, you know that electronic devices don’t work without power. So when you try to turn your television on and it doesn’t do anything, you might look to ensure that the power cord is firmly seated in the socket.
The trick is to know where to look. With this in mind, Python (and most other programming languages) breaks errors into the following types (arranged in order of difficulty, starting with the easiest to find):
Python is one of the few languages around today that has active support for two major language versions. Even though Python 2.x support will officially end in 2020 (see https://pythonclock.org/
for details), you can bet that many developers will continue to use it until they're certain that the libraries they need come in a fully compatible Python 3.x form. However, the problem isn’t just with libraries but also with processes, documentation, existing code, and all sorts of other things that could affect someone who is using functional programming techniques in Python.
You can encounter more than a few kinds of errors when working with Python code. This chapter doesn’t provide exhaustive treatment of those errors. However, the following sections do offer some clues as to what might be wrong with your functional code, especially as it deals with lambda expressions.
You need to realize that Python is late binding, which means that Python looks up the values of variables when it calls an inner function that is part of a loop only when the loop is completed. Consequently, rather than use individual values within a loop, what you see is the final value. For a demonstration of this issue, consider the following code:
def create_values(numValues):
return [lambda x : i * x for i in range(numValues)]
for indValue in create_values(5):
print(indValue(2))
This code creates the specified number of functions, one for each value in range(numValues)
, which is create_values(5)
(five) in this case. The idea is to create an output of five values using a particular multiplier (which is indValue(2)
in this case). You might assume that the first function call will be 0 (the value of i
) * 2 (the value of x
supplied as an input). However, the first function is never called while i
is equal to 0. In fact, it gets called the first time only when its value is 4 — at the end of the loop. As a result, the output you see when you call this function is a series of 8s. To fix this code, you need to use the following create_values()
code instead:
def create_values(numValues):
return [lambda x, i=i : i * x for i in
range(numValues)]
In some situations, you can't use a lambda expression inline. Fortunately, Python will generally find these errors and tell you about them, as in the following code:
garbled = "IXX aXXmX sXeXcXrXeXt mXXeXsXsXaXXXXXXgXeX!XX"
print filter(lambda x: x != "X", garbled)
Obviously, this example is incredible simple, and you likely wouldn’t use it in the real world. However, it shows that you can't use the lambda inline in this case; you must first assign it to a variable and then loop through the values. The following code shows the correct alternative code:
garbled = "IXX aXXmX sXeXcXrXeXt mXXeXsXsXaXXXXXXgXeX!XX"
ungarble = filter(lambda x: x != "X", garbled)
for x in ungarble:
print(x, end='')
Your Python functional programming experience will include third-party libraries that may not always benefit from the functional programming approach. Before you assume that a particular approach will work, you should review potential sources of error online. For example, the following message thread discusses potential problems with using lambda expressions to perform an aggregation with Pandas: https://github.com/pandas-dev/pandas/issues/7186
. In many cases, the community of developers will have alternatives for you to try, as happened in this case.
The key to fixing Python errors quickly is to have a strategy for dealing with each sort of error described in the “Distinguishing error types” section, earlier in this chapter. If Python doesn’t recognize an error during the compilation process, it often generates an exception or you see unwanted behavior. The use of lambda expressions to define an application that relies on the functional paradigm doesn’t really change things, but the use of lambda expressions can create special circumstances, such as those described in the “Introducing the algorithm connection” sidebar of Chapter 16. The following sections describe the mix of error-correction processes that you can employ when using Python in functional mode.
Python comes with a host of built-in exceptions — far more than you might think possible. You can see a list of these exceptions at https://docs.python.org/3.6/library/exceptions.html
. The documentation breaks the exception list down into categories. Here is a brief overview of the Python exception categories that you work with regularly:
Exception
exception) for other exceptions. However, you might actually see some of these exceptions, such as the ArithmeticError
exception, when working with an application.MemoryError
exception. Recovering from this error is hard because it releasing memory from other uses isn't always possible. When the user presses an interrupt key (such as Ctrl+C or Delete), Python generates a KeyboardInterrupt
exception. The application must handle this exception before proceeding with any other tasks.FileNotFoundError
exception.ResourceWarning
exception. You want to remember that this particular category is a warning and not an actual error: Ignoring it can cause you woe later, but you can ignore it.The list of arguments supplied with exceptions varies by exception and by what the sender provides. You can't always easily figure out what you can hope to obtain in the way of additional information. One way to handle the problem is to simply print everything by using code like this:
import sys
try:
File = open('myfile.txt')
except IOError as e:
for Arg in e.args:
print(Arg)
else:
print("File opened as expected.")
File.close();
The args
property always contains a list of the exception arguments in string format. You can use a simple for
loop to print each of the arguments. The only problem with this approach is that you're missing the argument names, so you know the output information (which is obvious in this case), but you don’t know what to call it.
import sys
try:
File = open('myfile.txt')
except IOError as e:
for Entry in dir(e):
if (not Entry.startswith("_")):
try:
print(Entry, " = ", e.__getattribute__(Entry))
except AttributeError:
print("Attribute ", Entry, " not accessible.")
else:
print("File opened as expected.")
File.close();
In this case, you begin by getting a listing of the attributes associated with the error argument object using the dir()
function. The output of the dir()
function is a list of strings containing the names of the attributes that you can print. Only those arguments that don't start with an underscore (_) contain useful information about the exception. However, even some of those entries are inaccessible, so you must encase the output code in a second try…except
block.
The attribute name is easy because it’s contained in Entry
. To obtain the value associated with that attribute, you must use the __getattribute()
function and supply the name of the attribute you want. When you run this code, you see both the name and the value of each of the attributes supplied with a particular error argument object. In this case, the actual output is as follows:
args = (2, 'No such file or directory')
Attribute characters_written not accessible.
errno = 2
filename = myfile.txt
filename2 = None
strerror = No such file or directory
winerror = None
with_traceback = <built-in method with_traceback of
FileNotFoundError object at 0x0000000003416DC8>
The previous sections of this chapter have discussed using exceptions, but as presented in previous chapters, Haskell actually discourages the use of exceptions, partly because they're indicative of state, and many functional programming aficionados discourage this use as well. The fact that Haskell does present exceptions as needed is proof that they’re not absolutely forbidden, which is a good thing considering that in some situations, you really do need to use exceptions when working with Python.
However, when working in a functional programming environment with Python, you have some alternatives to using exceptions that are more in line with the functional programming paradigm. For example, instead of raising an exception as the result of certain events, you could always use a base value, as discussed at https://softwareengineering.stackexchange.com/questions/334769/functional-style-exception-handling
.
Haskell also offers some specialized numeric handling that you might also want to incorporate as part of using Python. For example, as shown in Chapter 16, the Fractional
type allows statements such as 5 / 0
in Haskell. The same statement produces an error in Python. Fortunately, you have access to the fractions
package in Python, as described at https://docs.python.org/3/library/fractions.html
.
3.144.8.90