GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing inventwithpython.com — Chapter 5 - Finding Code Smells
(close)
Prev: Chapter 4 - Choosing Understandable Names | Next: Chapter 6 - Writing Pythonic Code
...and other books as well! Or register for the online video course. You can also donate to support the author directly. ![]() Code that causes a program to crash is obviously wrong, but crashes aren’t the only indicator of issues in your programs. Other signs can suggest the presence of more subtle bugs or unreadable code. Just as the smell of gas can indicate a gas leak or the smell of smoke could indicate a fire, a code smell is a source code pattern that signals potential bugs. A code smell doesn’t necessarily mean a problem exists, but it does mean you should investigate your program. This chapter lists several common code smells. It takes much less time and effort to prevent a bug than to encounter, understand, and fix a bug later. Every programmer has stories of spending hours debugging only to find that the fix involved changing a single line of code. For this reason, even a whiff of a potential bug should give you pause, prompting you to double-check that you aren’t creating future problems. Of course, a code smell isn’t necessarily a problem. Ultimately, whether to address or ignore a code smell is a judgment call for you to make. Duplicate CodeThe most common code smell is duplicate code. Duplicate code is any source code that you could have created by copying and pasting some other code into your program. For example, this short program contains duplicate code. Notice that it asks how the user is feeling three times:
Duplicate code is a problem because it makes changing the code difficult; a change you make to one copy of the duplicate code must be made to every copy of it in the program. If you forget to make a change somewhere, or if you make different changes to different copies, your program will likely end up with bugs. The solution to duplicate code is to deduplicate it; that is, make it appear once in your program by placing the code in a function or loop. In the following example, I’ve moved the duplicate code into a function and then repeatedly called that function:
In this next example, I’ve moved the duplicate code into a loop:
You could also combine these two techniques and use a function and a loop:
Notice that the code that produces the “Good morning/afternoon/evening!” messages is similar but not identical. In the third improvement to the program, I parameterized the code to deduplicate the identical parts. Meanwhile, the As with all code smells, avoiding duplicate code isn’t a hard-and-fast rule you must always follow. In general, the longer the duplicate code section or the more duplicate copies that appear in your program, the stronger the case for deduplicating it. I don’t mind copying and pasting code once or even twice. But I generally start to consider deduplicating code when three or four copies exist in my program. Sometimes, code is just not worth the trouble of deduplicating. Compare the first code example in this section to the most recent one. Although the duplicate code is longer, it’s simple and straightforward. The deduplicated example does the same thing but involves a loop, a new Duplicate code is a code smell because it makes your code harder to change consistently. If several duplicates are in your program, the solution is to place code inside a function or loop so it appears only once. Magic NumbersIt’s no surprise that programming involves numbers. But some of the numbers that appear in your source code can confuse other programmers (or you a couple weeks after writing them). For example, consider the number
The
This is a good solution, but an even better one is to replace these “magic” numbers with constants. Constants are variables whose names are written in uppercase letters to indicate that their values shouldn’t change after their initial assignment. Usually, constants are defined as global variables at the top of the source code file:
You should use separate constants for magic numbers that serve different purposes, even if the magic number is the same. For example, there are 52 cards in a deck of playing cards and 52 weeks in a year. But if you have both amounts in your program, you should do something like the following:
When you run this code, the output will look like this:
Using separate constants allows you to change them independently in the future. Note that constant variables should never change values while the program is running. But this doesn’t mean that the programmer can never update them in the source code. For example, if a future version of the code includes a joker card, you can change the
The term magic number can also apply to non-numeric values. For example, you might use string values as constants. Consider the following program, which asks the user to enter a direction and displays a warning if the direction is north. A
This bug can be hard to detect: the typo in the
The
Magic numbers are a code smell because they don’t convey their purpose, making your code less readable, harder to update, and prone to undetectable typos. The solution is to use constant variables instead. Commented-Out Code and Dead CodeCommenting out code so it doesn’t run is fine as a temporary measure. You might want to skip some lines to test other functionality, and commenting them out makes them easy to add back in later. But if commented-out code remains in place, it’s a complete mystery why it was removed and under what condition it might ever be needed again. Consider the following example:
This code prompts many unanswered questions: Why was Dead code is code that is unreachable or logically can never run. For example, code inside a function but after a
The Stubs are an exception to these code smell rules. These are placeholders for future code, such as functions or classes that have yet to be implemented. In lieu of real code, a stub contains a
When this function is called, it does nothing. Instead, it indicates that code will eventually be added in. Alternatively, to avoid accidentally calling an unimplemented function, you can stub it with a
Raising a Commented-out code and dead code are code smells because they can mislead programmers into thinking that the code is an executable part of the program. Instead, remove them and use a version control system, such as Git or Subversion, to keep track of changes. Version control is covered in Chapter 12. With version control, you can remove the code from your program and, if needed, easily add it back in later. Print DebuggingPrint debugging is the practice of placing temporary
Print debugging is deceptively quick and simple. But it often requires multiple iterations of rerunning the program before you display the information you need to fix your bug. The solution is to use a debugger or set up logfiles for your program. By using a debugger, you can run your programs one line of code at a time and inspect any variable. Using a debugger might seem slower than simply inserting a Logfiles can record large amounts of information from your program so you can compare one run of it to previous runs. In Python, the built-in
After importing the Variables with Numeric SuffixesWhen writing programs, you might need multiple variables that store the same kind of data. In those cases, you might be tempted to reuse a variable name by adding a numeric suffix to it. For example, if you’re handling a signup form that asks users to enter their password twice to prevent typos, you might store those password strings in variables named Let’s look at another example: if you have a function that deals with start and destination coordinates, you might have the parameters If your numeric suffixes extend past 2, you might want to use a list or set data structure to store your data as a collection. For example, you could store the values of This code smell doesn’t apply to every variable that simply ends with a number. For example, it’s perfectly fine to have a variable named Classes That Should Just Be Functions or ModulesProgrammers who use languages such as Java are used to creating classes to organize their program’s code. For example, let’s look at this example
This might seem like well-organized code, but think about what our actual needs are: a random number between 1 and 6. We could replace this entire class with a simple function call:
Compared to other languages, Python uses a casual approach to organizing code, because its code isn’t required to exist in a class or other boilerplate structure. If you find you’re creating objects simply to make a single function call, or if you’re writing classes that contain only static methods, these are code smells that indicate you might be better off writing functions instead. In Python, we use modules rather than classes to group functions together. Because classes must be in a module anyway, putting this code in classes just adds an unnecessary layer of organization to your code. Chapters 15 through 17 discuss these object-oriented design principles in more detail. Jack Diederich’s PyCon 2012 talk “Stop Writing Classes” covers other ways that you might be overcomplicating your Python code. List Comprehensions Within List ComprehensionsList comprehensions are a concise way to create complex list values. For example, to create a list of strings of digits for the numbers 0 up to 100, excluding all multiples of 5, you’d typically need a
Alternatively, you can create this same list in a single line of code by using the list comprehension syntax:
Python also has syntax for set comprehensions and dictionary comprehensions:
A set comprehension 1 uses braces instead of square brackets and produces a set value. A dictionary comprehension 2 produces a dictionary value and uses a colon to separate the key and value in the comprehension. These comprehensions are concise and can make your code more readable. But notice that the comprehensions produce a list, set, or dictionary based on an iterable object (in this example, the
But nested list comprehensions (or nested set and dictionary comprehensions) cram a lot of complexity into a small amount of code, making your code hard to read. It’s better to expand the list comprehension into one or more
Comprehensions can also contain multiple
This list comprehension contains two
Comprehensions are syntactic shortcuts that can produce concise code, but don’t go overboard and nest them within each other. Empty except Blocks and Poor Error MessagesCatching exceptions is one of the primary ways to ensure that your programs will continue to function even when problems arise. When an exception is raised but there is no You can prevent crashes by supplying an
This code doesn’t crash when Handling exceptions with poor error messages is another code smell. Look at this example:
This code doesn’t crash, which is good, but it doesn’t give the user enough information to know how to fix the problem. Error messages are meant to be read by users, not programmers. Not only does this error message have technical details that a user wouldn’t understand, such as a reference to the It’s easier for programmers to quickly write a single, unhelpful description of what happened rather than detailed steps that the user can take to fix the problem. But keep in mind that if your program doesn’t handle all possible exceptions that could be raised, it’s an unfinished program. Code Smell MythsSome code smells aren’t really code smells at all. Programming is full of half-remembered bits of bad advice that are taken out of context or stick around long after they’ve outlived their usefulness. I blame tech book authors who try to pass off their subjective opinions as best practices. You might have been told some of these practices are code smells, but they’re mostly fine. I call them code smell myths: they’re warnings that you can and should ignore. Let’s look at a few of them. Myth: Functions Should Have Only One return Statement at the EndThe “one entry, one exit” idea comes from misinterpreted advice from the days of programming in assembly and FORTRAN languages. These languages allowed you to enter a subroutine (a structure similar to a function) at any point, including in the middle of it, making it hard to debug which parts of the subroutine had been executed. Functions don’t have this problem (execution always begins at the start of the function). But the advice lingered, becoming “functions and methods should only have one Trying to achieve a single Myth: Functions Should Have at Most One try Statement“Functions and methods should do one thing” is good advice in general. But taking this to mean that exception handling should occur in a separate function goes too far. For example, let’s look at a function that indicates whether a file we want to delete is already nonexistent:
Proponents of this code smell myth argue that because functions should always do just one thing, and error handling is one thing, we should split this function into two functions. They argue that if you use a
This is unnecessarily complicated code. The Your functions should be small and simple, but this doesn’t mean they should always be limited to doing “one thing” (however you define that). It’s fine if your functions have more than one Myth: Flag Arguments Are BadBoolean arguments to function or method calls are sometimes referred to as flag arguments. In programming, a flag is a value that indicates a binary setting, such as “enabled” or “disabled,” and it’s often represented by a Boolean value. We can describe these settings as set (that is, The false belief that flag arguments to function calls are bad is based on the claim that, depending on the flag value, the function does two entirely different things, such as in the following example:
Indeed, if your function looks like this, you should create two separate functions rather than making an argument decide which half of the function’s code to run. But most functions with flag arguments don’t do this. For example, you can pass a Boolean value for the Myth: Global Variables Are BadFunctions and methods are like mini-programs within your program: they contain code, including local variables that are forgotten when the function returns. This is similar to how a program’s variables are forgotten after it terminates. Functions are isolated: their code either performs correctly or has a bug depending on the arguments passed when they’re called. But functions and methods that use global variables lose some of this helpful isolation. Every global variable you use in a function effectively becomes another input to the function, just like the arguments. More arguments mean more complexity, which in turn means a higher likelihood for bugs. If a bug manifests in a function due to a bad value in a global variable, that bad value could have been set anywhere in the program. To search for a likely cause of this bad value, you can’t just analyze the code in the function or the line of code calling the function; you must look at the entire program’s code. For this reason, you should limit your use of global variables. For example, let’s look at the
Let’s say when we run this program, we encounter the following exception:
The program has a zero divide error, caused by the line Note that global constants aren’t considered poor programming practice. Because their values never change, they don’t introduce complexity into the code the way other global variables do. When programmers mention that “global variables are bad,” they aren’t referring to constant variables. Global variables broaden the amount of debugging needed to find where an exception-causing value could have been set. This makes abundant use of global variables a bad idea. But the idea that all global variables are bad is a code smell myth. Global variables can be useful in smaller programs or for keeping track of settings that apply across the entire program. If you can avoid using a global variable, that’s a sign you probably should avoid using one. But “global variables are bad” is an oversimplified opinion. Myth: Comments Are UnnecessaryBad comments are indeed worse than no comments at all. A comment with outdated or misleading information creates more work for the programmer instead of a better understanding. But this potential problem is sometimes used to proclaim that all comments are bad. The argument asserts that every comment should be replaced with more readable code, to the point that programs should have no comments at all. Comments are written in English (or whatever language the programmer speaks), allowing them to convey information to a degree that variable, function, and class names cannot. But writing concise, effective comments is hard. Comments, like code, require rewrites and multiple iterations to get right. We understand the code we write immediately after writing it, so writing comments seems like pointless extra work. As a result, programmers are primed to accept the “comments are unnecessary” viewpoint. The far more common experience is that programs have too few or no comments rather than too many or misleading comments. Rejecting comments is like saying, “Flying across the Atlantic Ocean in a passenger jet is only 99.999991 percent safe, so I’m going to swim across it instead.” Chapter 10 has more information about how to write effective comments. SummaryCode smells indicate that there might be a better way to write your code. They don’t necessarily require a change, but they should make you take another look. The most common code smell is duplicate code, which can signal an opportunity to place code inside a function or loop. This ensures future code changes will need to be made in only one place. Other code smells include magic numbers, which are unexplained values in your code that can be replaced by constants with descriptive names. Similarly, commented-out code and dead code are never run by the computer, and might mislead programmers who later read the program’s code. It’s best to remove them and rely on a source control system like Git if you later need to add them back to your program. Print debugging uses Variables with numeric suffixes, such as Additionally, any exceptions handled with empty Along with these code smells are the code smell myths: programming advice that is no longer valid or has, over time, proven counterproductive. These include putting only a single Of course, as with all programming advice, the code smells described in this chapter might or might not apply to your project or personal preferences. A best practice isn’t an objective measure. As you gain more experience, you’ll come to different conclusions about what code is readable or reliable, but the recommendations in this chapter outline issues to consider. Buy this book direct from the publisher. (Free ebook) Bookshop.org Powells Amazon Barnes and Noble |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |