0
\$\begingroup\$

I am having some difficulty with commenting my code for a Python password cracking application I have created. I have commented on the majority of the code. However, I was wondering if anyone could suggest anymore comments I need as my instructor is really stick on me commenting my code.

import time 
import string

#Variables
max_attempts = 999999999 #the number of attempts the system makes for a brute-force attack
start       = time.time() #monitors the time length
chars       = list(string.printable) #all the possible ascii characters which the system uses to determine the password
base        = len(chars) #the base for conversion
n           = 0 #number of attempts
solved      = False
password    = input("Enter your password:") #the feature which allows the user to enter in their chosen password

print(chars) #record of previous password attempts

# converts number N base 10 to a list of digits base b
def numberToBase(n, b):
    digits = []
    while n: # takes n and the finds the modulus, the remainder of n % b
        digits.append(int(n % b)) # then it will append that to a list
        n //= b # following on it will then divide n into b
    return digits[::-1] #returns those digits into reverse order from -1, the end to the beginning


# checks if the user has inputted empty text/numbers/symbols
if password == '':

    print('Your password is empty')
    solved = True

elif password == chars[n]:
    print('Your password is ' + chars[n])
    solved = True

else:
    n = 1

#Begins systematically checking the password
if not solved:
    while n < max_attempts:
        list = numberToBase(n, base)
        print(list)
        word = ''
        for c in list: #loop through each of the characters in the list
            word += str(chars[c]) #adds the characters from the list to a word
        print(word)
        if password == word: #checks if the password equals to the word the system has generated
            solved = True # if the generation is correct the password will then commence to print statements
            print('***Results***')
            print('Password: ' + word)
            print('Attempts: ' + str(n))
            print('Time: ' + str((time.time() - start)) + ' sec') 
            break #stops the loop
        else:
            n += 1 #however if the password generation isn't correct, we increment with n

# the password is beyond max_attempts
if not solved:
    print('Unsolved after ' + str(n) + ' attempts!')
