1
0
Fork 0
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
blog/content/python-goto.md

218 lines
5.8 KiB
Markdown

2 years ago
Goto in Python
==============
I really know that Python was meant to be used without a goto, but sometimes it
really makes sense to use it.
## Goto usage patterns:
Let's first show a few common and in my opinion sensible (at least sometimes)
uses of goto. Also, I'll discuss possible goto-free alternatives of the code
and why they feel worse.
### Error handling
The most common one, used all over the Linux kernel for example:
```c
// ...
if(! something_we_dont_want_to_fail()):
goto err;
// We know it hasn't failed, so we can use it
// ...
return result; // OK
err:
clean_up()
return 0; // This did not work.
```
Often, there are more goto's for one label, and fall through is utilized, e.g.:
```c
// Open the file
if (file_empty(f))
goto clean_files;
// Read the file
if (bad_data(f))
goto clean_files;
// Load data into memory
if (could_not_process(data))
goto clean_memory;
return processed_data;
clean_memory:
free(data);
clean_files:
close(f);
return 0;
```
The goto-free alternative is to put everything into functions and check all the
functions. This might lead to creating functions with no semantics, and that
does not generally make the code more readable.
The lucky part is, this is the case that is simple to solve with exceptions in
much the same way.
There is still a case to be made with the multiple cleanup blocks and
fall-through, but one of the ways of solving that is to have the exception know
what to clean up. That might be a bit ugly, but the goto is way worse.
### Alternative methods of evaluation
Sometimes, one would like to try several different paths to get a result, in
some order, i.e. with pseudocode like this:
```c
int result = 0;
// Method 1 (the most likely to work)
result = method_1();
if(result)
goto done;
// Method 1 did not work, try method 2:
result = method_2();
if(result)
goto done;
// Try method 3 maybe?
result = method_3();
if (! result)
return ERROR;
done:
// We now have result, so process that
// ...
```
Possible options to fixing this:
- Create a function that gets the result. In that function, the goto's can be
replaced by return's, so that is nice, but the function might not have
sensible name and need an unreasonable number of arguments, if the methods
are complicated enough.
- Reorder the methods to try from the least preffered to most. That obscures
the algorithm, and might not be an option, if the methods have side effects.
There exists a solution with exceptions: Raise an exception when one method
fails, and try other methods in the exception handling block. That leads to
complicated catch[^I will call it "catch" block even though in Python this
technically is an "except" block, because it seems more obvious what I mean.]
blocks, which is not a good practice. And what is worse, for the third method
the blocks would be nested!
### Retries
Sometimes, you need to try something several times, or skip something. In some
code (not originally in C), I had something along the lines of:
```c
struct line_with_number get_next_reasonable_line():
// Imagine here a static FILE object, from which I read the lines.
int linelen = 255;
char *line;
static int line_number
next_line:
getline(line, linelen, file);
if (line == NULL)
// Just return some kind of error
return {.line = NULL, .number = line_number};
line = strip_comments(line);
if (strlen(line) == 0)
goto next_line;
// Look at the line more closely
if (we_do_not_like(line))
goto next_line;
return {.line = line, .number = line_number};
```
The "obvious" solution to this is to use `while` and `continue`. But that is
not necessarily better -- the code is quite likely to pass only once, so there
is no semantics of a cycle. Also, what would be the condition here? We have no
data, so maybe `while(true)`. Or in the better case, `do ... while(strlen(line)
== 0 || we_do_not_like(line))`. When the condition is not so simple, using
infinite loop is the way to go. But such code is not prettier, since it uses
unreasonable flow control and loses the jump's meaning.
There may be a few more places where a goto is a sensible option, but I don't
recall any of them, so they might not be as important.
## What to do in Python?
There is no simple way around the second use-case for a goto. In some code, I
ended up with following code (and to make it worse, that was inside a function,
all of it):
```python
# NOTE: This is really ugly goto, simulated using exceptions
class EverythingIsOK(Exception): pass
try:
# Method 1:
result = method_1()
if result: raise EverythingIsOK() # Goto
# Method 1 did not work, try method 2:
result = method_2()
if result: raise EverythingIsOK()
# Try method 3 maybe?
result = method_3()
if not result:
raise OperationalError() # Not OK, Not goto.
except EverythingIsOK: # Label
pass
# Process result
```
This is ugly enough, but can only jump downwards. Jumping upwards probably
needs the trick with `continue`, and unfortunately, there is no `do-while` in
Python.
```python
while True: # Label
# code
continue # Goto
# code
break # Boilerplate
```
If you do not like that the goto's look different, you can use this mess:
```python
class MyGoto(Exception): pass
while True: # Label
try:
# code
raise MyGoto() # Goto
# code
break
except MyGoto: # Even more boilerplate.
continue
```
I have no solution for using a single label for jumping from above _and_ below.
But I hope I will never see code that needs that.
This post is not meant to encourage use of goto. It is considered harmful, and
in most cases, rightfully so. But still, there are some reasonable use cases,
so it is worth knowing when to use it and what can you do when the language
does not support goto.
And probably needless to say: I would still rather have native goto support,
than need to use awful hacks to simulate the flow which I have in mind.