[Tutor] Setting thresholds in a compact way
Robert Alexander
gogonegro at gmail.com
Thu Jan 2 04:19:09 EST 2020
Thank you all for your great replies which go a long way into making me
better understand python and my brain :)
While you prepared your better replies I actually had wrapped my beginner
head around the simple nested if as follows:
def set_frequency(down, up, loss):
"""Depending on the speed degradation change email frequency.
The returned frequency is expressed in hours.
"""
down_degradation = down / TD
if down_degradation <= .1:
return 1
elif (down_degradation > .1) and (down_degradation <= .4):
return 3
elif (down_degradation > .4) and (down_degradation <= .8):
return 4
elif (down_degradation > .8) and (down_degradation <= .9):
return 6
else:
return DEFAULT_TEST_FREQUENCY
As you can surmise DEFAULT_TEST_FREQUENCY is an external constant (set to
24) and currently uses only the passed download value.
As you suggested I had tested this function with a small main calling it
with random.random() value that provides a float between 0 and 1 and
printing the value it returned. I am satisfies by the behaviour.
I will spend the next month poring through the wisdom and arcane :) tricks
in your emails.
Would it be of any value for me to share the whole program (on GitHub) on
the list when it’s soundly working to see if you can suggest some overall
betterment of the style or flow?
Again very grateful for your inputs.
Ciao from Rome, Italy
PS The program monitors your ADSL speeds and when values drop under
thresholds sends a warning email
PPS The critical feature I still have to implement is to be able to reply
in a safe way to the email and stop the monitor in case I am traveling
around the planet and the ADSL goes bonkers and programs spits out an email
per hour ;)
On 2 January 2020 at 05:45:38, David L Neil via Tutor (tutor at python.org)
wrote:
On 2/01/20 5:20 AM, Robert Alexander wrote:
> I am trying to setup a varying time.sleep value (in house) depending on a
> priorly determined threshold.
>
> If threshold <= .9 AND > .8 then sleep should be 6 hours
> if threshold <= 0.8 AND > .4 then sleep should be 4 hours
> if threshold <= .4 AND > .1 then sleep should be 3 hours
> if threshold <= .1 then sleep should be set to 1 hour
>
> Instead of a complex/lengthy series of (nested?) if/ands based on the
> above, I was wondering if I could declare a dictionary such as:
>
> sleep_thresholds_hours = {.9:6, .8:4, .4:3, .1:1}
>
> And then use some sort of single statement to set the sleep_time to the
the
> proper value.
>
> Not even quite sure that using floats (albeit single decimal) as
dictionary
> keys will be ok.
>
> Getting old and in 2020 even older and my brain fogged by too much food
:)
A worked-solution...
Perhaps the seasonal influences explain it, but my (aged and decrepit)
mind prefers to construct the problem in the reverse of how it is stated
here...
Familiar with Cartesian coordinates and similar, to me the
x-axis/time-line starts at zero and goes 'up' to the right - and the
y-axis starts at zero and goes 'up', um, as the y-value goes 'up'. That
said, increasing/rising ranges seem easier (for me) to visualise:
<= .1 1
.4 3
.8 4
.9 6
(per 'excuses', below, am ignoring values of .9 or above, and similarly,
numbers which may/not be 'below' the illustrated range)
Notice that (in mathematical terminology) we now have two (ordered)
vectors (or 1-D matrices - or a single 2-D matrix, if you prefer.
Yeah/nah, leave that until the end...)
Let's call the left column-vector "sleep_threshold", and the right,
"sleep_hours". In Python:
sleep_threshold = [.1, .4, .8, .9]
sleep_hours = [ 1, 3, 4, 6 ]
Proving that we know what we're doing (maybe):-
for ptr in range( 0, len( sleep_threshold ) ):
print( f"{ ptr } { sleep_threshold[ ptr ] } { sleep_hours[ ptr ] }" )
0 0.1 1
1 0.4 3
2 0.8 4
3 0.9 6
(apologies, email word-wrapping may produce a less-than pleasing
graphological presentation. Similarly, when coding tests/"proofs", I've
saved vertical-space by typing the entire for-loop on a single line -
which many of my colleagues will (quite rightly) decry...)
Right, time to get to work! We could (per an earlier suggestion) try
iterating through the "sleep_threshold" list, in order to select the
corresponding "sleep_hours" element:
def sleepy( value ):
for ptr in range( 0, len( sleep_threshold ) ):
if value < sleep_threshold[ ptr ]:
print( f"{ value } results in sleep for {
sleep_hours[ ptr ] }" )
break
Yes, a function *should* be named using a verb which describes what it
does. I don't really know what you want it to do! (what a feeble excuse!)
Also, in this case, I'm only bothering to print the amount of sleep-time
by way of illustration. Presumably, you will want such a function to
return the sleep_hours value, or to execute the sleep right
there-and-then...]
What more criticism? Yes! Why did I put this code inside a function?
1. Presumption on my part (of your needs)
2. Programming style - each separate action is to be carried-out by a
single function/method ("separation of concerns")
3. For ease of testing!
Speaking of testing: how would we know if our magnificent-creation is
working, without testing the code? Normally, I would use PyTest, but as
this is being presented as a running-commentary, let's code some tests,
in-line:-
test_data = [ .05, .1, .2, .6, .85 ]
for test_value in test_data: print( test_value )
0.05
0.1
0.2
0.6
0.85
Please note that I've only used one 'border' value (0.1) and ignored any
concept of "outliers". (I'm going to claim simplicity. You can say
'laziness'...).
Now we're ready to experiment:-
for test_value in test_data: sleepy( test_value )
0.05 results in sleep for 1
0.1 results in sleep for 3
0.2 results in sleep for 3
0.6 results in sleep for 4
0.85 results in sleep for 6
What brilliance! It's almost as good-looking as am I!
Did you notice the 'oops!' in the test-data output? (*and* in the code?)
A very common error when coding such a 'ladder', is the comparison. How
do we regard values 'on the border' - are they 'in' or 'out' (in
mathematics: "inclusive" or "exclusive" of the range)? In this case,
your outline clearly shows them as "inclusive" and therefore the second
test (0.1 to sleep for 3) *failed*.
Let's correct 'my' error (and evaluate the results, again), before we go
any further...
def sleepy( value ):
for ptr in range( 0, len( sleep_threshold ) ):
if value <= sleep_threshold[ ptr ]:
print( f"{ value } results in sleep for {
sleep_hours[ ptr ] }" )
break
for test_value in test_data: sleepy( test_value )
0.05 results in sleep for 1
0.1 results in sleep for 1
0.2 results in sleep for 3
0.6 results in sleep for 4
0.85 results in sleep for 6
That's better! Now are you happy?
Good, but I'm not!
(some say I'm never happy...)
A second, similar, and also startlingly-common, error, in this coding
idiom is the "out by one error". In this case, I've not fallen into such
a 'trap', but essentially the 'danger' is in looping (in this case, over
the test data) one time too few, or one time too many, ie "out by one"!
Is this correct: for ptr in range( 0, len( sleep_threshold ) )?
I describe the above "style" as coding in a Java or C (or ...) idiom. A
'purist' would say that it has *no* place in Python.
Python's language-design gives us a significantly more-powerful for
statement, which can be differentiated from 'the others' by terming it a
"for-each" statement, ie for-each element in a collection, do...
Thus, we don't need to know how large the collection happens to be
(len()), and the 'out by one' error goes away - 'out by one' thrown-out?
- consigned to languish in the history of cyber-space...
OK, so with for-each in mind, we could try:
for threshold in sleep_threshold: etc
but what to do when we want to access the corresponding element in
sleep_hours, ie sleep_hours[ ptr ]???
Back to the drawing-board? Back to using a "pointer"?
No! Python has another trick up its sleeve (another 'battery' - when
they say that Python has "batteries included").
Somehow, we need to link the two lists/column-vectors (sleep_threshold
and sleep_hours). We can do this with zip() - we zip the two lists into
a single list, such that each element of the new list is comprised of
two values.
(remember how other posts talked of using a Python dictionary? A
dictionary's elements have two components, a "key", and a "value".
Similar motivation: finding a method of keeping the two 'together')
NB 'linking' lists using zip() is *not* the same as a ComSc concept or
algorithm called "linked lists"!
Onwards:-
sleep_thresholds_hours = zip( sleep_threshold, sleep_hours )
print( sleep_thresholds_hours )
<zip object at 0x7f747579a640>
Oh dear, it's not going to show us what we want to see. Let's use a bit
more power - more POWER!!!
for row in sleep_thresholds_hours: print( row )
(0.1, 1)
(0.4, 3)
(0.8, 4)
(0.9, 6)
Will you look at that, I'm back to mathematics and raving about
matrices/matrixes! Someone (please) stop him...
Here's revised code, written to be "pythonic", ie using a Python idiom,
and ridding us of any distasteful Java-influence (or whatever) - "we"
are so much better than "they"!
Engaging the powers of zip() and for-each:-
def sleepy_zip( value ):
for sleep_threshold, sleep_hours in sleep_thresholds_hours:
if value <= sleep_threshold:
print( f"{ threshold } results in sleep for {
sleep_hours }" )
break
for test_value in test_data: sleepy_zip( test_value )
Incidentally, if it will help comprehension, the twin-components of each
sleep_thresholds_hours element are a tuple - and therefore you might
prefer the more *explicit* syntax:-
for ( sleep_threshold, sleep_hours ) in sleep_thresholds_hours:
The 'Zen of Python' does recommend preferring the explicit over
implications!
Yahoo! Finally, it's time to go home...
No! Not so fast. Where's the output?
Um, there was none!
Do you 'see' any problem with this?
What happened is that zip() produces an iterator object
(https://docs.python.org/3.5/library/functions.html#zip), and iterators
can only be "consumed" once.
Think about it, if you zip 'up' a piece of clothing (ie use the zip()),
and then "consume" the zip in the for-loop (unzipping it), you can't
thereafter "consume" it again - the zip has been unzipped all the way
'down' and so can't be unzipped any further...
(As 'home-spun wisdom' goes, that's not too bad - or is it "home-zipped
wisdom"?)
So, when I demonstrated the results of the zip() by printing-out the
contents for you, It was "consumed". Thus, when we came to run
sleepy_zip(), there was no 'zip' left to 'unzip', and to borrow from
another piece of 'wisdom': no data in, no data out.
My fault, AGAIN!
Sure enough:-
print( "v"*8 )
for row in sleep_thresholds_hours: print( row )
print( "^"*8 )
vvvvvvvv
^^^^^^^^
(the 'results' should appear between the two lines of 'arrows')
Let's re-start, this time with feeling (yes, I can feel that zip going
'up'!). This time, let's put the zip() where it is going to be consumed,
ie where there's no chance that someone, or something else, will consume
it before we execute the function/do the business:-
def sleepy_zip( value ):
for threshold, hours in zip( sleep_threshold, sleep_hours ):
if value <= threshold:
print( f"{ value } results in sleep for { hours }" )
break
for test_value in test_data: sleepy_zip( test_value )
0.05 results in sleep for 1
0.1 results in sleep for 1
0.2 results in sleep for 3
0.6 results in sleep for 4
0.85 results in sleep for 6
BTW have you spotted the virtue of using "break" - or what might happen
if we didn't? Mind you, I do feel a little short of sleep - so I'm not
complaining!
Hope this helps. If I have misunderstood any of the 'real' criteria,
hopefully you will be able to adapt-to-suit.
As mentioned, normally I use PyTest to help prove my code. Thus the "for
test_value etc" lines would be located elsewhere (rather than in-line).
Also, were I to start writing this all over again, but still using
in-line testing, I'd zip() test_value with the expected results - if we
go to the trouble of assembling a test-data-set, then let's ensure we
also know/remember the (correct) answers! In fact, you might even ask
the computer to check its own results, then:-
assert returned_result == expected_result
Python will come to an abrupt halt if things are not tickety-boo!
Similarly, you could improve the output by formatting the f-strings
within our print()-s. More power to you!
However, in time-honored fashion, I shall leave such 'extras' to you,
"as an exercise" (and/or when you're ready for such more-advanced coding
techniques)...
--
Regards =dn
_______________________________________________
Tutor maillist - Tutor at python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor
More information about the Tutor
mailing list