\$\endgroup\$
2
  • 6
    \$\begingroup\$ Code should speak for itself. If a comment is necessary, and it describes what some code is doing (rather than why it's doing it), then the code needs to be simplified. \$\endgroup\$
    – Joe C
    Jan 31, 2018 at 22:11
  • \$\begingroup\$ Related question \$\endgroup\$ Jan 31, 2018 at 23:01

2 Answers 2

5
\$\begingroup\$

1. The purpose of comments

Comments are a tool that you use in order to make code easier to understand and easier to maintain. But they are not the best tool to achieve that aim. Comments increase the amount of text that has to be read (you have to read the comment as well as the code) and there is a risk that when code is changed, a programmer will forget to change the nearby comments, leading to nonsense like the comments in this question.

So comments are a last resort when you can't make the code clear enough by other means. Before writing a comment you should ask these questions:

  1. Do functions have documentation explaining their arguments and results? (In Python, do they have docstrings?)

  2. Are there test cases (or documentation examples) demonstrating the correct behaviour of the code?

  3. Do function names clearly describe what they do? And do variable names clearly indicate the meaning of their values?

  4. Is the code written in the simplest, clearest way?

Only when the answers are all "yes", and the code is still not clear, should you consider writing a comment.

2. Worked example

Let's take the numberToBase function and ask these questions:

# converts number N base 10 to a list of digits base b
def numberToBase(n, b):
    digits = []
    while n: # takes n and the finds the modulus, the remainder of n % b
        digits.append(int(n % b)) # then it will append that to a list
        n //= b # following on it will then divide n into b
    return digits[::-1] #returns those digits into reverse order from -1, the end to the beginning
  1. There's no documentation. Let's turn the first comment into a docstring:

    def numberToBase(n, b):
        """Convert the number n to base b and return a list of digits."""
        digits = []
        while n:
            digits.append(int(n % b))
            n //= b
        return digits[::-1]
    
  2. There are no examples. Let's add some:

    def numberToBase(n, b):
        """Convert the number n to base b and return a list of digits.
    
            >>> numberToBase(1234, 10)
            [1, 2, 3, 4]
            >>> numberToBase(1023, 2)
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    
        """
        digits = []
        while n:
            digits.append(int(n % b))
            n //= b
        return digits[::-1]
    

    These are in the form of doctests so that they can be automatically checked using the doctest module.

  3. There are bunch of improvements we can make to the names. (i) Something like number_to_digits would be clearer than numberToBase since the result is a list of digits. (ii) base would be clearer than b. (iii) The built-in function divmod would make it clear that we are computing the division and modulus. (iv) The name digit for the modulus would make its meaning clear. (v) Using the built-in functions list and reversed would make it clear that we are computing a list in reverse order.

    def number_to_digits(n, base):
        """Convert the number n to a list of digits in the given base.
    
            >>> number_to_digits(1234, 10)
            [1, 2, 3, 4]
            >>> number_to_digits(1023, 2)
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    
        """
        digits = []
        while n:
            n, digit = divmod(n, base)
            digits.append(digit)
        return list(reversed(digits))
    
  4. The code seems clear and simple now: there is no need to add any comments.

3. Other review points

  1. When n is 0, the result is an empty list. It might be better to return the list [0].

  2. When b is 1, the function goes into an infinite loop. It might be better to raise an exception in this case.

\$\endgroup\$
4
\$\begingroup\$

About commenting and readability

Some instructors like to insist on commenting every line of code. Hopefully your instructor is not one of those people, since commenting every line of code is a harmful practice that doubles the amount of code and makes it harder to read and maintain.

The most important kind of comment is the docstring, and you didn't write any. There should be one docstring for the program, and one for every function. Furthermore, you should strive to package as much of your code in functions as possible. That forces you to structure your code into small chunks, each with a specific documented purpose, and with obvious inputs and outputs.

You've defined a numberToBase() function, which is good. But it is buried in a sea of free-floating code, which is bad.

You started off by defining 7 variables, which could potentially be used and altered anywhere in your program. That makes your code hard to understand. n, in particular, would be better named attempts, both to eliminate the need for a comment and to avoid confusion with the n in numberToBase(n, b).

Benchmarking

What exactly are you measuring? If the user takes a long time to respond to the prompt, do you really want that to be included in the elapsed time?

Furthermore, a huge amount of the password-cracking time is spent in printing the words being tried. Do you care more about the cracking speed, or the time it takes for your terminal to render text?

Here's one kind of comment that would actually useful to future maintainers of your code:

# Disabled printing, because the cracking would go much slower!
# print(word)

Suggested solution

This solution uses illustrates how a readable, well documented program should be written.

"""
Brute-force password-cracking demonstration.
"""

import string
from time import time

def nth_possible_word(n, chars=string.printable):
    """
    Generate the nth possible string composed of the given characters,
    in order of increasing length.  (By default, use the printable ASCII
    characters.)

    >>> nth_possible_word(0, chars='abc')
    ''
    >>> nth_possible_word(1, chars='abc')
    'a'
    >>> [nth_possible_word(n, chars='ab') for n in range(10)]
    ['', 'a', 'b', 'aa', 'ab', 'ba', 'bb', 'aaa', 'aab', 'aba']
    """
    word = ''
    while n:
        n -= 1
        word += chars[n % len(chars)]
        n //= len(chars)
    return word[::-1]

def crack(password, max_attempts=10**9):
    """
    Try all possible strings of increasing length until one of them matches
    the given password, or max_attempts strings have been tried.  Return a
    tuple (word, n), where word is the password that was found (or None if
    the password was not found), and n is the number of attempts made.

    >>> crack('a')
    ('a', 12)
    >>> crack('xyz')
    ('xyz', 343537)
    >>> crack('xyz', 343537)
    ('xyz', 343537)
    >>> crack('xyz', 343536)
    (None, 343536)
    """
    for attempt in range(max_attempts):
        word = nth_possible_word(attempt)
        # Disabled printing, because the cracking would go much slower!
        # print(word)
        if password == word:
            return word, attempt + 1
    return None, attempt + 1


def main():
    password = input("Enter your password: ")
    # print(string.printable)

    start_time = time()
    word, attempts = crack(password)
    elapsed_secs = time() - start_time

    if word is None:
        print('Unsolved after {0} attempts!'.format(attempts))
    elif word == '':
        print('Your password is empty')
    else:
        print("""***Results***
Password: {0}
Attempts: {1}
Time: {2} sec""".format(word, attempts, elapsed_secs))


if __name__ == '__main__':
    main()

Observations:

  • The main() function is relatively short. You can get a feeling of what this program does by looking at it, without getting lost in the details. In particular, all of the input/output happens in main(); all of the "calculations" happen within other functions.
  • The code within the nth_possible_word() and crack() functions might be difficult for a beginner to understand, but the docstrings clarify how the functions behave. You only look at the implementation when you need to.
  • Empty input is treated as a special case only for reporting (to replicate your output). For the purposes of cracking, a zero-length password isn't all that special.

    By the way, your elif password == chars[n]: special case was pretty pointless. It would only get activated if the password is '0'.

Alternate solution

In a sense, your numberToBase() technique is a way to iterate through all of the possible passwords to try. By using some Python features such as itertools and yield), you can iterate through all possible strings more expressively. If you replace the nth_possible_word() and crack() functions in the program above with the two functions below, the program will behave the same.

from itertools import count, islice, product

def passwords_to_try(chars=string.printable):
    """
    Generate an infinite sequence of all possible strings composed of the
    given characters, in order of increasing length.  (By default, the
    strings consist of printable ASCII characters.)

    >>> p = passwords_to_try()
    >>> next(p)
    ''
    >>> next(p)
    '0'
    >>> next(p)
    '1'
    """
    for length in count():
        for characters in product(chars, repeat=length):
            yield ''.join(characters)


def crack(password, max_attempts=10**9):
    """
    Try all possible strings of increasing length until one of them matches
    the given password, or max_attempts strings have been tried.  Return a
    tuple (word, n), where word is the password that was found (or None if
    the password was not found), and n is the number of attempts made.

    >>> crack('a')
    ('a', 12)
    >>> crack('xyz')
    ('xyz', 343537)
    >>> crack('xyz', 343537)
    ('xyz', 343537)
    >>> crack('xyz', 343536)
    (None, 343536)
    """
    for n, word in enumerate(islice(passwords_to_try(), max_attempts), 1):
        # Disabled printing, because the cracking would go much slower!
        # print(word)
        if password == word:
            return word, n
    return None, n
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.