Re: [Tutor] How to avoid "UnboundLocalError: local variable 'goal_year' referenced before assignment"?

2019-03-24 Thread Cameron Simpson

On 24Mar2019 14:33, boB Stepp  wrote:

On 23Mar2019 22:15, boB Stepp  wrote:

The lambda is just a single line function definition, and doesn't get a
function name.



So your get_input name now accepts a "date_constructor" parameter and
you would change the input step from this:

try:
identifier = int(input(input_prompt))
if date_value_err_ck:
date(*date_value_err_ck)
except ValueError:

to this:

try:
value = int(input(input_prompt))
date = date_constructor(value)
except ValueError:


You could not know this from the code I showed (As I pared it down to
illustrate my problem.), but I am also using get_input() to pull in
from the user non-date values, too, but they are also all integers:
pages_read and total_pages_to_read.  Because of this, for these latter
two items, "date_value_err_ck" will be assigned "None", which is why I
have the "if date_value_err_ck:".


Ok. So I realised later that you're not using the date from 
date_constructor; you're just calling it to see if it raises ValueError 
as a sanity check.


Why not make that a condition, and drop date_value_err_ck altogether?

   'conditions': [
   (lambda goal_year: datetime.date(goal_year, 1, 1),
   "This doesn't look like a valid date year to datetime.date()."),


and make the condition checking code more robust:

   for bad_condition, message in conditions:
   try:
   badness = bad_condition(input_value)
   except ValueError as e:
   print("Invalid goal_year, cannot even run the condition check! " + 
str(e))
   else:
   if badness:
   print(message)

That reduces your opening try/except to just checking that the input is 
a valid int, a lot simpler.



Well, when you define the goal_month constructor lambda function,
goal_year _is_ a local variable. And _because_ it is not a parameter of
the lambda, the lambda finds it in the scope where it was defined: the
scope at the bottom of your programme.

This is called a "closure": functions has access to the scope they are
define in for identifiers not locally defined (eg in the parameters or
the function local variables - which are those assigned to in the code).


This is so cool and useful!  I recall seeing this mentioned for nested
functions, but did not know it worked for lambdas as well.


Lambda are functions. Same rules.

Cheers,
Cameron Simpson 
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor


Re: [Tutor] How to avoid "UnboundLocalError: local variable 'goal_year' referenced before assignment"?

2019-03-24 Thread boB Stepp
Oh, happy day!  eval() has been expunged from my program!!  I will now
continue from where I left off earlier.

On Sun, Mar 24, 2019 at 12:22 AM Cameron Simpson  wrote:
>
> On 23Mar2019 22:15, boB Stepp  wrote:
>
> The lambda is just a single line function definition, and doesn't get a
> function name.

> So your get_input name now accepts a "date_constructor" parameter and
> you would change the input step from this:
>
> try:
> identifier = int(input(input_prompt))
> if date_value_err_ck:
> date(*date_value_err_ck)
> except ValueError:
>
> to this:
>
> try:
> value = int(input(input_prompt))
> date = date_constructor(value)
> except ValueError:

You could not know this from the code I showed (As I pared it down to
illustrate my problem.), but I am also using get_input() to pull in
from the user non-date values, too, but they are also all integers:
pages_read and total_pages_to_read.  Because of this, for these latter
two items, "date_value_err_ck" will be assigned "None", which is why I
have the "if date_value_err_ck:".

> So you're passing in a function to make the date, and only calling it
> once you've read in the value.
>
> This solves your use of goal_year before it is defined in 2 ways: it
> doesn't use the value before the call to get_input and also the name
> "goal_year" in the constructor function is local to that function - it
> may as well be called "year".
>
> Now to your conditions. You have:
>
> 'conditions': [
> ('goal_year < date.today().year',
> "Have you invented a time machine?  If not, please enter a"
> " year that makes more sense!"),
> ('goal_year >= date.today().year + 100',
> "Have you discovered the secret to eternal life?  And how"
> " long is this book anyway?  Please enter a year that"
> " makes more sense.")]}
>
> These seem to be tests for bad values (personally I tend to write tests
> for good values, but this is an arbitrary choice). These should also be
> written as lambdas:

I just like writing corny error messages in response to bad values. ~(:>))

> But wait, there's more!
>
> After you have phase 1 complete (inputting goal_year) you then want
> goal_month.
>
> Let me introduce you to the closure:
>
> For the goal_year you have the year-based constructor using only the
> input value:
>
> 'date_constructor': lambda goal_year: datetime.date(goal_year, 1, 1),
>
> in your setup dict. For the goal_month you want both the goal_year and
> the input value. So...
>
> 'date_constructor': lambda goal_month: datetime.date(goal_year, 
> goal_month, 1),
>
> But, I hear you ask, I'm not passing in the goal_year to the get_input()
> for the month value! Indeed, your code will look like this:
>
> goal_year_params = {
> 
> 'date_constructor': lambda goal_year: datetime.date(goal_year, 1, 1),
> 
> }
> goal_year = get_input(**goal_year_params)
> goal_month_params = {
> 
> 'date_constructor': lambda goal_month: datetime.date(goal_year, 
> goal_month, 1),
> 
> }
> goal_month = get_input(**goal_month_params)
>
> That's your plan, yes?

