diff --git a/README.md b/README.md index feef6cc..7812680 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,8 @@ to learn more about whatever you want after studying it. 18. [Modules](basics/modules.md) 19. [Exceptions](basics/exceptions.md) 20. [Classes](basics/classes.md) -21. [Docstrings](basics/docstrings.md) +21. [More Classes](basics/more-classes.md) +22. [Docstrings](basics/docstrings.md) ### Advanced diff --git a/basics/README.md b/basics/README.md index 24f8547..2dbbbcc 100644 --- a/basics/README.md +++ b/basics/README.md @@ -26,7 +26,8 @@ to learn more about whatever you want after studying it. 18. [Modules](modules.md) 19. [Exceptions](exceptions.md) 20. [Classes](classes.md) -21. [Docstrings](docstrings.md) +21. [More Classes](more-classes.md) +22. [Docstrings](docstrings.md) *** diff --git a/basics/answers.md b/basics/answers.md index de739c3..2b6f9f3 100644 --- a/basics/answers.md +++ b/basics/answers.md @@ -433,6 +433,116 @@ isn't exactly like mine but it works just fine it's ok, and you can >>> ``` + +## More Classes + +1. Delete the `global` everywhere, and add arguments and return values until + the code works. + + We also need to handle the `len(words) == 0` case differently, and returning + `None` makes sense now. + + ```python + import random + + + # returns None if the sentence contains 0 words + def change_words_order(sentence): + no_dots_sentence = sentence.replace('.', '') + words = no_dots_sentence.split() + if len(words) == 0: + return None + + lowercase_words = [] + for word in words: + lowercase_words.append(word.lower()) + random.shuffle(lowercase_words) + + # in lowercase_words[0], first character uppercase, rest lowercase + # note that empty_list_or_empty_string[0] doesn't work + # lowercase_words is not empty because len(words) == 0 was handled separately + # lowercase_words[0] is not empty because it came from .split() + lowercase_words[0] = lowercase_words[0][0].upper() + lowercase_words[0][1:] + return ' '.join(lowercase_words) + '.' + + + def main(): + print("This program changes the order of the words of a sentence randomly.") + print() + + while True: + sentence = input('Enter a sentence: ') + new_sentence = change_words_order(sentence) + if new_sentence is not None: + print(new_sentence) + + if __name__ == '__main__': + main() + ``` + +2. This answer doesn't contain any code because I haven't seen your + program, and I don't know what your program does. You can + [show your program to me](../contact-me.md) if you want to. + + If your program is short, the cleaned program that uses arguments and + return values is probably cleaner. + +3. See 2. + +4. One way is to replace all the allowed characters with empty strings (delete + them), and see what is left. Of course, add `import string` to the beginning + of the program. + + ```python + bad_characters = user_name + for good_character in (string.ascii_letters + string.digits + '-'): + bad_characters = bad_characters.replace(good_character, '') + + if bad_characters != '': + return "Username must not contain these characters: " + bad_characters + ``` + + For phone numbers, you need to decide how they have to be entered. The code + below accepts `xxx-xxx-xxxx`, where each `x` means a digit. The code ended + up being quite long, so I put it to a separate function. + + I'm assuming that all phone numbers are 10 digits long. This is probably + wrong, and in a real program you should instead figure out what's the + correct way to validate a phone number, or find someone else's code that + does it correctly and use that. + + ```python + def validate_phone_number(phone_number): + # split 'xxx-yyy-zzzz' to ['xxx', 'yyy', 'zzzz'] + parts = phone_number.split('-') + if len(parts) != 3: + return False + if len(parts[0]) != 3 or len(parts[1]) != 3 or len(parts[2]) != 4: + return False + + for part in parts: + for character in part: + if character not in string.digits: + return False + + return True + ``` + + Now you can use this function like this: + + ```python + if not validate_phone_number(self.work_phone_number): + return "work phone number must be entered as xxx-xxx-xxxx" + if not validate_phone_number(self.personal_phone_number): + return "personal phone number must be entered as xxx-xxx-xxxx" + ``` + + It's also important to tell the user how to enter the phone numbers. + For example, you can replace `input("Work phone number: ")` with + `input("Work phone number (xxx-xxx-xxxx, each x is a digit): ")` or + something like that. + + *** If you have trouble with this tutorial please [tell me about diff --git a/basics/classes.md b/basics/classes.md index 3c1f27d..ef97370 100644 --- a/basics/classes.md +++ b/basics/classes.md @@ -424,5 +424,5 @@ star](../README.md#how-can-i-thank-you-for-writing-and-sharing-this-tutorial). You may use this tutorial freely at your own risk. See [LICENSE](../LICENSE). -[Previous](exceptions.md) | [Next](docstrings.md) | +[Previous](exceptions.md) | [Next](more-classes.md) | [List of contents](../README.md#basics) diff --git a/basics/docstrings.md b/basics/docstrings.md index bbebfa3..96b3903 100644 --- a/basics/docstrings.md +++ b/basics/docstrings.md @@ -226,5 +226,5 @@ star](../README.md#how-can-i-thank-you-for-writing-and-sharing-this-tutorial). You may use this tutorial freely at your own risk. See [LICENSE](../LICENSE). -[Previous](classes.md) | [Next](../advanced/datatypes.md) | +[Previous](more-classes.md) | [Next](../advanced/datatypes.md) | [List of contents](../README.md#basics) diff --git a/basics/more-classes.md b/basics/more-classes.md new file mode 100644 index 0000000..e4a8786 --- /dev/null +++ b/basics/more-classes.md @@ -0,0 +1,518 @@ +# More Classes + +Read the [classes](classes.md) and +[writing a larger program](larger-program.md) chapters if you haven't read them +yet. In this chapter you'll learn how to use classes when writing larger +programs, and how to avoid things that cause problems when you want to write +larger programs. + + +## Global + +Python has a `global` keyword. I haven't talked about it before, and that is +for a reason. Let's look at this: + +```python +>>> stuff = 1 +>>> def thingy(): +... stuff = 2 +... +>>> thingy() +>>> stuff # this did NOT change to 2 +1 +>>> +``` + +The problem is that `stuff` is a +[local variable](defining-functions.md#locals-and-globals) inside the +`thingy` function. There are actually two variables called `stuff`; the +local `stuff` in `thingy`, and the global `stuff`: + +![Global scope: stuff = 1. Local scope inside global scope: stuff = 2.](../images/stuff-global-and-local.png) + +Of course, this is bad style because it's very confusing. To fix this, you +"can" use `global` like this (but **that's not the best way to fix it**, +see below): + +```python +def thingy(): + global stuff + stuff = 2 +``` + +Now the global `stuff` variable gets changed instead of creating a new variable. +Note that you only need `global` when **setting** a variable. This function +doesn't need `global`, and it's unnecessary to use `global` here: + +```python +def show_stuff(): + # global is not needed here, don't put it here + print('The stuff is:', stuff) +``` + +**Using global variables is bad** in general. To see why, look at this program: + +```python +# THIS CODE IS BAD + +def ask_name(): + global name + name = input("Enter your name: ") + +def create_message(): + global message + message = "Hello" + name + +def show_message(): + print(message) + +def main(): + ask_name() + create_message() + show_message() + +if __name__ == '__main__': + main() +``` + +The `__name__ == '__main__'` thing is explained +[here](larger-program.md#the-main-function). The code would be much simpler +without functions, but imagine that each function is about 20 lines long. Then +the code would be much harder to understand if it wasn't divided into multiple +functions. + +This code doesn't work correctly, and our job is to figure out which line is +wrong. If you run it and enter `Blah` when it asks for the name, it prints +`HelloBlah` instead of `Hello Blah`. Let's guess that `create_message()` is +broken, and try to run just that function without running anything else: + +```python +>>> import globalprogram # the program must be saved to globalprogram.py +>>> globalprogram + +>>> globalprogram.create_message() +Traceback (most recent call last): + File "", line 1, in + File "/home/akuli/globalprogram.py", line 9, in create_message + message = "Hello" + name +NameError: name 'name' is not defined +>>> +``` + +This didn't work at all because `ask_name()` should have created the `name` +variable, but we wanted to run `create_message()` without `ask_name()`. This is +bad, because now we need to use `ask_name()` if we want to do anything at all +with `create_message`. This may not seem very bad, but imagine what happens if +`ask_name` was instead something that took about 10 minutes to complete, or +something that you don't want to run for some other reason. Also, if +we use `ask_name` and `create_message` and it doesn't work, we don't know which +function has the problem unless we can somehow run one of them without the +other. + +Next, imagine that someone sent you this code, and you have never seen it +before. If you look at the code of `create_message`, can you figure out which +things it needs to run, and what it produces as a result? These things are +important when you are trying to understand code that you have never seen +before. You need to read these 2 lines of code in it to figure it out... + +```python +global message +message = "Hello" + name +``` + +...and notice that they use `name` and set `message`. Again, this might not +seem very bad, but imagine the same thing with 10 functions that do stuff with +20 different variables instead of 3 functions that do stuff with 2 variables. +Each function could also contain much more lines of code. + +If you still don't believe that global variables are bad, search for +`python why are global variables bad`. Some of the reasons you will find show +up mostly in large projects, so I don't have good and short examples of them +here, but I want to teach you to do things in ways that also work well when +your projects get larger. + + +## Avoiding global variables with arguments and return values + +Now you have seen why global variables are bad, so let's +[write the code with arguments and return values](larger-program.md) +instead of global variables. + +```python +# this code is not bad + +def ask_name(): + name = input("Enter your name: ") + return name + +def create_message(name): + message = "Hello" + name + return message + +def show_message(message): + print(message) + +def main(): + name = ask_name() + message = create_message(name) + show_message(message) + +if __name__ == '__main__': + main() +``` + +The code still prints `HelloBlah` instead of `Hello Blah`, but now we can +figure out where the problem is. Let's try running `create_message` again: + +```python +>>> import globalprogram # the program must be saved to globalprogram.py +>>> globalprogram + +>>> globalprogram.create_message() +Traceback (most recent call last): + File "", line 1, in +TypeError: create_message() missing 1 required positional argument: 'name' +>>> +``` + +We got an error again, but this time the error tells us exactly what's wrong: + +```python +>>> globalprogram.create_message('Blah') +'HelloBlah' +``` + +Now we know that `create_message` is broken, and to see why, we can try running +some of the code inside it: + +```python +>>> "Hello" + "Blah" +'HelloBlah' +>>> "Hello " + "Blah" +'Hello Blah' +>>> +``` + +Now we know how to fix the code. We just need to replace `"Hello"` with +`"Hello "` to get a space before the name, and it'll work better. + + +## Avoiding global variables with a class + +Arguments and return values work well for small programs, but what if your +program is bigger? For example, this program asks lots of information from the +user, checks whether it's correctly entered, and if it is, it writes it to a +file. + +```python +# THIS CODE IS BAD + +def ask_user_info(): + global user_name + global full_name + global room_number + global work_phone_number + global personal_phone_number + + user_name = input("User name: ") + full_name = input("Full name: ") + room_number = input("Room number: ") + work_phone_number = input("Work phone number: ") + personal_phone_number = input("Personal phone number (a phone number not for work): ") + + +# returns an error message, or None if everything is ok +def validate_user_info(): + if (user_name == '' or + full_name == '' or + room_number == '' or + work_phone_number == '' or + personal_phone_number == ''): + return "something was left blank" + + try: + if int(room_number) <= 0: + # room_number is '0', or negative (e.g. '-2') + return "room number must be positive" + except ValueError: + # int(room_number) failed, room_number is e.g. 'lol' + return "room number is not an integer" + + if ' ' in user_name: + return "username contains a space" + if '/' in user_name: + return "username contains a slash" + + return None + + +def create_user_info_file_name(): + global file_name + file_name = 'user_info_' + user_name + '.txt' + + +def display_user_info(): + print("User name:", user_name) + print("Full name:", full_name) + print("Room number:", room_number) + print("Work phone number:", work_phone_number) + print("Personal phone number:", personal_phone_number) + + +def write_user_info_to_file(): + print("Writing to file:", file_name) + with open(file_name, 'w') as f: + print('user_name =', user_name, file=f) + print('full_name =', full_name, file=f) + print('room_number =', room_number, file=f) + print('work_phone_number =', work_phone_number, file=f) + print('personal_phone_number =', personal_phone_number, file=f) + + +def main(): + while True: + ask_user_info() + error = validate_user_info() + if error is None: + break + + print() + print("Error:", error) + print("Please try again.") + print() + + print() + print("Here's the information you entered:") + display_user_info() + print() + + create_user_info_file_name() + write_user_info_to_file() + + +if __name__ == '__main__': + main() +``` + +The code is quite long because **classes are not usually needed for smaller programs**. +If you can return things and take arguments instead, do that. It +won't work very well in this case, because the code would end up looking like +this: + +```python +def ask_user_info(): + ... + return (user_name, full_name, room_number, work_phone_number, personal_phone_number) + +def validate_user_info(user_name, full_name, room_number, work_phone_number, personal_phone_number): + ... + +def display_user_info(user_name, full_name, room_number, work_phone_number, personal_phone_number): + ... +``` + +That's annoying. We don't want that. Let's use a class instead. + +- We had too many variables, so we'll put all of them into a class: + + ```python + class UserInfo: + + def __init__(self, user_name, full_name, room_number, work_phone_number, personal_phone_number): + self.user_name = user_name + self.full_name = full_name + self.room_number = room_number + self.work_phone_number = work_phone_number + self.personal_phone_number = personal_phone_number + ``` + +- `ask_user_info` can now return a `UserInfo` object: + + ```python + def ask_user_info(): + return UserInfo( + user_name=input("User name: "), + full_name=input("Full name: "), + room_number=input("Room number: "), + work_phone_number=input("Work phone number: "), + personal_phone_number=input("Personal phone number (a phone number not for work): "), + ) + ``` + +- `validate_user_info` can be a method of the `UserInfo` class. This way it's + also obvious that it does something with user info, so we can call it + `validate` instead of `validate_user_info`. + + ```python + class UserInfo: + ... + def validate(self) + if (self.user_name == '' or + self.full_name == '' or + self.room_number == '' or + ... + ``` + +- `create_user_info_file_name` can return the file name. + + There's no reason why + you couldn't also use arguments and return values when you use a class. Use + whatever is easiest. In this case, setting the file name to + `self.file_name` or something would mean that the method has to be called + before `self.file_name` is used anywhere. That would give the same problem + that we had earlier with global variables not being defined. This doesn't + happen with other `self.something` attributes because they are defined in + `__init__`, so they are never missing. + + I'll also rename the method to `get_file_name` instead of e.g. + `create_file_name` to give the reader of the code a hint that it might + return the file name. + + ```python + class UserInfo: + ... + def get_file_name(self): + return 'user_info_' + self.user_name + '.txt' + ... + ``` + +- `display_user_info` and `write_user_info_to_file` can be converted to methods + just like `validate_user_info`. I'll call them `display` and `write_to_file`. + My `write_to_file` method uses `self.get_file_name()` instead of `file_name`, + but it could also take `file_name` as an argument. Both ways are good. + + ```python + class UserInfo: + ... + def write_to_file(self): + file_name = self.get_file_name() + print("Writing to file:", file_name) + with open(file_name, 'w') as f: + ... + ``` + +- `main` needs to be modified more: + + ```python + def main(): + while True: + user_info = ask_user_info() + error = user_info.validate() + if error is None: + break + + print() + print("Error:", error) + print("Please try again.") + print() + + print() + print("Here's the information you entered:") + user_info.display() + print() + + user_info.write_to_file() + ``` + +Now we have a program that doesn't use global variables. If it doesn't work, +you can import it and try out all the things as usual, and figure out what's +wrong: + +```python +>>> import userinfo # save the program to userinfo.py +>>> info = userinfo.UserInfo('a', 'b', 'lol', '123-456-7890', '123-456-7890') +>>> info + +>>> info.display() +User name: a +Full name: b +Room number: lol +Work phone number: 123-456-7890 +Personal phone number: 123-456-7890 +>>> info.validate() +'room number is not an integer' +>>> +``` + + +## Exercises + +**TODO:** a longer cleanup exercise where using a class is good + +1. Clean up this code. + +```python +import random + + +def change_words_order(): + global sentence + no_dots_sentence = sentence.replace('.', '') + global words + words = no_dots_sentence.split() + global it_was_empty + if len(words) == 0: + it_was_empty = True + else: + it_was_empty = False + global lowercase_words + lowercase_words = [] + for word in words: + lowercase_words.append(word.lower()) + random.shuffle(lowercase_words) + + # in lowercase_words[0], first character uppercase, rest lowercase + # note that empty_list_or_empty_string[0] doesn't work + # lowercase_words is not empty because len(words) == 0 was handled separately + # lowercase_words[0] is not empty because it came from .split() + lowercase_words[0] = lowercase_words[0][0].upper() + lowercase_words[0][1:] + sentence = ' '.join(lowercase_words) + '.' + + +def main(): + global sentence + print("This program changes the order of the words of a sentence randomly.") + print() + + while True: + sentence = input('Enter a sentence: ') + change_words_order() + if not it_was_empty: + print(sentence) + +if __name__ == '__main__': + main() +``` + +2. Create a program that does something. Use global variables first. Clean up + the program in two ways, with a class and with arguments and return values. + Which cleaned up program is cleaner? + +3. Create a program that does something. Don't use global variables at all. + +4. My username validation code is not very good. For example, it says that + `my @$$` is a valid user name. Write better username validating code + that doesn't allow usernames to contain anything else than digits + `0`, `1`, `2`, ..., `9`, uppercase and lowercase letters `a` to `z`, and + dashes `-`. For example, `my-UserName-123` is valid, but `my @$$` is not. + (Hint: You don't need to write all the letters `ABCDEF...XYZabc...xyz` or + the digits `0123...9` to your code if you `ascii_letters` and `digits` from + [the string module](https://docs.python.org/3/library/string.html). This + is good because you can be sure that you didn't forget to write some letter + or digit, for example, and everyone reading your code can also be sure about + it.) If that's too easy, validate the phone numbers. + +The answers are [here](answers.md#more-classes). + +*** + +If you have trouble with this tutorial please [tell me about +it](../contact-me.md) and I'll make this tutorial better. If you +like this tutorial, please [give it a +star](../README.md#how-can-i-thank-you-for-writing-and-sharing-this-tutorial). + +You may use this tutorial freely at your own risk. See +[LICENSE](../LICENSE). + +[Previous](classes.md) | [Next](docstrings.md) | +[List of contents](../README.md#basics) diff --git a/images/drawings.odg b/images/drawings.odg index e890e1c..0a275a2 100644 Binary files a/images/drawings.odg and b/images/drawings.odg differ diff --git a/images/stuff-global-and-local.png b/images/stuff-global-and-local.png new file mode 100644 index 0000000..1623d6b Binary files /dev/null and b/images/stuff-global-and-local.png differ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy