-1

Sorry about the length of this but I figured more info is better than not enough!!

I'm trying to split the (working) piece of Python code into functions to make it clearer / easier to use but am coming unstuck as soon as i move stuff into functions. It's basically a password generator which tries to only output a password to the user once the password qualifies as having a character from all 4 categories in it. (Lowercase, uppercase, numbers and symbols).


import random
import string
lowerasciis = string.ascii_letters[0:26]
upperasciis = string.ascii_letters[26:]
numberedstrings = str(1234567809)
symbols = "!@$%^&*()[]"
password_length = int(raw_input("Please enter a password length: "))

while True:
    lowerasscii_score = 0
    upperascii_score = 0
    numberedstring_score = 0
    symbol_score = 0
    password_as_list = []

    while len(password_as_list) < password_length:
        char = random.choice(lowerasciis+upperasciis+numberedstrings+symbols)
        password_as_list.append(char)
    for x in password_as_list:
        if x in lowerasciis:
            lowerasscii_score +=1
        elif x in upperasciis:
            upperascii_score +=1
        elif x in numberedstrings:
            numberedstring_score +=1
        elif x in symbols:
            symbol_score +=1
# a check for the screen. Each cycle of the loop should display a new score:
    print lowerasscii_score, upperascii_score, numberedstring_score, symbol_score 

    if lowerasscii_score >= 1 and upperascii_score >= 1 and numberedstring_score >= 1 and symbol_score >=1:
        password = "".join(password_as_list)
        print password
        break

And here is my attempt at splitting it. When i try to run the below it complains of "UnboundLocalError: local variable 'upperascii_score' referenced before assignment" in the scorepassword_as_a_list() function

import random
import string
lowerasciis = string.ascii_letters[0:26]
upperasciis = string.ascii_letters[26:]
numberedstrings = str(1234567809)
symbols = "!@$%^&*()[]"
password_length = int(raw_input("Please enter a password length: "))
lowerasscii_score = 0
upperascii_score = 0
numberedstring_score = 0
symbol_score = 0
password_as_list = []

def genpassword_as_a_list():
    while len(password_as_list) < password_length:
        char = random.choice(lowerasciis+upperasciis+numberedstrings+symbols)
        password_as_list.append(char)

def scorepassword_as_a_list():
    for x in password_as_list:
        if x in lowerasciis:
            lowerasscii_score +=1
        elif x in upperasciis:
            upperascii_score +=1
        elif x in numberedstrings:
            numberedstring_score +=1
        elif x in symbols:
            symbol_score +=1
    # give user feedback about password's score in 4 categories
    print lowerasscii_score, upperascii_score, numberedstring_score, symbol_score

def checkscore():
    if lowerasscii_score >= 1 and upperascii_score >= 1 and numberedstring_score >= 1 and symbol_score >=1:
        return 1
    else:
        return 0

def join_and_printpassword():
    password = "".join(password_as_list)
    print password  

while True:
    genpassword_as_a_list()
    scorepassword_as_a_list()
    if checkscore() == 1:
        join_and_printpassword()
        break
slowreader
  • 11
  • 1
  • 1
  • 4
    If you want feedback on working code or if you're looking for optimizations, you should rather post your question on http://codereview.stackexchange.com/ – Mike Scotty Jun 05 '17 at 14:22
  • You have to give arguments to your functions​. You should start with some Python tutorials – Nuageux Jun 05 '17 at 14:27
  • 3
    I don't think it belongs to codereview, it's just the good old "I've heard I have to split into functions and it doesn't work anymore because I don't know what scope is" problem. – polku Jun 05 '17 at 14:48
  • 1
    If you're going to use global variables, read about _how_ to use them first. Or, if that fails, learn to google the error messages you receive. I can guarantee we have at least a couple dozen duplicates of this question here on SO. – Aran-Fey Jun 05 '17 at 14:51
  • @Peilonrayz the second sentence reads "I'm trying to split the (**working**) piece of Python code into functions". It only stopped working after OP tried to change it. – Mike Scotty Jun 05 '17 at 14:53

1 Answers1

1