You understand me perfectly!

> Well, when you define the goal_month constructor lambda function,
> goal_year _is_ a local variable. And _because_ it is not a parameter of
> the lambda, the lambda finds it in the scope where it was defined: the
> scope at the bottom of your programme.
>
> This is called a "closure": functions has access to the scope they are
> define in for identifiers not locally defined (eg in the parameters or
> the function local variables - which are those assigned to in the code).

This is so cool and useful!  I recall seeing this mentioned for nested
functions, but did not know it worked for lambdas as well.

This has been incredibly helpful and educational!  Many thanks!!

-- 
boB
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor


Re: [Tutor] How to avoid "UnboundLocalError: local variable 'goal_year' referenced before assignment"?

2019-03-23 Thread boB Stepp
Ah!  After another long break from coding, I resume a pattern of one
step forward, who knows how many steps back, as I forget what I once
remembered and understood.

It is the wee morning hours where I am at, so I don't think I will
make it through everything, but I will make a start...

On Sun, Mar 24, 2019 at 12:22 AM Cameron Simpson  wrote:
>
> Discussion below your post here, since I feel I should quote it all:
>
> On 23Mar2019 22:15, boB Stepp  wrote:

> >Traceback (most recent call last):
> >  File "pages_per_day.py", line 250, in 
> >start_pgm()
> >  File "pages_per_day.py", line 246, in start_pgm
> >goal_date_obj, pages_read, total_pages_to_read = get_inputs()
> >  File "pages_per_day.py", line 63, in get_inputs
> >'date_value_err_ck': (goal_year, 1, 1),
> >UnboundLocalError: local variable 'goal_year' referenced before assignment
> >
> >I understand this result, but cannot come up with a way to implement
> >my desired DRY strategy as I am stuck on how to get around this "local
> >variable ... referenced before assignment" issue.  On subsequent
> >passes "goal_year" will become "goal_month" and then "goal_day" as the
> >user needs to input all three of these numbers.  Is there a way to
> >accomplish my goal or am I attempting to be too clever?
>
> You're not attemnpting to be too clever, but you are making some basic
> errors. For example, you can't just run a string like 'goal_year <
> date.today).year' and you shouldn't be trying i.e. do _not_ reach for
> the eval() function.

Too late!  About the time your message hit my inbox, I just finished a
working replacement after pulling the the eval() trigger.  I found a
non-eval() workaround for dealing with date(*date_value_err_ck), but
could not come up with anything better than using
eval(condition.format(user_input)) to replace the "if condition:",
where the embedded "{0}" in "condition" from the calling code is being
used to pass in the actual user input value.  I imagine a clever user
(other than myself) could now wreak all sorts of havoc!  I totally got
rid of "identifier" as an argument.

> Let's look at your get_input() function. It basicly loops until you get
> a value you're happy with, and returns that value. Or it would if it
> were correct. Let's examine the main loop body:
>
> try:
> identifier = int(input(input_prompt))
> if date_value_err_ck:
> date(*date_value_err_ck)
> except ValueError:
> print(err_msg)
> continue
> for (condition, condition_msg) in conditions:
> if condition:
> print(condition_msg)
> break
> else:
> return identifier
>
> To start with, you have confusion in the code bwteen the name you're
> intending to use for the input value (the "identifier" parameter) and
> the value you're reading from the user. You go:

It wasn't really confusion on my part.  What I *wanted* to do was to
substitute a more specific identifier from the calling code for the
generic "identifier" in the get_input() function, and use it both for
the actual user input and to fill in that value wherever I need it.
But I could not find a way to accomplish this and just left my
question at the last error state.  But I guess this is not a doable
thing.

> identifier = int(input(input_prompt))
>
> That immediatey destroys the name you passed in as a parameter. Instead,
> use a distinct variable for the input value. Let's be imaginitive and
> call it "value":

Or "user_input", which I finally wound up with.

> Then you try to create a date from that value (though you don't save it
> anywhere). I presume you want to use the datetime.date() constructor.
> So:
>
> # at the start of your programme
> import datetime
>
> then in the function:
>
> date = datetime.date(*date_value_err_ck)
>
> I think your plan is that datetime.date() will also raise a ValueError
> for a bad year number in "value". So you want, in fact, to call:

That is, in fact, the plan.  Since I only want to check for a bad
year, then a bad month, and finally, a bad day of the month, I saw no
point in binding a name to the date object.  In the part of the code I
did not include, I create a date object from date(goal_year,
goal_month, goal_day) to use in the rest of the program.

> Let me introduce you to the lambda.

