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

On 23Mar2019 22:15, boB Stepp <robertvst...@gmail.com> 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 <module>
   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 me introduce you to the lambda.

You _don't_ want to compute (goal_year,1,1) beofre the call to get_input (and in fact doing so is the immediate cause of your error message). What you actaully want is to compute that value _during_ the get_input function, _after_ you've read a value from the user.

So you should not be passing a tuple (goal_year,1,1). You should be passing a function which computes that tuple from the input value.

Let me suggest this dict entry for the bottom dict:

   'date_constructor': lambda goal_year: datetime.date(goal_year, 1, 1),

instead of your 'date_value_err_ck' entry. The lambda is shorthand for a single function definition: it gets you a function which accepts a single parameter "goal_year" and returns a datetime.date result. Note that here, at the bottom of the programme, it DOES NOT CALL the function. It just makes the function. The longer way to do this might look like this:

   def date_from_year(goal_year):
     return datetime.date(goal_year, 1, 1)
   goal_year_params = {
     ....
     'date_constructor': date_from_year,
     ....
   }

The lambda is just a single line function definition, and doesn't get a function name. The above could also be written:

   date_from_year = lambda goal_year: datetime.date(goal_year, 1, 1)
   goal_year_params = {
     ....
     'date_constructor': date_from_year,
     ....
   }

much like the one liner I started with.

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:

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:

   'conditions': [
       (lambda goal_year: goal_year < date.today().year,
           "Have you invented a time machine?  If not, please enter a"
           " year that makes more sense!"),
       (lambda goal_year: 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.")]}

so that now the conditions are functions, not strings. (And you want to call datetime.date, not date, because we're using a variable "date" locally anyway.) Then the get_input function test becomes:

   for condition, condition_msg in conditions:
       if condition(value):
           print(condition_msg)
           break

i.e. call each function with the input value.

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?

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 means you get to pass in a lambda which still just takes one parameter (the input value, which it will use for the month) and still have access to the goal_year from the place the lambda was defined.

A final note regarding "what is a local variable"? A local variable is one which is assigned to in the function code. Looks:

   x = 1
   y = 2
   z = 3
   def func(x):
     y = 5
     return x + y + z

In "func", "y" is local because of the "y = 5" line. "x" is also local because it is a function parameter (which amounts to assigning a value to "x" when the function is called. However, "z" comes from the scrope where the function was defined.

Cheers,
Cameron Simpson <c...@cskk.id.au>
_______________________________________________
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor

Reply via email to