My last update on the continuing saga of a former ABAP Detective now in private practice hinted at a case. A statement case, as it were. This isn't the usual evidence chain of some performance or security issue, this is a grammar beat.
Background
I'm using Python in several recent projects and have observed the language specifications evolve, primarily from the perspective of new features or modules to make code simpler and clearer.
As I've used it for about 10 years (much less than my usual toolkit of C, Perl, and shell scripts, not to mention SQL), I have installed Python repeatedly. Depending on the platform, possibly deploying as a pre-built package but sometimes compiling from source if packages are not yet available.
On my last PC, I had over a half-dozen steps in the Python base:
Python 2.7.14 (default, Sep 18 2017, 09:17:44) [GCC 7.2.0 64 bit (AMD64)] on win32
Python 2.7.18 (default, Jan 2 2021, 09:22:32)
Python 3.5.0 (v3.5.0:374f501f4567, Sep 13 2015, 02:27:37) [MSC v.1900 64 bit (AMD64)] on win32
Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 17:00:18) [MSC v.1900 64 bit (AMD64)] on win32
Python 3.6.13 (default, Feb 16 2021, 07:46:47)
Python 3.7.1 (v3.7.1:260ec2c36a, Oct 20 2018, 14:57:15) [MSC v.1915 64 bit (AMD64)] on win32
Python 3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 18:58:18) [MSC v.1900 64 bit (AMD64)] on win32
Python 3.8.1 (tags/v3.8.1:1b293b6, Dec 18 2019, 23:11:46) [MSC v.1916 64 bit (AMD64)] on win32
I'd find folders with Python instances unpacked from software packages such as QGIS:
./Program Files/QGIS 3.24.1/apps/Python39/python39.dll
The newer PC doesn't have quite as many, and no 2.X at all:
Python 3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 18:58:18) [MSC v.1900 64 bit (AMD64)] on win32
Python 3.8.10 (default, Jan 3 2022, 18:29:01) [MSC v.1929 64 bit (AMD64)] on win32
Python 3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)] on win32
UNIX-like/Linux/BSD systems likely to be yoked to an SAP enterprise design have similar numbers/versions, even in the much smaller class systems available (that run databases just fine too).
/* A */
Python 2.7.18 (default, Nov 19 2021, 17:56:02)
Python 3.8.12 (default, Nov 17 2021, 15:26:49)
Python 3.9.7 (default, Jan 11 2022, 14:50:02)
/* B */
Python 3.8.12 (default, Jan 30 2022, 00:31:31)
Python 3.10.2 (main, Jul 13 2022, 22:00:15) [Clang 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3 on freebsd13
Go To Statement Considered Harmful
Before delving into the syntax and statements for logic switches, I was reminded of the classic article by Edgar Dijkstra, which the reader can find if interested.
https://www.cwi.nl/about/history/e-w-dijkstra-brilliant-colourful-and-opinionated
https://stackoverflow.com/questions/5878770/how-to-use-go-to-in-cobol
The concepts Dijkstra mentioned include "conditional clauses" and "alternative clauses". A simple gate of, say, positive or negative allows for simple language statements. Deeper logic would require more complex grammar, and the avoidance of GOTO should make code easier to read (for humans, not for execution pipelines).
I threw in a reference to GOTO in COBOL as that language has similarities to ABAP. The latter has a switch/when/then/else statement [see
help.sap.com/doc/abapdocu_750_index_htm/7.50/en-US/abenconditional_expression_switch.htm ] but as this is a Python thread we'll leave that there.
My albatross
Here is the Python code I want to rework. Originally the intent was to substitute the implied millibar unit values with those for inches of mercury, which quickly led to a reconsideration of the syntax beyond simply swapping 970 for 29.02, for instance.
def describe_pressure(pressure):
"""Convert pressure into barometer-type description."""
if pressure < 970:
description = "storm"
elif 970 <= pressure < 990:
description = "rain"
elif 990 <= pressure < 1010:
description = "change"
elif 1010 <= pressure < 1030:
description = "fair"
elif pressure >= 1030:
description = "dry"
else:
description = ""
return description
Source:
https://github.com/pimoroni/enviroplus-python/blob/master/examples/weather-and-light.py
Would I want to re-factor the above, using "cursor" type variables from lookup tables? You bet. Do I want to re-code the run-on sentence of if-then-else clauses to a switch/case statement? Also yes.
It matters here because the concept of switch/case/default that is available in multiple languages is only in Python 3.10 or later. A few examples follow.
Shell
case $INPUT_STRING in
hello)
echo "Hello yourself!"
;;
*)
echo "Sorry, I don't understand"
;;
esac
Source:
https://www.shellscript.sh/case.html
Muddy if you don't do shell scripts, otherwise normal as day/night. For the unfamiliar, bracketing the beginning and ending with the word "case" and then "case" spelled backwards is how this feature was implemented, and one of the idiosyncrasies you just live with. Ending each stanza with a double-semi-colon (does that mean a whole colon?) is another twist, though ending the default with the widely used "asterisk" meaning anything left seems tame.
K&R C
switch(curtok) {
case LBRACK:
if (cur == NULL)
cur = newtrans();
else
fatal("duplicate state list");
statelist(fp,cur); /* set states */
continue; /* prepare to read char */
/* ... */
default: fatal("bad input format");
}
Source: C-Kermit v 292
find me here:
https://www.kermitproject.org/ckermit70.html
latest:
https://www.kermitproject.org/ck90.html
Old C had "integer-valued constants or constant expressions." Meaning hard-coded specific values and not ranges or other conditions.
Perl
Perl doesn't have switch/case in the core; sort of an equivalent can be sourced in a loadable module:
use feature "switch";
use v5.14;
given ($var) {
$abc = 1 when /^abc/;
$def = 1 when /^def/;
$xyz = 1 when /^xyz/;
default { $nothing = 1 }
}
Source:
https://perldoc.perl.org/perlsyn#Switch-Statements
Since this feature add-on was crafted late in the evolution of Perl grammar, I don't see it widely used, and frankly, had to search to even find it. Instead of the term "case" or maybe "switch" we now have a different term "given" which is very English-sentence-structure-logic based. And for me, the left-to-right reversal of the condition check/result makes it harder to follow because my brain has been wired with the more common order.
SQL
For SQL, CASE is handy for inline transformations:
CASE
WHEN month = 'Jan' THEN 1
when month = 'Feb' then 2
WHEN month = 'Mar' THEN 3
when month = 'Apr' then 4
WHEN month = 'May' THEN 5
/**/
when month = 'Dec' then 12
else 0 END month_N,
While basic shell script logic might be restricted to integral conditions, other languages, including SQL, permit complex statements in the test cases. If not careful, this can lead to errors if the fallout is not trapped correctly. Here, I placed the fallout "else" on the same line with the clause-ending "END", yet another variant on the closure. Mixing upper and lower case was just for fun and legibility.
Python
In the statement I'm planning to rewrite, the ranges must overlap cleanly, which requires thought when committing the code.
The first attempt:
match value:
case val if val < 970:
result = 'storm'
case val if val in range (970, 990):
result = 'rain'
case val if val in range (990, 1010):
result = 'change'
case val if val in range (1010, 1030):
result = 'fair'
case val if val >= 1030:
result = 'dry'
case other:
result = ''
Source: here
This style is easier for me to check for validity than the above if-then-else blocks earlier. When I worked this logic out, I found the edge cases less than intuitive. While the original if/then/else statement is clear where each value should fall (
less than as opposed
to less than or equal to) the
range function is not.
Typically, you might see the Python range function in the place where other languages would have a "for ..." clause, iterating a loop. Here, it is used as a logical true/false check.
Back story
In a previous
post, I wrote: "The above declaration of '
python 3.10 or later' is an aside I may expound further later. The switch/case language grammar muddle. Added to the Python language spec as per 'PEP 634: Structural Pattern Matching'."
The plethora of installed Python versions prior to 3.10 is worrying when considering leveraging a nice feature not yet widely available. For specific landscapes, you would want to ensure that all of the target systems support this version. Otherwise, the code would fault with an error:
File "./throttle38.py", line 8
match command:
^
SyntaxError: invalid syntax
To ensure the right python is called, one way (for UNIX like shells) is:
#!/usr/local/bin/python3.10
If this path is not found, the code will also fail.
bash: ./throttle310.py: /usr/local/bin/python3.10: bad interpreter: No such file or directory
Conclusions
Hopefully you will now appreciate that code refactoring may sometimes lead to a wider expansion of the scope due to minimum version requirements, be they language, operating system, or any external API rules that must be obeyed.
References
https://peps.python.org/pep-0634/
https://peps.python.org/pep-0636/