The primary issue here is that you need to keep track of the scope of the various variables that you're using. In general, one of the advantages of splitting your code into functions (if done properly) is that you can reuse code without worrying about whether any initial states have been modified somewhere else. To be concrete, in your particular example, even if you got things working right (using global variables), every time you called one of your functions, you'd have to worry that e.g. lowerassci_score was not getting reset to 0.

Instead, you should accept anything that your function needs to run as parameters and output some return value, without manipulating global variables. In general, this idea is known as "avoiding side-effects." Here is your example re-written with this in mind:

import random
import string
lowerasciis = string.ascii_letters[0:26]
upperasciis = string.ascii_letters[26:]
numberedstrings = str(1234567809)
symbols = "!@$%^&*()[]"

def genpassword_as_a_list(password_length):
    password_as_list = []
    while len(password_as_list) < password_length:
        char = random.choice(lowerasciis+upperasciis+numberedstrings+symbols)
        password_as_list.append(char)
    return password_as_list

def scorepassword_as_a_list(password_as_list):
    lowerasscii_score = 0
    upperascii_score = 0
    numberedstring_score = 0
    symbol_score = 0
    for x in password_as_list:
        if x in lowerasciis:
            lowerasscii_score +=1
        elif x in upperasciis:
            upperascii_score +=1
        elif x in numberedstrings:
            numberedstring_score +=1
        elif x in symbols:
            symbol_score +=1
    # give user feedback about password's score in 4 categories
    return (
        lowerasscii_score, upperascii_score, numberedstring_score,
        symbol_score
    )

def checkscore(
        lowerasscii_score, upperascii_score, numberedstring_score,
        symbol_score):
    if lowerasscii_score >= 1 and upperascii_score >= 1 and numberedstring_score >= 1 and symbol_score >=1:
        return 1
    else:
        return 0

def join_and_printpassword(password_as_list):
    password = "".join(password_as_list)
    print password  

password_length = int(raw_input("Please enter a password length: "))

while True:
    password_list = genpassword_as_a_list(password_length)
    current_score = scorepassword_as_a_list(password_list)
    if checkscore(*current_score) == 1:
        join_and_printpassword(password_list)
        break

A few notes on this:

  1. Notice that the "score" variables are introduced inside the scorepassword_as_list function and (based on the scoping rules) are local to that function. We get them out of the function by passing them out as a return value.
  2. I've used just a bit of magic near the end with *current_score. Here, the asterisk is used as the "splat" or "unpack" operator. I could just as easily have written checkscore(current_score[0], current_score[1], current_score[2], current_score[3]); they mean the same thing.

It would probably be useful to read up a bit more on variable scoping and namespaces in Python. Here's one guide, but there may be better ones out there.

wphicks
  • 355
  • 2
  • 9
  • That's very kind! I shall see if this runs (I am sure it will) and then try to digest it. And the guide... I am actually just working my way through the practicepython.org site exercises and we haven't really covered this level of scoping / namespaces yet. I always thought moving code into functions was pretty straightforward but guess i need to get my head around return and flow a little more. Thanks again – slowreader Jun 05 '17 at 15:42
  • Oh, hey - any idea why it throws a hissy fit if i include the £ symbol (even in a comment?!) – slowreader Jun 05 '17 at 15:43
  • In general, it's a good idea to post questions that are unrelated to the original one as a new question rather than a comment (keeps everything clean and easy to search), but this is related to how Python handles UTF8 encoding. Post a separate question if you'd like more details. – wphicks Jun 05 '17 at 15:49
  • OK. Thanks. Still a bit of an outsider here! – slowreader Jun 05 '17 at 15:51
  • OK. I have one more question. And it is based on the original one... Any pointers on how I might set this up so that if the password generated doesn't satisfy the "one character from each category" rule that only that missing category of character is regenerated? I would have to keep track of which category was not satisfied and then 'sacrifice' a character from a category which has more than 1 already satisfied. It was all too much for my brain! Let me know if that's actually a separate question after all! And thanks again – slowreader Jun 05 '17 at 16:09
  • Why don't you try writing a function to do that and then post your attempt as a new question if you get stuck on something specific? Hint: You probably want to accept the scores and previous password attempt as input parameters. Good luck! – wphicks Jun 05 '17 at 16:51