Re: [Tutor] How to avoid "UnboundLocalError: local variable 'goal_year' referenced before assignment"?
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"?
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"?
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"?
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