Or, in my case, "reintroduce" me to the lambda.  It has been probably
at least a couple of years since I had need of this and I had totally
forgotten about it!  There are those backward steps again!

I guess I will have to head for bed now and look more closely at the
rest later today.  But I can see that this totally eliminates any need
for eval() -- which I did not want to use anyway -- and solves what I
was struggling so much with.

As for closures, I will have to read that portion *most* carefully
later.  I know I have asked some questions in the past where these
were brought up as answers, but this did not stick in my brain at all.

Thanks, Cameron, 

Re: [Tutor] How to avoid "UnboundLocalError: local variable 'goal_year' referenced before assignment"?

2019-03-23 Thread Cameron Simpson

Discussion below your post here, since I feel I should quote it all:

On 23Mar2019 22:15, boB Stepp  wrote:

I have just written a small program earlier today to allow the user
(me) to enter a date by which I wish to finish reading a book (=
massive programming-related book) and calculate how many pages I need
to read each day (starting today) in order to finish the book by the
target date.  Looking over my code I found that I was repeating a
fairly regular pattern in collecting the user input, so I thought I
would *improve* my working program by removing the duplicated code
into a single function.  So I came up with:

from datetime import date

def get_input(greeting_msg, identifier, input_prompt, date_value_err_ck,
   err_msg, conditions):
   """ ??? """

   if greeting_msg:
   print(greeting_msg)

   while True:
   try:
   identifier = int(input(input_prompt))
   if date_value_err_ck:
   date(*date_value_err_ck)
   except ValueError:
   print(err_msg)
   continue
   for (condition, condition_msg) in conditions:
   if condition:
   print(condition_msg)
   break
   else:
   return identifier

When I attempt to use this function with:

goal_year_params = {
   'greeting_msg': "Please enter the date by which you wish to attain"
   " your goal.\n",
   'identifier': 'goal_year',
   'input_prompt': "Enter year of your goal as an integer:  ",
   'date_value_err_ck': (goal_year, 1, 1),
   'err_msg': "That is not a valid year.  Please try again.",
   'conditions': [
   ('goal_year < date.today().year',
   "Have you invented a time machine?  If not, please enter a"
   " year that makes more sense!"),
   ('goal_year >= date.today().year + 100',
   "Have you discovered the secret to eternal life?  And how"
   " long is this book anyway?  Please enter a year that"
   " makes more sense.")]}
goal_year = get_input(**goal_year_params)

I get the following traceback:

Traceback (most recent call last):
 File "pages_per_day.py", line 250, in 
   start_pgm()
 File "pages_per_day.py", line 246, in start_pgm
   goal_date_obj, pages_read, total_pages_to_read = get_inputs()
 File "pages_per_day.py", line 63, in get_inputs
   'date_value_err_ck': (goal_year, 1, 1),
UnboundLocalError: local variable 'goal_year' referenced before assignment

I understand this result, but cannot come up with a way to implement
my desired DRY strategy as I am stuck on how to get around this "local
variable ... referenced before assignment" issue.  On subsequent
passes "goal_year" will become "goal_month" and then "goal_day" as the
user needs to input all three of these numbers.  Is there a way to
accomplish my goal or am I attempting to be too clever?


You're not attemnpting to be too clever, but you are making some basic 
errors. For example, you can't just run a string like 'goal_year < 
date.today).year' and you shouldn't be trying i.e. do _not_ reach for 
the eval() function.


Let's look at your get_input() function. It basicly loops until you get 
a value you're happy with, and returns that value. Or it would if it 
were correct. Let's examine the main loop body:


   try:
   identifier = int(input(input_prompt))
   if date_value_err_ck:
   date(*date_value_err_ck)
   except ValueError:
   print(err_msg)
   continue
   for (condition, condition_msg) in conditions:
   if condition:
   print(condition_msg)
   break
   else:
   return identifier

To start with, you have confusion in the code bwteen the name you're 
intending to use for the input value (the "identifier" parameter) and 
the value you're reading from the user. You go:


   identifier = int(input(input_prompt))

That immediatey destroys the name you passed in as a parameter. Instead, 
use a distinct variable for the input value. Let's be imaginitive and 
call it "value":


   value = int(input(input_prompt))

and at the bottom of the get_input() function you should:

   return value

Then you try to create a date from that value (though you don't save it 
anywhere). I presume you want to use the datetime.date() constructor.  
So:


   # at the start of your programme
   import datetime

then in the function:

   date = datetime.date(*date_value_err_ck)

I think your plan is that datetime.date() will also raise a ValueError 
for a bad year number in "value". So you want, in fact, to call:


   date = datetime.date(goal_year, 1, 1)

And the construction of that will be different on your subsequent calls 
where you have goal_year and are inputting goal_month, and so on.


Your date_value_err_ck comes from your dict at the bottom of the 
programme. However, it is in the natural of things that you're computing 
the content of that dict _before_ you call get_input().


Let