Chapter 17

Handling Errors in Python

IN THIS CHAPTER

Check Understanding Python bugs

Check Considering bug sources

Check Locating and describing Python errors

Check 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.

Defining a Bug in Python

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.

Considering the sources of errors

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

  • Errors that occur at a specific time
  • Errors that are of a specific type

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.

Classifying when errors occur

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

  • Compile time: A compile time error occurs when you ask Python to run the application. Before Python can run the application, it must interpret the code and put it into a form that the computer can understand. A computer relies on machine code that is specific to that processor and architecture. If the instructions you write are malformed or lack needed information, Python can’t perform the required conversion. It presents an error that you must fix before the application can run.
  • 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).

    Remember 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.

Distinguishing error types

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.

Tip Understanding the error types helps you locate errors faster, earlier, and more consistently, resulting in fewer misdiagnoses. The best developers know that fixing errors while an application is in development is always easier than fixing it when the application is in production because users are inherently impatient and want errors fixed immediately and correctly. In addition, fixing an error earlier in the development cycle is always easier than fixing it when the application nears completion because less code exists to review.

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):

  • Syntactical: Whenever you make a typo of some sort, you create a syntactical error. Some Python syntactical errors are quite easy to find because the application simply doesn’t run. The interpreter may even point out the error for you by highlighting the errant code and displaying an error message. However, some syntactical errors are quite hard to find. Python is case sensitive, so you may use the wrong case for a variable in one place and find that the variable isn’t quite working as you thought it would. Finding the one place where you used the wrong capitalization can be quite challenging.
  • Semantic: When you create a loop that executes one too many times, you don’t generally receive any sort of error information from the application. The application will happily run because it thinks that it’s doing everything correctly, but that one additional loop can cause all sorts of data errors. When you create an error of this sort in your code, it’s called a semantic error. Semantic errors are tough to find, and you sometimes need some sort of debugger to find them.
  • Logical: Some developers don’t create a division between semantic and logical errors, but they are different. A semantic error occurs when the code is essentially correct but the implementation is wrong (such as having a loop execute once too often). Logical errors occur when the developer’s thinking is faulty. In many cases, this sort of error happens when the developer uses a relational or logical operator incorrectly. However, logical errors can happen in all sorts of other ways, too. For example, a developer might think that data is always stored on the local hard drive, which means that the application may behave in an unusual manner when it attempts to load data from a network drive instead. Logical errors are quite hard to fix because the problem isn’t with the actual code, yet the code itself is incorrectly defined. The thought process that went into creating the code is faulty; therefore, the developer who created the error is less likely to find it. Smart developers use a second pair of eyes to help spot logical errors.

Considering version differences

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.

Remember Although the Python community has worked hard to make the transition easier, you can see significant functional programming differences by reviewing the Python 2.x material at https://docs.python.org/2/howto/functional.html and comparing it to the Python 3.x material at https://docs.python.org/3/howto/functional.html. The transition will introduce bugs into your applications, some of them quite hard to find and others that the compiler will let you know about. Articles, such as the one at http://sebastianraschka.com/Articles/2014_python_2_3_key_diff.html can help you locate and potentially fix these issues. (Note especially the integer division differences stated by the article because they really can throw your functional code off in a manner that is particularly hard to find.)

Understanding the Python-Related Errors

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.

Dealing with late binding closures

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)]

Remember This version of the code uses a trick to force the value of i to reflect the actual value produced by each of the values output by range(numValues). Instead of being part of the inner function, i is now provided as an input. You call the function in the same manner as before, but now the output is correct. Oddly enough, this particular problem isn't specific to lambda expressions; it can happen in any Python code. However, developers see it more often in this situation because the tendency is to use a lambda expression in this case.

Tip You can find another example of this late-binding closure issue in the posting at https://bugs.python.org/issue27738 (with another fix like the one shown in this section). The discussion at https://stackoverflow.com/questions/1107210/python-lambda-problems provides another solution to this problem using functools.partial(). The point is that you must remember that Python is late binding.

Using a variable

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='')

Working with third-party libraries

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.

Fixing Python Errors Quickly

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.

Understanding the built-in exceptions

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:

  • Base classes: The base classes provide the essential building blocks (such as the Exception exception) for other exceptions. However, you might actually see some of these exceptions, such as the ArithmeticError exception, when working with an application.
  • Concrete exceptions: Applications can experience hard errors — errors that are hard to overcome because no good way to handle them exists or they signal an event that the application must handle. For example, when a system runs out of memory, Python generates a 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.
  • OS exceptions: The operating system can generate errors that Python then passes along to your application. For example, if your application tries to open a file that doesn't exist, the operating system generates a FileNotFoundError exception.
  • Warnings: Python tries to warn you about unexpected events or actions that could result in errors later. For example, if you try to inappropriately use a resource, such as an icon, Python generates a 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.

Obtaining a list of exception arguments

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.

Technicalstuff A more complex method of dealing with the issue is to print both the names and the contents of the arguments. The following code displays both the names and the values of each of the arguments:

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>

Considering functional style exception handling

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.

Tip Although the fractions package addresses some issues and you get a full fractional type, that package doesn't address the 5 / 0 problem; you still get a ZeroDivisionError exception. To avoid this final issue, you can use specialized techniques such as those found in the message thread at https://stackoverflow.com/questions/27317517/make-division-by-zero-equal-to-zero. The point is that you have ways around exceptions in some cases if you want to use a more functional style of reporting. If you really want some of the advantages of using Haskell in your Python application, the hyphen module at https://github.com/tbarnetlamb/hyphen makes it possible.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.8.90