WEBVTT X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000 00:00:00.000 --> 00:00:03.479 [CLASSICAL MUSIC] 00:00:23.880 --> 00:00:26.580 DAVID J. MALAN: All right, this is CS50's Introduction 00:00:26.580 --> 00:00:27.900 to Programming with Python. 00:00:27.900 --> 00:00:31.530 My name is David Malan, and this is our week on object-oriented programming, 00:00:31.530 --> 00:00:32.790 or OOP. 00:00:32.790 --> 00:00:34.770 It turns out that, in the world of programming, 00:00:34.770 --> 00:00:37.570 there's different paradigms of programming languages. 00:00:37.570 --> 00:00:40.208 There's different ways of solving problems with code, 00:00:40.208 --> 00:00:42.000 and it's a little hard to see this at first 00:00:42.000 --> 00:00:43.920 if you've only learned one language. 00:00:43.920 --> 00:00:47.403 But over time, if and when you learn other languages besides Python, 00:00:47.403 --> 00:00:50.070 you'll start to notice certain patterns and certain capabilities 00:00:50.070 --> 00:00:52.020 of some languages but not another. 00:00:52.020 --> 00:00:54.780 Thus far, within the world of Python, you and I 00:00:54.780 --> 00:00:58.560 have largely been writing code that's procedural in nature, whereby we're 00:00:58.560 --> 00:01:00.720 writing procedures; we're writing functions; 00:01:00.720 --> 00:01:03.150 and we're doing things top to bottom. 00:01:03.150 --> 00:01:05.459 Everything is step by step by step, as you would 00:01:05.459 --> 00:01:07.200 expect in general from an algorithm. 00:01:07.200 --> 00:01:09.270 But along the way, we've actually dabbled 00:01:09.270 --> 00:01:14.070 in another paradigm known as functional programming with Python whereby we've 00:01:14.070 --> 00:01:15.870 been able to pass functions around. 00:01:15.870 --> 00:01:18.670 We even had an anonymous function some weeks ago. 00:01:18.670 --> 00:01:22.482 And that's evidence of features of a functional programming language, 00:01:22.482 --> 00:01:24.690 even though we've just scratched the surface thereof. 00:01:24.690 --> 00:01:28.080 Today we focus on another paradigm, and this one in more detail-- 00:01:28.080 --> 00:01:30.150 namely object-oriented programming. 00:01:30.150 --> 00:01:32.970 And now, while some of you might have prior programming experience 00:01:32.970 --> 00:01:36.510 and have learned languages like Java, which are, by design, fundamentally 00:01:36.510 --> 00:01:40.290 object-oriented, Python indeed allows you a bit of flexibility 00:01:40.290 --> 00:01:43.230 when it comes to how you solve problems with code. 00:01:43.230 --> 00:01:46.470 But it turns out object-oriented programming 00:01:46.470 --> 00:01:49.020 is a pretty compelling solution to problems 00:01:49.020 --> 00:01:52.650 that you invariably encounter as your programs get longer, larger, 00:01:52.650 --> 00:01:54.370 and more complicated. 00:01:54.370 --> 00:01:56.340 So indeed, OOP, for our purposes, is going 00:01:56.340 --> 00:02:00.810 to be a solution to a problem that builds on so many of the lessons past. 00:02:00.810 --> 00:02:02.470 So let's go ahead and do this. 00:02:02.470 --> 00:02:05.850 Let's start by writing a program very procedurally 00:02:05.850 --> 00:02:07.620 by opening up VS Code here. 00:02:07.620 --> 00:02:10.889 I'm going to go ahead and create a program called student.py. 00:02:10.889 --> 00:02:14.100 And in this program, I want to do something relatively simple 00:02:14.100 --> 00:02:18.360 initially, as we might have done some weeks ago now, where I just 00:02:18.360 --> 00:02:22.170 ask a user for their name and, maybe in the context of the Harry Potter 00:02:22.170 --> 00:02:26.310 universe, their house, and just print out where that student is from. 00:02:26.310 --> 00:02:30.150 And let's gradually enhance this program by adding more and more features to it 00:02:30.150 --> 00:02:33.870 and see if we don't stumble upon problems that, up until now, 00:02:33.870 --> 00:02:36.960 we might not have had very elegant, well-designed solutions to. 00:02:36.960 --> 00:02:40.230 But if we introduce, explicitly, object-oriented programming 00:02:40.230 --> 00:02:43.230 as a programming technique, I bet we can clean up our code 00:02:43.230 --> 00:02:47.220 and set the stage for writing even more sophisticated programs, longer programs 00:02:47.220 --> 00:02:48.100 down the line. 00:02:48.100 --> 00:02:51.780 So in student.py, let me go ahead and do a name variable, 00:02:51.780 --> 00:02:54.270 setting it equal to the return value of input, 00:02:54.270 --> 00:02:57.060 and just prompt the user for their name like this. 00:02:57.060 --> 00:02:59.790 And then let me go ahead and do the same for a house variable 00:02:59.790 --> 00:03:03.060 and prompt the user for their house, using input like this. 00:03:03.060 --> 00:03:05.220 And let's do something super simple now. 00:03:05.220 --> 00:03:07.860 Let's just go ahead and print out an f string 00:03:07.860 --> 00:03:11.310 that says something like name from house, 00:03:11.310 --> 00:03:13.950 just so that I can confirm that the contents of these variables 00:03:13.950 --> 00:03:14.987 are indeed as I expect. 00:03:14.987 --> 00:03:17.070 I'm not going to do any error checking or trimming 00:03:17.070 --> 00:03:18.320 or anything like that for now. 00:03:18.320 --> 00:03:21.420 I'm really just going to spit back out whatever the user just typed in. 00:03:21.420 --> 00:03:24.390 All right, let me go ahead and run Python of student.py. 00:03:24.390 --> 00:03:29.100 Let's use our go-to, like Harry, as in Harry Potter, from Gryffindor. 00:03:29.100 --> 00:03:32.970 And when I hit Enter, now let's see if I see that Harry from Gryffindor 00:03:32.970 --> 00:03:34.050 is indeed the case. 00:03:34.050 --> 00:03:37.380 All right, so I think we have a working program at this point, but let's 00:03:37.380 --> 00:03:40.560 now introduce some of those lessons learned way back from week zero 00:03:40.560 --> 00:03:42.570 where we started writing our own functions, 00:03:42.570 --> 00:03:46.260 not necessarily because it solves the problem more correctly-- 00:03:46.260 --> 00:03:47.910 I daresay this is correct as is. 00:03:47.910 --> 00:03:52.050 But it begins to give us building blocks that we can extend so 00:03:52.050 --> 00:03:54.130 as to solve more complicated programs. 00:03:54.130 --> 00:03:58.360 So let me go back up to student.py, and let's go ahead now and do this. 00:03:58.360 --> 00:04:01.890 Let's put the entire logic I just wrote inside of our typical method 00:04:01.890 --> 00:04:05.460 called main, and let me indent those three lines so that at least they're 00:04:05.460 --> 00:04:07.350 now combined into one main method. 00:04:07.350 --> 00:04:12.225 But instead of using input on line 2 an input on line 3, 00:04:12.225 --> 00:04:14.100 don't we go ahead and assume, for the moment, 00:04:14.100 --> 00:04:16.992 that we've got some function called get_name in the world, 00:04:16.992 --> 00:04:19.950 and let's go ahead and assume we've got another function like get_house 00:04:19.950 --> 00:04:21.720 in the world that don't take parameters. 00:04:21.720 --> 00:04:24.180 But their purpose in life is, by their name, 00:04:24.180 --> 00:04:28.450 going to be to get the user's name and to get their users house, respectively. 00:04:28.450 --> 00:04:32.070 And then I'm going to print out the exact same f string as before. 00:04:32.070 --> 00:04:34.320 I, of course, need to implement these functions now. 00:04:34.320 --> 00:04:38.310 So let me go lower in my file and define a function called get_name. 00:04:38.310 --> 00:04:41.370 Nothing in these parentheses because it's not going to take a parameter. 00:04:41.370 --> 00:04:44.078 And I'm going to go ahead and do something like name equals input 00:04:44.078 --> 00:04:48.750 ("Name"), just like before, and then I'm going to go ahead and return name. 00:04:48.750 --> 00:04:52.020 So it's a super simple function, but it's an abstraction. 00:04:52.020 --> 00:04:55.452 I now have a function called get_name whose implementation details 00:04:55.452 --> 00:04:56.910 I don't have to care about anymore. 00:04:56.910 --> 00:04:58.470 I just know that the function exists. 00:04:58.470 --> 00:05:00.270 And I can tighten this up, in fact, . 00:05:00.270 --> 00:05:02.880 Don't really need a name variable on line 8 00:05:02.880 --> 00:05:05.880 if I'm immediately going to return that same name variable on line 9. 00:05:05.880 --> 00:05:07.822 So let me just tighten this up a little bit 00:05:07.822 --> 00:05:09.780 even though it doesn't change the functionality 00:05:09.780 --> 00:05:15.730 and just immediately return the return value of the inputs function call here. 00:05:15.730 --> 00:05:18.900 Let's do something very similar now for get_house, 00:05:18.900 --> 00:05:20.730 which will similarly take no arguments. 00:05:20.730 --> 00:05:22.650 I'm going to go ahead and return the return 00:05:22.650 --> 00:05:25.530 value of input, this time prompting the user for their house. 00:05:25.530 --> 00:05:27.270 And I need one final detail. 00:05:27.270 --> 00:05:32.220 At the very bottom, let's continue our habit of doing if the name of this file 00:05:32.220 --> 00:05:35.730 equals, equals, quote, unquote, main, then let's go ahead 00:05:35.730 --> 00:05:38.860 and actually call main and recall that we have that in place 00:05:38.860 --> 00:05:42.460 so that, if this eventually becomes part of a module, a library of sorts, 00:05:42.460 --> 00:05:44.790 I don't accidentally call main blindly. 00:05:44.790 --> 00:05:50.250 I only do it if I mean to run main from the command line on this file. 00:05:50.250 --> 00:05:53.460 All right, so if I didn't make any mistakes here, let me go ahead 00:05:53.460 --> 00:05:57.420 and, in my terminal window, again, run Python of student.py, Enter. 00:05:57.420 --> 00:05:59.250 Let's type in Harry, Enter. 00:05:59.250 --> 00:06:01.560 Let's type in Gryffindor, Enter. 00:06:01.560 --> 00:06:02.880 And we're set. 00:06:02.880 --> 00:06:05.580 Harry from Gryffindor seems to still be working. 00:06:05.580 --> 00:06:09.420 So we haven't really solved the problem any more correctly, 00:06:09.420 --> 00:06:11.882 but I've laid the foundation to maybe now do 00:06:11.882 --> 00:06:13.590 some more interesting things because I've 00:06:13.590 --> 00:06:16.890 had these building blocks in place. 00:06:16.890 --> 00:06:21.090 But let me propose that we could be doing this a little bit differently. 00:06:21.090 --> 00:06:22.990 get_name, get_house is fine. 00:06:22.990 --> 00:06:26.430 But at the end of the day, I'm really trying to get a student from the user. 00:06:26.430 --> 00:06:30.070 I want their name and their house, not just one or the other. 00:06:30.070 --> 00:06:32.820 So maybe it would be a little cleaner still 00:06:32.820 --> 00:06:36.090 to define a function called get_student and let 00:06:36.090 --> 00:06:38.310 get_student do all of this work for us. 00:06:38.310 --> 00:06:42.990 Now, theoretically, get_student could call get_name and could call get_house. 00:06:42.990 --> 00:06:44.760 But because these functions are so short, 00:06:44.760 --> 00:06:48.870 I think I'm OK with just defining one function, called get_student, 00:06:48.870 --> 00:06:50.670 that similarly won't take any arguments. 00:06:50.670 --> 00:06:52.240 But it's going to do two things. 00:06:52.240 --> 00:06:55.880 It's going to get the students name, by prompting them with input as before. 00:06:55.880 --> 00:06:57.630 And it's going to get the student's house, 00:06:57.630 --> 00:07:01.110 by also prompting them as before. 00:07:01.110 --> 00:07:02.430 Now, hmm. 00:07:02.430 --> 00:07:04.830 I want to return the student, but I think 00:07:04.830 --> 00:07:06.870 I might have painted myself into a corner 00:07:06.870 --> 00:07:10.110 here because I now have two variables-- name and house. 00:07:10.110 --> 00:07:13.260 And yet, up until now, we've pretty much returned one or the other. 00:07:13.260 --> 00:07:15.010 We've returned one value. 00:07:15.010 --> 00:07:17.490 So any suggestions for how we can perhaps 00:07:17.490 --> 00:07:22.380 solve this problem that I just created for myself, whereby I want to return, 00:07:22.380 --> 00:07:28.080 really, a student, but I currently have a name variable and a house variable. 00:07:28.080 --> 00:07:30.555 I'd minimally like to return both of those. 00:07:30.555 --> 00:07:33.210 AUDIENCE: I believe that, we can return a dictionary, 00:07:33.210 --> 00:07:35.010 includes the name and the house. 00:07:35.010 --> 00:07:36.843 DAVID J. MALAN: Yeah, so we absolutely could 00:07:36.843 --> 00:07:40.980 return a dictionary-- a dict object in Python, whereby maybe one key is name; 00:07:40.980 --> 00:07:43.650 one key is house; and the values thereof are exactly 00:07:43.650 --> 00:07:44.900 the values of these variables. 00:07:44.900 --> 00:07:46.810 So we could totally do that. 00:07:46.810 --> 00:07:49.360 I worry that that might be getting a little complicated. 00:07:49.360 --> 00:07:51.900 I wonder if there's a simpler way instead. 00:07:51.900 --> 00:07:54.985 Any other instincts-- even if you're not sure it would work? 00:07:54.985 --> 00:07:56.527 AUDIENCE: Return both name and house? 00:07:56.527 --> 00:07:58.318 DAVID J. MALAN: Return both name and house. 00:07:58.318 --> 00:07:59.430 I like the sound of that. 00:07:59.430 --> 00:08:00.248 It sounds simple. 00:08:00.248 --> 00:08:03.040 I don't have to figure out what a dictionary is going to look like. 00:08:03.040 --> 00:08:05.730 And in fact, this, too, would be a valid approach, 00:08:05.730 --> 00:08:07.230 even if you've not seen this before. 00:08:07.230 --> 00:08:11.610 It turns out, in Python, that you can return multiple values, 00:08:11.610 --> 00:08:12.960 but that's a bit of a white lie. 00:08:12.960 --> 00:08:16.320 Or we could take Muhammad's approach of actually returning a dictionary 00:08:16.320 --> 00:08:17.820 and putting multiple keys therein. 00:08:17.820 --> 00:08:19.830 So here, again, we have yet another example 00:08:19.830 --> 00:08:22.380 of how you can solve the same problem in at least two ways, 00:08:22.380 --> 00:08:24.720 and I daresay we're about to see even more. 00:08:24.720 --> 00:08:27.900 So one way you could solve this problem, whereby 00:08:27.900 --> 00:08:32.919 you want to return multiple values, would be to do something like this. 00:08:32.919 --> 00:08:37.650 I could go ahead and literally return not just name, but I could put a comma 00:08:37.650 --> 00:08:39.270 and also return house. 00:08:39.270 --> 00:08:41.970 This is not necessarily something you can do in other languages 00:08:41.970 --> 00:08:44.159 if you have programmed in other languages before. 00:08:44.159 --> 00:08:45.420 It depends on the language. 00:08:45.420 --> 00:08:49.290 But it looks like, thanks to this comma, maybe I can, in fact, 00:08:49.290 --> 00:08:51.930 return two values as [INAUDIBLE] proposed. 00:08:51.930 --> 00:08:55.380 Well, if I'm returning to values in this way on line 10, 00:08:55.380 --> 00:08:57.908 how do I get both values at the same time? 00:08:57.908 --> 00:08:59.200 Well, there's a couple of ways. 00:08:59.200 --> 00:09:01.830 Let me go up to my main function. 00:09:01.830 --> 00:09:04.410 I know, minimally, I'm going to have to change the get_name 00:09:04.410 --> 00:09:05.940 and get_house to get_student. 00:09:05.940 --> 00:09:09.030 But what am I going to store the return value in? 00:09:09.030 --> 00:09:11.520 I think I could actually do this. 00:09:11.520 --> 00:09:14.920 And we have seen this technique before, where you can unpack, 00:09:14.920 --> 00:09:17.550 so to speak, sequences of values that are coming back. 00:09:17.550 --> 00:09:21.090 And indeed, consider this to be exactly that. name, 00:09:21.090 --> 00:09:25.020 house is some kind of sequence that I'm returning of values-- name, house. 00:09:25.020 --> 00:09:27.300 So if I want to unpack those and store the return 00:09:27.300 --> 00:09:30.010 values in two separate variables, I can, in fact, 00:09:30.010 --> 00:09:34.200 use the commas on the left-hand side of my assignment operator, the equal sign, 00:09:34.200 --> 00:09:35.410 to do just that. 00:09:35.410 --> 00:09:40.020 Now, to be clear, I don't need to call these variables name and house here. 00:09:40.020 --> 00:09:43.500 I could simplify this and use just n here and h here, 00:09:43.500 --> 00:09:46.590 and then I could return just n and h. 00:09:46.590 --> 00:09:51.200 But I would argue that's not very clear to the reader as to what's going on. 00:09:51.200 --> 00:09:52.950 So I think, in this case, even though it's 00:09:52.950 --> 00:09:56.010 a coincidence that I've used the same variable names in get_student 00:09:56.010 --> 00:09:59.890 and get_name, and in main, it's a little more readable to someone like me. 00:09:59.890 --> 00:10:01.560 So I'm going to leave it as is. 00:10:01.560 --> 00:10:04.060 Well, let's go ahead and see, now, if this works. 00:10:04.060 --> 00:10:07.170 Let me clear my screen down here and run Python of student.py, Enter. 00:10:07.170 --> 00:10:08.370 Let's again type in Harry. 00:10:08.370 --> 00:10:11.040 Let's again type in Gryffindor, Enter. 00:10:11.040 --> 00:10:14.790 And voila, we still see that Harry is from Gryffindor. 00:10:14.790 --> 00:10:16.950 But what are we actually doing here? 00:10:16.950 --> 00:10:19.660 What are we actually doing by returning this value? 00:10:19.660 --> 00:10:24.320 Well, it turns out that what we've just done is use a tuple. 00:10:24.320 --> 00:10:29.810 A tuple is another type of data in Python that's a collection of values-- 00:10:29.810 --> 00:10:32.300 x, y or x, y, z. 00:10:32.300 --> 00:10:36.770 It's similar in spirit to a list, in that sense, but it's immutable. 00:10:36.770 --> 00:10:37.648 It's not mutable. 00:10:37.648 --> 00:10:38.690 Now, what does that mean? 00:10:38.690 --> 00:10:41.720 A list, as we've seen it before, is a data structure in Python 00:10:41.720 --> 00:10:43.460 that you can change the values of. 00:10:43.460 --> 00:10:47.240 You can go into bracket 0 for the first location and change the value there. 00:10:47.240 --> 00:10:50.210 You can go to bracket 1, bracket 2, bracket 3 and actually change 00:10:50.210 --> 00:10:51.450 the values in lists. 00:10:51.450 --> 00:10:54.800 But if you have no intention of changing the values of variables 00:10:54.800 --> 00:10:57.748 and you want to return, effectively, multiple values, 00:10:57.748 --> 00:10:59.540 you don't have to even return it as a list. 00:10:59.540 --> 00:11:03.410 You can return it as a tuple instead, just by using a comma. 00:11:03.410 --> 00:11:07.010 And it turns out we can make explicit that-- here's the white lie. 00:11:07.010 --> 00:11:10.490 I'm not actually returning to values per se. 00:11:10.490 --> 00:11:13.400 Whenever you use a comma in this way on line 9, 00:11:13.400 --> 00:11:17.510 you're actually returning one value, which is a tuple. 00:11:17.510 --> 00:11:20.280 Inside of that tuple now are two values. 00:11:20.280 --> 00:11:23.790 So it's similar in spirit to returning one list with two thing Here 00:11:23.790 --> 00:11:26.300 I'm returning one tuple with two things. 00:11:26.300 --> 00:11:28.970 And the mere fact that I've used a comma and nothing else 00:11:28.970 --> 00:11:31.370 tells Python that I indeed want to return a tuple. 00:11:31.370 --> 00:11:34.100 But there's more explicit syntax that we can use instead. 00:11:34.100 --> 00:11:39.290 I can actually-- more verbosely-- put explicit parentheses around the values 00:11:39.290 --> 00:11:42.440 of this tuple just to make more clear to me, to the reader 00:11:42.440 --> 00:11:44.090 that this isn't two values per se. 00:11:44.090 --> 00:11:46.520 This is one value with two things inside of it. 00:11:46.520 --> 00:11:48.440 And what I can actually do then, too, is-- 00:11:48.440 --> 00:11:51.020 I don't have to unpack this up here, so to speak. 00:11:51.020 --> 00:11:54.830 I can actually go up here and maybe give a more apt name, like student, 00:11:54.830 --> 00:11:58.370 and I can name the value, or rather name the variable 00:11:58.370 --> 00:12:01.340 in which I'm storing the return value of get_student as, 00:12:01.340 --> 00:12:02.810 quote, unquote, "student." 00:12:02.810 --> 00:12:05.180 So maybe this is a little better design now 00:12:05.180 --> 00:12:08.630 because I'm sort of abstracting away what a student is. 00:12:08.630 --> 00:12:11.660 It's implemented at the moment as a tuple with two values. 00:12:11.660 --> 00:12:15.890 But at least, now I have a variable called what I mean, a student. 00:12:15.890 --> 00:12:17.360 But there's going to be a catch. 00:12:17.360 --> 00:12:21.590 On line 3, I still want to print out that student's name and their house. 00:12:21.590 --> 00:12:24.560 But I don't have a name variable anymore, and I don't have a house. 00:12:24.560 --> 00:12:28.640 And I also don't have a dictionary, as was proposed earlier, so I can't even 00:12:28.640 --> 00:12:31.830 go at those keys by name. 00:12:31.830 --> 00:12:35.180 But what a tuple is-- it's very similar in spirit to a list, 00:12:35.180 --> 00:12:36.980 but it is indeed just immutable. 00:12:36.980 --> 00:12:39.800 And what I mean by that is I can still index into it 00:12:39.800 --> 00:12:45.050 numerically by saying student [0] for the item 00:12:45.050 --> 00:12:47.000 in the first location in that tuple. 00:12:47.000 --> 00:12:50.420 And then over here, instead of house, I can say student [1]. 00:12:50.420 --> 00:12:54.275 student [1] is going to give me the second location in that tuple. 00:12:54.275 --> 00:12:56.150 Let me go ahead and clear my terminal window. 00:12:56.150 --> 00:12:58.100 Again, run Python of student.py. 00:12:58.100 --> 00:12:59.240 Let's type in Harry. 00:12:59.240 --> 00:13:04.520 Let's type in Gryffindor, Enter, and we still have some working code. 00:13:04.520 --> 00:13:07.190 Let me pause here now and see if there are 00:13:07.190 --> 00:13:11.510 any questions on this technique of returning a tuple 00:13:11.510 --> 00:13:14.735 and indexing into it in this way. 00:13:14.735 --> 00:13:17.120 AUDIENCE: I guess, what's an actual use case where 00:13:17.120 --> 00:13:21.347 you would use a tuple versus a list or something else that's similar? 00:13:21.347 --> 00:13:23.180 DAVID J. MALAN: It's a really good question. 00:13:23.180 --> 00:13:25.280 When would you use a tuple versus a list? 00:13:25.280 --> 00:13:28.190 When you want to program defensively, or, in general, 00:13:28.190 --> 00:13:31.380 when you know that the values in this variable shouldn't change, 00:13:31.380 --> 00:13:34.320 so why would you use a data type that allows them to be changed? 00:13:34.320 --> 00:13:37.790 It just invites mistakes, bugs down the line, either 00:13:37.790 --> 00:13:40.650 by you or colleagues who are interacting with your code. 00:13:40.650 --> 00:13:44.090 So tuple is just another way where you can increase 00:13:44.090 --> 00:13:47.720 the probability of correctness by just not letting anyone, yourself included, 00:13:47.720 --> 00:13:49.740 change the contents therein. 00:13:49.740 --> 00:13:52.260 So it's just another tool in your toolkit. 00:13:52.260 --> 00:13:55.340 But let's make clear, then, what I mean by "immutable." 00:13:55.340 --> 00:13:58.850 Again, I claim that "immutable" means that you cannot change the value. 00:13:58.850 --> 00:14:01.020 Well, let's go ahead and try to do this. 00:14:01.020 --> 00:14:03.740 Let me go ahead and run this program once more as is-- 00:14:03.740 --> 00:14:05.300 Python of student.py. 00:14:05.300 --> 00:14:09.410 Let me go ahead and type in, for instance-- 00:14:09.410 --> 00:14:11.387 how about Padma's name? 00:14:11.387 --> 00:14:13.220 And I'm going to go ahead and say that Padma 00:14:13.220 --> 00:14:14.930 is in Gryffindor as in the movies. 00:14:14.930 --> 00:14:16.940 And we see-- Padma from Gryffindor. 00:14:16.940 --> 00:14:20.810 But technically, I went down this rabbit hole in looking at Harry Potter 00:14:20.810 --> 00:14:21.560 more closely. 00:14:21.560 --> 00:14:24.980 Technically, in the books, Padma, I believe, was from Ravenclaw. 00:14:24.980 --> 00:14:27.080 So this is actually a mistake or an inconsistency 00:14:27.080 --> 00:14:28.730 between the movies and the books. 00:14:28.730 --> 00:14:31.610 Let's see if we can't fix this inconsistency in our code. 00:14:31.610 --> 00:14:33.200 So how about we do this? 00:14:33.200 --> 00:14:36.350 If the student's name that's inputted equals Padma, 00:14:36.350 --> 00:14:40.220 why don't we override whatever the house is 00:14:40.220 --> 00:14:42.890 and change it to be properly Gryffindor. 00:14:42.890 --> 00:14:46.070 Let me go ahead and do if student-- 00:14:46.070 --> 00:14:49.700 now, if I want to get at Padma's name, I'm going to have to do student [0]. 00:14:49.700 --> 00:14:52.610 I have to know what location the name is in in this tuple. 00:14:52.610 --> 00:14:57.740 But if that value equals equals Padma, let's go ahead with this if statement 00:14:57.740 --> 00:14:58.670 and make a change. 00:14:58.670 --> 00:15:02.190 Let's change the student's [1] value. 00:15:02.190 --> 00:15:04.610 So the second value, if we're zero indexing-- 00:15:04.610 --> 00:15:07.370 let's change it to be another house in the world of Harry Potter 00:15:07.370 --> 00:15:08.930 called Ravenclaw. 00:15:08.930 --> 00:15:11.750 So I'm just fixing maybe the user's input. 00:15:11.750 --> 00:15:14.180 They watched the movie so they type in Padma Gryffindor, 00:15:14.180 --> 00:15:17.330 but, mm-mm, in the books, it was Padma from Ravenclaw. 00:15:17.330 --> 00:15:20.240 All right, let me go ahead and go down to my terminal window, 00:15:20.240 --> 00:15:24.040 clear my terminal, and do Python of student.py, Enter. 00:15:24.040 --> 00:15:26.995 I'm going to do Harry as well as Gryffindor, just to demonstrate 00:15:26.995 --> 00:15:29.770 that that is still working as intended. 00:15:29.770 --> 00:15:33.970 Let me clear my screen again, though, and run Python of student.py on Padma, 00:15:33.970 --> 00:15:37.480 and I'll put her, too, in Gryffindor, as in the movies, and hit Enter. 00:15:37.480 --> 00:15:40.930 And now I just see a big mess of errors on the screen. 00:15:40.930 --> 00:15:42.700 Some kind of exception has been thrown. 00:15:42.700 --> 00:15:45.160 And indeed, a type error has happened. 00:15:45.160 --> 00:15:48.730 I'm using a data type wherein there's an error, and what is that error? 00:15:48.730 --> 00:15:53.030 Well, 'tuple' object does not support item assignment. 00:15:53.030 --> 00:15:57.355 It's a little arcanely expressed-- that is, it's not really very user friendly. 00:15:57.355 --> 00:15:59.230 But if you think about what those words mean, 00:15:59.230 --> 00:16:01.450 'tuple" object does not support item assignment. 00:16:01.450 --> 00:16:03.470 So assignment is copying from right to left. 00:16:03.470 --> 00:16:05.200 So somehow, that's invalid. 00:16:05.200 --> 00:16:10.270 And here is a manifestation of the immutability of tuples. 00:16:10.270 --> 00:16:14.200 You cannot change location 0 or 1 or anything inside. 00:16:14.200 --> 00:16:15.250 That is a feature. 00:16:15.250 --> 00:16:17.270 That is the design of a tuple. 00:16:17.270 --> 00:16:20.020 So if I want to override that, I think I'm 00:16:20.020 --> 00:16:23.140 going to have to use a different type of data that we've used before-- 00:16:23.140 --> 00:16:24.730 namely a list, and that's fine. 00:16:24.730 --> 00:16:28.420 If you want to enable yourself and colleagues using your code 00:16:28.420 --> 00:16:31.090 to change the contents of that container, 00:16:31.090 --> 00:16:34.360 well, we can go ahead and return not a tuple using 00:16:34.360 --> 00:16:37.550 explicit parentheses or no parentheses, just the comma, 00:16:37.550 --> 00:16:39.250 but I can use square brackets. 00:16:39.250 --> 00:16:41.920 And if I'm using square brackets on the left and the right, 00:16:41.920 --> 00:16:44.440 this is indeed explicitly a list. 00:16:44.440 --> 00:16:47.050 Same idea, but it's mutable. 00:16:47.050 --> 00:16:50.000 That is to say you can change the contents of a list. 00:16:50.000 --> 00:16:53.110 So making no other changes, just returning a list 00:16:53.110 --> 00:16:57.820 with square brackets instead of a tuple with parentheses or just the comma. 00:16:57.820 --> 00:17:01.030 Let me go ahead now and run Python of student.py, Enter. 00:17:01.030 --> 00:17:04.000 Let me type in Harry and Gryffindor again. 00:17:04.000 --> 00:17:05.470 That's still working. 00:17:05.470 --> 00:17:06.400 Good to see. 00:17:06.400 --> 00:17:11.410 Let me run this once more and type in Padma and Gryffindor, as in the movies, 00:17:11.410 --> 00:17:15.339 but no, now we've corrected it to be Padma from Ravenclaw, 00:17:15.339 --> 00:17:17.970 as in the books instead. 00:17:17.970 --> 00:17:22.560 Any questions now on tuples versus lists or this idea 00:17:22.560 --> 00:17:26.549 of immutability versus mutability. 00:17:26.549 --> 00:17:31.845 AUDIENCE: Can we use a nested tuple in Python, like a nest list? 00:17:31.845 --> 00:17:32.970 DAVID J. MALAN: Absolutely. 00:17:32.970 --> 00:17:35.250 You can have not only nested lists in Python, 00:17:35.250 --> 00:17:38.282 where one of the elements in a list could be another list-- so you 00:17:38.282 --> 00:17:41.490 have some square brackets out here; you might have some other square brackets 00:17:41.490 --> 00:17:42.030 inside. 00:17:42.030 --> 00:17:44.830 You can absolutely do the same with a tuple as well. 00:17:44.830 --> 00:17:47.820 There is no constraint on the types of values you can put in there. 00:17:47.820 --> 00:17:50.140 We've not had occasion to do that in this case. 00:17:50.140 --> 00:17:54.030 I'm just returning a simple tuple with two elements. 00:17:54.030 --> 00:17:56.310 But yes, you could absolutely do that, too. 00:17:56.310 --> 00:17:59.415 Other questions on tuples versus lists? 00:17:59.415 --> 00:18:03.210 AUDIENCE: OK, for example, when I see the square brackets, 00:18:03.210 --> 00:18:05.258 is it mainly used for the list? 00:18:05.258 --> 00:18:07.050 DAVID J. MALAN: Oh, a really good question. 00:18:07.050 --> 00:18:07.860 Sort of. 00:18:07.860 --> 00:18:12.248 So when you create a value like a list, you use square brackets, 00:18:12.248 --> 00:18:14.040 and that would indeed be a visual indicator 00:18:14.040 --> 00:18:16.470 that this is definitely a list. 00:18:16.470 --> 00:18:19.050 If you instead see parentheses, that's a visual indicator, 00:18:19.050 --> 00:18:21.690 when creating a value, that it's definitely a tuple. 00:18:21.690 --> 00:18:26.400 However, somewhat confusingly, both lists and tuples 00:18:26.400 --> 00:18:30.390 use square brackets when you access the contents of them. 00:18:30.390 --> 00:18:34.020 When you index into them at location 0 or location 1 00:18:34.020 --> 00:18:36.070 you always use square brackets. 00:18:36.070 --> 00:18:37.500 So that's the distinction there. 00:18:37.500 --> 00:18:38.880 Good question. 00:18:38.880 --> 00:18:42.930 Allow me to propose now, if I may, that we solve this problem yet another way, 00:18:42.930 --> 00:18:46.260 and let's see if we're either making things better or for worse than us. 00:18:46.260 --> 00:18:49.920 Recall that dictionaries, or dict objects, also exist in Python. 00:18:49.920 --> 00:18:53.250 And a dictionary is this collection of keys and values. 00:18:53.250 --> 00:18:55.470 And the upside, in particular, of a dictionary 00:18:55.470 --> 00:18:57.090 is that they have better semantics. 00:18:57.090 --> 00:18:59.640 You don't just have to assume that a name is always 00:18:59.640 --> 00:19:03.300 going to be at location 0; house is always going to be at location 1. 00:19:03.300 --> 00:19:06.930 That's the kind of thing, especially if you had three, four, or more values-- 00:19:06.930 --> 00:19:09.990 eventually you or someone is going to get confused and forget 00:19:09.990 --> 00:19:12.960 what the order is and you're going to write buggy code. 00:19:12.960 --> 00:19:15.330 So a dictionary is a little more powerful 00:19:15.330 --> 00:19:18.990 in that you can semantically associate keys, little descriptions, 00:19:18.990 --> 00:19:20.040 with the values-- 00:19:20.040 --> 00:19:22.525 those keys and those values, respectively. 00:19:22.525 --> 00:19:25.650 So let me go ahead and do this, and we can do this in a few different ways. 00:19:25.650 --> 00:19:28.950 But let me propose that we focus on get_student here . 00:19:28.950 --> 00:19:30.580 And let's go ahead and do this. 00:19:30.580 --> 00:19:35.880 Let me go ahead and delete the implementation of get_student as is. 00:19:35.880 --> 00:19:40.080 Let me create a student variable and initialize it to an empty dictionary. 00:19:40.080 --> 00:19:43.080 And I can do that with just two curly braces here. 00:19:43.080 --> 00:19:46.950 And then let me go ahead and set two keys inside of that dictionary. 00:19:46.950 --> 00:19:49.710 Inside of the student, there will be, quote unquote, a "name" 00:19:49.710 --> 00:19:53.610 key, and the value of that is going to be whatever the return value of input 00:19:53.610 --> 00:19:55.620 is when I prompt the user for their name. 00:19:55.620 --> 00:19:59.700 And then the "house" key, inside of that same student dictionary, 00:19:59.700 --> 00:20:02.730 is going to be the return value of whatever the user types 00:20:02.730 --> 00:20:07.020 in for their house. , And lastly I'm going to go ahead and return student. 00:20:07.020 --> 00:20:11.010 So now I am literally returning one thing, still, 00:20:11.010 --> 00:20:15.300 but this time, it's a dict rather than a tuple, rather than a list. 00:20:15.300 --> 00:20:18.060 But there are still two things in it, technically four things 00:20:18.060 --> 00:20:19.710 if you count the keys and the values. 00:20:19.710 --> 00:20:22.088 But there's two key value pairs. 00:20:22.088 --> 00:20:24.630 Now, my code up here is going to have to change a little bit. 00:20:24.630 --> 00:20:27.390 And let's simplify this and remove, for instance, now 00:20:27.390 --> 00:20:31.470 the Padma if statement just to focus on what's changing at hand. 00:20:31.470 --> 00:20:34.020 And let me go ahead now and leave line 2 alone. 00:20:34.020 --> 00:20:37.710 I'm still going to have a student variable that gets assigned the return 00:20:37.710 --> 00:20:39.150 value of get_student. 00:20:39.150 --> 00:20:45.390 But what I want to do here now is actually access the keys inside of that 00:20:45.390 --> 00:20:49.080 dictionary-- not by numeric index, which was for tuples and lists-- 00:20:49.080 --> 00:20:52.530 0 and 1, but by way of the keys. 00:20:52.530 --> 00:20:55.290 Now, normally, I might be in the habit, as I personally 00:20:55.290 --> 00:20:58.920 am, of using double quotes-- quote, unquote, "name" inside of there 00:20:58.920 --> 00:21:02.170 and quote, unquote, "house" inside of there. 00:21:02.170 --> 00:21:05.820 But before I even run this code and show you a mistake-- 00:21:05.820 --> 00:21:08.760 see an error on the screen, does anyone want to call out 00:21:08.760 --> 00:21:10.680 what I have done wrong here? 00:21:10.680 --> 00:21:12.360 This is just an f string. 00:21:12.360 --> 00:21:16.710 I just want to print out the value of the name key, the value of the house 00:21:16.710 --> 00:21:18.660 key in this dictionary. 00:21:18.660 --> 00:21:21.070 AUDIENCE: [INAUDIBLE] 00:21:21.348 --> 00:21:23.640 DAVID J. MALAN: Your audio was a little garbled for us. 00:21:23.640 --> 00:21:26.230 But I think I heard double quotes and single quotes. 00:21:26.230 --> 00:21:29.710 So I'm going to assume that, indeed, you've identified precisely the issue. 00:21:29.710 --> 00:21:31.560 I'm just going to confuse Python right now. 00:21:31.560 --> 00:21:35.820 Even though this is an f string inside of double quotes, prefixed with an f, 00:21:35.820 --> 00:21:39.005 I can't actually use my double quotes inside my double quotes 00:21:39.005 --> 00:21:41.130 because that's going to potentially confuse Python. 00:21:41.130 --> 00:21:44.370 If I run this program now, Python of student.py and hit 00:21:44.370 --> 00:21:46.180 Enter, I get a syntax error. 00:21:46.180 --> 00:21:48.000 So the program didn't even run fully. 00:21:48.000 --> 00:21:51.970 It just couldn't be understood because it got confused by those double quotes. 00:21:51.970 --> 00:21:54.060 So the simplest fix here would indeed just 00:21:54.060 --> 00:21:58.260 be to use not double quotes but single quotes around the keys, 00:21:58.260 --> 00:22:02.058 or conversely, flip the double quotes on the outside to single quotes, 00:22:02.058 --> 00:22:03.600 then use double quotes in the inside. 00:22:03.600 --> 00:22:05.110 You just want to be consistent. 00:22:05.110 --> 00:22:09.060 So a subtle detail, but again, this is now specific to dictionary syntax. 00:22:09.060 --> 00:22:14.673 This isn't fundamental to how we're solving this current problem at hand. 00:22:14.673 --> 00:22:16.090 Well, let's go ahead and try this. 00:22:16.090 --> 00:22:18.400 Let me go ahead now and run Python of student.py. 00:22:18.400 --> 00:22:19.840 Let's go ahead and type in Harry. 00:22:19.840 --> 00:22:20.920 Let's type in Gryffindor. 00:22:20.920 --> 00:22:24.070 And hopefully, Harry is back from Gryffindor. 00:22:24.070 --> 00:22:25.360 No syntax errors. 00:22:25.360 --> 00:22:26.260 No other errors. 00:22:26.260 --> 00:22:28.000 I think I'm back in business here. 00:22:28.000 --> 00:22:31.120 And what I do like to be clear about using a dictionary 00:22:31.120 --> 00:22:34.000 is that it's allowing me just better semantics. 00:22:34.000 --> 00:22:38.950 And again, I don't have to remember, memorize, document that 0 is name; 00:22:38.950 --> 00:22:40.180 1 is house. 00:22:40.180 --> 00:22:43.312 Instead, "name" is name and "house" is house. 00:22:43.312 --> 00:22:45.520 It's just a little clearer, a little more expressive. 00:22:45.520 --> 00:22:49.270 So that's generally a good thing, especially if we stored more data about 00:22:49.270 --> 00:22:52.990 students than just their name and their house-- if you had three fields, four, 00:22:52.990 --> 00:22:54.640 five, 10 different fields-- 00:22:54.640 --> 00:22:58.870 no one's going to want to remember or be able to remember forever which is zero 00:22:58.870 --> 00:23:00.610 which is 1, which is 2, and so forth. 00:23:00.610 --> 00:23:04.960 Better to introduce names, like "name" and "house" in this case. 00:23:04.960 --> 00:23:07.120 But let me tighten this up further. 00:23:07.120 --> 00:23:10.540 I'm typically in the habit of not introducing variables unnecessarily, 00:23:10.540 --> 00:23:12.250 unless they make the code more readable. 00:23:12.250 --> 00:23:15.940 And an alternative way to format the same code would be this. 00:23:15.940 --> 00:23:19.330 Strictly speaking, I don't need to create an empty dictionary, 00:23:19.330 --> 00:23:23.380 then add one key to it, then add a second key to it, 00:23:23.380 --> 00:23:25.360 and then return that dictionary. 00:23:25.360 --> 00:23:29.470 I can actually consolidate this all into one statement, if you will. 00:23:29.470 --> 00:23:30.950 Let me go ahead and do this. 00:23:30.950 --> 00:23:35.470 Let me go ahead and say name equals inputs return value, 00:23:35.470 --> 00:23:38.567 house equals inputs return value, and then, 00:23:38.567 --> 00:23:41.650 instead of returning any variable name student, which I'm going to propose 00:23:41.650 --> 00:23:44.200 doesn't need to exist anymore, let me just create 00:23:44.200 --> 00:23:46.430 and return the dictionary all at once. 00:23:46.430 --> 00:23:50.470 Let me do, quote, unquote, "name" in lowercase here, and then the variable. 00:23:50.470 --> 00:23:52.030 It's storing the user's name. 00:23:52.030 --> 00:23:55.210 Then, quote, unquote, "house," as my second key, the value of which 00:23:55.210 --> 00:23:57.070 is going to be house, the variable. 00:23:57.070 --> 00:23:58.150 Now, is this better? 00:23:58.150 --> 00:23:59.012 Maybe, maybe not. 00:23:59.012 --> 00:24:00.970 Maybe the first way was a little more readable, 00:24:00.970 --> 00:24:03.400 and that's totally fine to create variables 00:24:03.400 --> 00:24:05.830 if they improve the readability of your code. 00:24:05.830 --> 00:24:09.850 But just know that you can also create and return a dictionary on the fly 00:24:09.850 --> 00:24:12.225 like this, so to speak, all in one line, and I 00:24:12.225 --> 00:24:14.350 think it's arguably pretty reasonable in this case. 00:24:14.350 --> 00:24:14.860 Why? 00:24:14.860 --> 00:24:16.088 It's just pretty short. 00:24:16.088 --> 00:24:18.880 I probably wouldn't do this if it got longer and longer and longer. 00:24:18.880 --> 00:24:22.600 I might minimally then start moving my key value pairs to separate lines. 00:24:22.600 --> 00:24:27.470 But this would just be a slightly more compact way of doing this as well. 00:24:27.470 --> 00:24:29.390 But let me propose we do one more change. 00:24:29.390 --> 00:24:32.950 Let's go ahead and introduce that same special casing of Padma 00:24:32.950 --> 00:24:36.460 to fix her house from Gryffindor, for instance, to Ravenclaw. 00:24:36.460 --> 00:24:38.230 How do we do this with dictionaries? 00:24:38.230 --> 00:24:41.620 Well, dictionaries, like lists, are mutable. 00:24:41.620 --> 00:24:45.400 You can change what is in them, just like you can lists. 00:24:45.400 --> 00:24:46.240 How do you do that? 00:24:46.240 --> 00:24:48.032 It's just a little different syntactically. 00:24:48.032 --> 00:24:50.290 So let's go back into main and do this fix. 00:24:50.290 --> 00:24:57.700 If the student variable has a name key that equals equals Padma, then, 00:24:57.700 --> 00:25:04.390 indented, go ahead and change the value of the house key inside of that student 00:25:04.390 --> 00:25:08.660 dictionary to be, quote, unquote, "Ravenclaw" instead. 00:25:08.660 --> 00:25:11.410 So very similar in spirit to what we did with a list. 00:25:11.410 --> 00:25:16.270 But instead of using location 0 and 1, we're much more clearly, explicitly, 00:25:16.270 --> 00:25:19.390 semantically using, quote, unquote, "name" and, quote, unquote, 00:25:19.390 --> 00:25:24.250 "house," because you index into lists and tuples using numbers, but you index 00:25:24.250 --> 00:25:27.910 into dictionaries using strings, as I've done here. 00:25:27.910 --> 00:25:31.240 All right, let me go ahead and run Python of student.py. 00:25:31.240 --> 00:25:33.190 We'll again do Harry from Gryffindor. 00:25:33.190 --> 00:25:35.140 And I think all is well. 00:25:35.140 --> 00:25:38.710 Let me run it one more time this time with Padma, who, in the movies, 00:25:38.710 --> 00:25:43.360 is from Gryffindor, but should really be from Ravenclaw. 00:25:43.360 --> 00:25:49.240 Any questions then on this progression from tuples to lists to dictionaries? 00:25:49.240 --> 00:25:53.450 We haven't necessarily introduced anything new, other than those tuples, 00:25:53.450 --> 00:25:56.260 which have been available to us all this time. 00:25:56.260 --> 00:25:59.980 But the goal at the moment is just to demonstrate this distinction 00:25:59.980 --> 00:26:02.230 among these different data types and how they 00:26:02.230 --> 00:26:04.350 each work a little bit differently. 00:26:04.350 --> 00:26:10.420 AUDIENCE: What if a combination of lists is there in our tuple? 00:26:10.420 --> 00:26:16.140 We can change the list because are immutable but lists are mutable? 00:26:16.140 --> 00:26:17.140 DAVID J. MALAN: Correct. 00:26:17.140 --> 00:26:19.300 You can change the contents of lists, and you 00:26:19.300 --> 00:26:23.110 can put most anything you want in them-- other lists or strings, as I've done, 00:26:23.110 --> 00:26:24.580 integers, or anything else. 00:26:24.580 --> 00:26:27.910 Tuples you can do the exact same thing, but you cannot change them once 00:26:27.910 --> 00:26:29.080 you've created them. 00:26:29.080 --> 00:26:33.040 A dictionary is more like a list in that it is mutable. 00:26:33.040 --> 00:26:34.210 You can change it. 00:26:34.210 --> 00:26:37.600 But the way you index into a dictionary is 00:26:37.600 --> 00:26:40.540 by way of these keys, these strings, as we keep saying, 00:26:40.540 --> 00:26:42.250 rather than by numbers-- 00:26:42.250 --> 00:26:45.100 those numeric indices. 00:26:45.100 --> 00:26:47.860 All right, well, let me propose that there is yet 00:26:47.860 --> 00:26:50.450 another way of solving this problem. 00:26:50.450 --> 00:26:53.890 And I would argue that there's now an opportunity at hand. 00:26:53.890 --> 00:26:56.770 Even though this program isn't particularly complicated-- 00:26:56.770 --> 00:27:01.210 all I'm doing is collecting a name from the user and a house 00:27:01.210 --> 00:27:03.160 from the user-- you could imagine wanting, 00:27:03.160 --> 00:27:05.290 longer term, to collect even more information, 00:27:05.290 --> 00:27:07.780 like the student's patronus or magical spell 00:27:07.780 --> 00:27:10.660 or a whole bunch of other information that might belong in a student. 00:27:10.660 --> 00:27:15.310 And right now, we're just using these very general purpose data 00:27:15.310 --> 00:27:16.450 types in Python-- 00:27:16.450 --> 00:27:20.090 a tuple to combine some values together; a list to do the same, 00:27:20.090 --> 00:27:23.620 but let us change it later; a dictionary, which is more powerful 00:27:23.620 --> 00:27:25.270 because it's a little more structured. 00:27:25.270 --> 00:27:28.690 It does have keys, and it has values, not just values. 00:27:28.690 --> 00:27:30.230 But you know what? 00:27:30.230 --> 00:27:33.520 We wouldn't have to be having this conversation if the authors of Python 00:27:33.520 --> 00:27:37.105 had just given us a data type called student. 00:27:37.105 --> 00:27:38.980 Wouldn't it have been nice if there were just 00:27:38.980 --> 00:27:42.180 a type of variable I could create in my code called student? 00:27:42.180 --> 00:27:43.930 Then we wouldn't have to figure out, well, 00:27:43.930 --> 00:27:46.420 do we use a tuple or a list or a dictionary? 00:27:46.420 --> 00:27:48.340 But that's pretty reasonable. 00:27:48.340 --> 00:27:52.180 You can imagine just how slippery of a slope that is, so to speak, 00:27:52.180 --> 00:27:55.150 if the creators of a language had to anticipate 00:27:55.150 --> 00:27:58.240 all the possible types of data that programmers like you 00:27:58.240 --> 00:28:00.380 and me want to store in your programs. 00:28:00.380 --> 00:28:02.890 So they just gave us these general purpose tools. 00:28:02.890 --> 00:28:05.740 But they gave us another general purpose tool 00:28:05.740 --> 00:28:09.370 that's going to allow us to create our own data types as well 00:28:09.370 --> 00:28:14.300 and actually give them names, and that terminology is a class. 00:28:14.300 --> 00:28:20.110 A class is like a blueprint for pieces of data objects. 00:28:20.110 --> 00:28:24.160 A class is a mold that you can define and give a name. 00:28:24.160 --> 00:28:26.860 And when you use that mold or you use that blueprint, 00:28:26.860 --> 00:28:31.010 you get types of data that are designed exactly as you want. 00:28:31.010 --> 00:28:36.220 So in short, classes allow you to invent your own data types in Python 00:28:36.220 --> 00:28:37.360 and give them a name. 00:28:37.360 --> 00:28:41.290 And this is a primary feature of object oriented programming, 00:28:41.290 --> 00:28:44.050 to be able to create your own objects in this way 00:28:44.050 --> 00:28:48.280 and, in the case of Python in classes, even give them some custom names. 00:28:48.280 --> 00:28:50.350 So what does this mean in real terms? 00:28:50.350 --> 00:28:52.390 Well, let me go ahead and come back to VS Code 00:28:52.390 --> 00:28:56.500 here, and let me propose that we introduce a little bit of new syntax. 00:28:56.500 --> 00:28:58.930 I'm going to go ahead and clear my terminal window first. 00:28:58.930 --> 00:29:02.800 I'm going to go to the top of my file, and I'm just going to start a thought 00:29:02.800 --> 00:29:03.910 but not finish it yet. 00:29:03.910 --> 00:29:09.070 I'm going to use this new keyword for classes, called, literally, class, 00:29:09.070 --> 00:29:11.720 so the new keyword we're going to have here. 00:29:11.720 --> 00:29:13.923 And if I go back to our slides here, this 00:29:13.923 --> 00:29:16.090 would be the official URL where you can read up more 00:29:16.090 --> 00:29:18.010 on this particular feature of Python. 00:29:18.010 --> 00:29:22.090 In the official tutorial, class is a new keyword we can use. 00:29:22.090 --> 00:29:25.510 Now, this is coincidentally related to students because students take classes, 00:29:25.510 --> 00:29:28.510 but it has nothing to do with the fact that we're dealing with students. 00:29:28.510 --> 00:29:31.780 Class is a general purpose term in a lot of languages-- 00:29:31.780 --> 00:29:35.500 Python among them-- that allow you to define these custom containers 00:29:35.500 --> 00:29:38.630 with custom names for pieces of data. 00:29:38.630 --> 00:29:39.880 So let's go back to VS Code. 00:29:39.880 --> 00:29:41.350 Let's use this new keyword. 00:29:41.350 --> 00:29:44.500 And let me propose that we create a class called Student. 00:29:44.500 --> 00:29:47.020 And by convention, I'm going to use a capital S here, 00:29:47.020 --> 00:29:49.570 and I'm going to go ahead, and with a colon, 00:29:49.570 --> 00:29:52.490 get to, later, the implementation of this class. 00:29:52.490 --> 00:29:55.615 So I'm just going to use dot dot dot, which is a valid placeholder for now, 00:29:55.615 --> 00:29:57.823 that just indicates to me that I'm going to come back 00:29:57.823 --> 00:29:58.990 to implementing this later. 00:29:58.990 --> 00:30:02.110 But as of now, it does, in fact, exist. 00:30:02.110 --> 00:30:08.470 I now have a student class defined for me that I can now use in my code here. 00:30:08.470 --> 00:30:09.860 How am I going to use it? 00:30:09.860 --> 00:30:13.240 Well, first of all, let me go down to get_student, 00:30:13.240 --> 00:30:18.610 and let me change this code to no longer use a dictionary but to use this class. 00:30:18.610 --> 00:30:19.660 I'm going to do this. 00:30:19.660 --> 00:30:22.810 I'm going to give myself a variable called student, as I've done before, 00:30:22.810 --> 00:30:26.170 but I'm going to set it equal to capital Student (). 00:30:28.900 --> 00:30:32.200 So I'm going to do what appears to be calling a function 00:30:32.200 --> 00:30:35.590 and that function, Student with a capital S, notice, 00:30:35.590 --> 00:30:39.490 matches the name that I gave this class at the top of my file. 00:30:39.490 --> 00:30:41.170 All right, what do I next want to do? 00:30:41.170 --> 00:30:43.330 I'm going to go ahead and give this student a name. 00:30:43.330 --> 00:30:46.060 Now, if I were still using a dictionary, I 00:30:46.060 --> 00:30:50.560 would say student, quote, unquote, "name," using square brackets. 00:30:50.560 --> 00:30:52.120 But this is not a dictionary. 00:30:52.120 --> 00:30:54.490 It turns out classes have what, for now, we'll 00:30:54.490 --> 00:30:58.910 call attributes, properties of sorts that allow you to specify values inside 00:30:58.910 --> 00:30:59.410 of them. 00:30:59.410 --> 00:31:01.720 And the syntax for that happens to be a dot. 00:31:01.720 --> 00:31:03.160 We've seen dots before. 00:31:03.160 --> 00:31:06.340 We've used it in the context of modules and libraries, more generally. 00:31:06.340 --> 00:31:08.860 This is another similar in spirit use of a dot 00:31:08.860 --> 00:31:12.140 that allows you to get at something inside of something else. 00:31:12.140 --> 00:31:15.190 So student.name is going to be the syntax 00:31:15.190 --> 00:31:18.070 I use for giving this student a name. 00:31:18.070 --> 00:31:21.670 And that name is going to be whatever the return value of "Name" is. 00:31:21.670 --> 00:31:24.970 And then I'm going to go ahead and say student.house to give 00:31:24.970 --> 00:31:29.272 another attribute called "House" and give that the return value of input 00:31:29.272 --> 00:31:30.730 here, prompting the user for house. 00:31:30.730 --> 00:31:33.610 And then, as before, I'm just going to return student. 00:31:33.610 --> 00:31:37.180 But now what's really powerful about class, 00:31:37.180 --> 00:31:39.790 and object-oriented programming more generally, 00:31:39.790 --> 00:31:43.360 is that I've created this custom data type called, literally, 00:31:43.360 --> 00:31:47.340 Student, capital S. I've stored one such student in a variable 00:31:47.340 --> 00:31:50.820 like I can always do in a variable called student, lowercase s. 00:31:50.820 --> 00:31:52.320 But I could call it anything I want. 00:31:52.320 --> 00:31:56.160 It just makes sense to call it student as well, but lowercase for clarity. 00:31:56.160 --> 00:31:58.300 And then I'm returning that variable. 00:31:58.300 --> 00:32:01.680 And because of my syntax in lines 14 and 15, 00:32:01.680 --> 00:32:05.070 that has the result of putting inside of that class 00:32:05.070 --> 00:32:09.300 a name attribute and a house attribute. 00:32:09.300 --> 00:32:11.610 I just need to make one more change up here. 00:32:11.610 --> 00:32:13.410 I'm going to go ahead and remove our Padma 00:32:13.410 --> 00:32:15.510 code, just so we can focus only on what's new, 00:32:15.510 --> 00:32:17.400 rather than fixing her house. 00:32:17.400 --> 00:32:20.190 And I'm going to go in here and change the syntax that 00:32:20.190 --> 00:32:21.930 previously was for dictionaries. 00:32:21.930 --> 00:32:26.970 Again, dictionaries use square brackets and then strings in quotes-- 00:32:26.970 --> 00:32:29.970 either single quotes or double quotes, depending on the context. 00:32:29.970 --> 00:32:33.210 Here, though, I'm going to change this to be student.name, 00:32:33.210 --> 00:32:37.440 and over here, I'm going to change it to be student.house. 00:32:37.440 --> 00:32:40.800 And that's just going to be my new syntax for getting the contents of what 00:32:40.800 --> 00:32:43.230 appears to be a class called student. 00:32:43.230 --> 00:32:47.460 Let me go ahead and rerun Python of student.py, Enter. 00:32:47.460 --> 00:32:49.320 Let's type in Harry's name as before. 00:32:49.320 --> 00:32:52.500 Let's put him in Gryffindor, crossing our fingers as we often do, 00:32:52.500 --> 00:32:56.500 and Harry is indeed from Gryffindor. 00:32:56.500 --> 00:32:58.150 What, though, have I done? 00:32:58.150 --> 00:33:01.000 Let's introduce one other bit of terminology here 00:33:01.000 --> 00:33:05.590 it turns out that I can create a class, using that class keyword. 00:33:05.590 --> 00:33:09.970 But any time you use a class, you're creating what are called objects. 00:33:09.970 --> 00:33:14.680 And here is the word objects, as an object-oriented programming, or OOP. 00:33:14.680 --> 00:33:16.243 Let me go back to my code here. 00:33:16.243 --> 00:33:18.910 And even though I haven't really implemented much of it at all-- 00:33:18.910 --> 00:33:20.950 I literally just left it with a dot, dot, dot-- 00:33:20.950 --> 00:33:25.420 that's enough code, lines 1 and 2, to just invent a new data type called 00:33:25.420 --> 00:33:29.710 Student, capital S, that may or may not have some future functionality as well. 00:33:29.710 --> 00:33:31.390 That's enough to create a class. 00:33:31.390 --> 00:33:34.150 What, though, am I doing on line 11? 00:33:34.150 --> 00:33:40.100 On line 11, what I'm technically doing is creating an object of that class. 00:33:40.100 --> 00:33:42.010 So this, too, is another term of art. 00:33:42.010 --> 00:33:44.780 You create objects from classes. 00:33:44.780 --> 00:33:47.020 So if we go back to that metaphor, that a class is 00:33:47.020 --> 00:33:50.910 like a blueprint for a house or a class is like a mold, 00:33:50.910 --> 00:33:54.760 an object is when you use that blueprint to build a specific house 00:33:54.760 --> 00:33:58.450 or something that comes out of-- in plaster, the mold, when you actually 00:33:58.450 --> 00:34:01.270 use that mold to create such an object. 00:34:01.270 --> 00:34:04.900 So a class is, again, the definition of a new data type. 00:34:04.900 --> 00:34:10.120 The object is the incarnation of, or technically instantiation of. 00:34:10.120 --> 00:34:13.570 And another term for objects would actually be an instance. 00:34:13.570 --> 00:34:16.190 You have instances of classes as well. 00:34:16.190 --> 00:34:17.440 So that's a lot of vocabulary. 00:34:17.440 --> 00:34:19.690 But at the end of the day, it just boils down to this. 00:34:19.690 --> 00:34:22.730 You can define your own class, which is really your own data type. 00:34:22.730 --> 00:34:26.800 You can then store attributes inside of it, using this dot notation here. 00:34:26.800 --> 00:34:31.040 And then you can access those same attributes using code like this here. 00:34:31.040 --> 00:34:33.969 And now, I have a proper "student" data type, 00:34:33.969 --> 00:34:36.610 and I don't have to hack something together 00:34:36.610 --> 00:34:39.010 using a tuple or a list or even a dictionary. 00:34:39.010 --> 00:34:43.510 I now have a proper data type called "student" that the authors of Python 00:34:43.510 --> 00:34:46.060 didn't give me; I gave myself. 00:34:46.060 --> 00:34:50.800 Any questions now on classes, this new keyword, class, or this idea 00:34:50.800 --> 00:34:53.605 of these objects or instances thereof? 00:34:53.605 --> 00:34:58.317 AUDIENCE: Is the class object mutable or immutable? 00:34:58.317 --> 00:34:59.650 DAVID J. MALAN: A good question. 00:34:59.650 --> 00:35:02.200 And we've clearly laid the stage for having that conversation 00:35:02.200 --> 00:35:03.550 about every data type now. 00:35:03.550 --> 00:35:07.960 We will see that they are mutable, but you can make them immutable. 00:35:07.960 --> 00:35:10.100 So you can get the best of both worlds. 00:35:10.100 --> 00:35:13.420 Now, by writing some actual code-- and we'll write more code than the dot, 00:35:13.420 --> 00:35:18.205 dot, dot in just a bit, other questions on classes or these objects thereof? 00:35:18.205 --> 00:35:21.635 AUDIENCE: Then what would be the properties of those classes? 00:35:21.635 --> 00:35:24.760 DAVID J. MALAN: So at the moment, the properties of-- or the attributes of, 00:35:24.760 --> 00:35:28.270 as I've been calling them thus far-- would just be "Name" and "House." 00:35:28.270 --> 00:35:32.320 It turns out that there may very well be other attributes built into classes 00:35:32.320 --> 00:35:33.550 that we may see before long. 00:35:33.550 --> 00:35:36.160 But for now, the only two attributes that I care about 00:35:36.160 --> 00:35:38.320 are the ones that I myself created-- 00:35:38.320 --> 00:35:42.222 namely "Name" and "House" or, again, what I would call attributes. 00:35:42.222 --> 00:35:43.930 And in a little bit, we're going to start 00:35:43.930 --> 00:35:47.560 calling those same attributes, more technically, instance variables. 00:35:47.560 --> 00:35:50.710 "Name" and "House," as I presented them here in VS Code 00:35:50.710 --> 00:35:55.750 are really just variables called "name" and called "house" inside 00:35:55.750 --> 00:36:00.130 of an object whose type is student. 00:36:00.130 --> 00:36:02.590 All right, so what more can we do with these classes? 00:36:02.590 --> 00:36:06.190 Well, again, on line 11 is where we're instantiating 00:36:06.190 --> 00:36:11.020 an object of the student class and assigning it to a student variable. 00:36:11.020 --> 00:36:12.400 We're then adding attributes-- 00:36:12.400 --> 00:36:16.000 "Name" and "House," respectively-- on lines 12 and 13 currently. 00:36:16.000 --> 00:36:17.950 Both of those have values that are technically 00:36:17.950 --> 00:36:21.220 strings or strs, because that's what the return value of the input is. 00:36:21.220 --> 00:36:23.980 But those attributes values could actually be any data type. 00:36:23.980 --> 00:36:26.980 We're just keeping things simple and focusing on defining students 00:36:26.980 --> 00:36:28.330 in terms of two strings-- 00:36:28.330 --> 00:36:29.380 "Name" and "House." 00:36:29.380 --> 00:36:32.140 And then, on line 14, we're returning that variable. 00:36:32.140 --> 00:36:35.320 We're returning that object to main so that we can actually 00:36:35.320 --> 00:36:37.710 print out who is from what house. 00:36:37.710 --> 00:36:40.960 Well, let's go ahead and add a bit more functionality here because, right now, 00:36:40.960 --> 00:36:44.170 on lines 12 and 13, this is a little manual. 00:36:44.170 --> 00:36:47.350 And it's a little reckless of me to just be putting anything 00:36:47.350 --> 00:36:50.320 I want inside of this student object. 00:36:50.320 --> 00:36:53.030 It turns out with classes, unlike with dictionaries, 00:36:53.030 --> 00:36:57.640 we can actually standardize, all the more, what those attributes can be 00:36:57.640 --> 00:37:00.310 and what kinds of values you can set them to. 00:37:00.310 --> 00:37:02.390 So let me go ahead and do this. 00:37:02.390 --> 00:37:04.360 Let me propose that it would actually be really 00:37:04.360 --> 00:37:08.860 nice if, instead of doing this here, let me go ahead 00:37:08.860 --> 00:37:10.820 and simplify my code as follows. 00:37:10.820 --> 00:37:13.720 Let me go ahead and give myself a local variable called name 00:37:13.720 --> 00:37:17.200 and set it equal to the return value of input, like we've done many times now 00:37:17.200 --> 00:37:17.800 already. 00:37:17.800 --> 00:37:20.320 Let me give myself one other variable for now, called house, 00:37:20.320 --> 00:37:23.350 and set it equal to the return value of input 00:37:23.350 --> 00:37:25.300 as well, prompting the user for their house. 00:37:25.300 --> 00:37:30.400 And now, instead of creating a student object from my student class 00:37:30.400 --> 00:37:34.520 and then manually putting the name attribute inside of it 00:37:34.520 --> 00:37:37.150 and the house attribute inside of it, let 00:37:37.150 --> 00:37:39.470 me actually do something more powerful. 00:37:39.470 --> 00:37:40.600 Let me do this. 00:37:40.600 --> 00:37:45.340 Let me call that Student function, which is identical to the class name-- 00:37:45.340 --> 00:37:48.130 just by defining a class, you get a function 00:37:48.130 --> 00:37:52.600 whose name is identical to the class name, with the capital letter included. 00:37:52.600 --> 00:37:54.610 But instead of just doing open parenthesis, 00:37:54.610 --> 00:37:58.160 closed parenthesis, let me pass in the name that I 00:37:58.160 --> 00:38:02.810 want to fill this object with and the house that I want to put in that object 00:38:02.810 --> 00:38:03.360 as well. 00:38:03.360 --> 00:38:09.360 And now let me set the return value as before to be student equals like this. 00:38:09.360 --> 00:38:11.080 So what have I done that's different? 00:38:11.080 --> 00:38:13.580 Fundamentally, I'm still getting user input in the same way. 00:38:13.580 --> 00:38:16.760 I'm using input on line 11 and input on line 12. 00:38:16.760 --> 00:38:21.110 And I just so happen to be storing those return values in local variables. 00:38:21.110 --> 00:38:24.860 And now we're setting the stage for the more powerful features of classes 00:38:24.860 --> 00:38:27.200 and object-oriented programming more generally. 00:38:27.200 --> 00:38:31.490 Notice that I'm deliberately passing to this capital S Student 00:38:31.490 --> 00:38:33.860 function, name, house-- 00:38:33.860 --> 00:38:36.300 I'm passing in arguments to the function. 00:38:36.300 --> 00:38:39.740 Now, the student class is not going to know what to do with those yet, 00:38:39.740 --> 00:38:44.780 but now I'm standardizing how I'm passing data into this student class. 00:38:44.780 --> 00:38:47.840 And ultimately, it's going to give me an opportunity to error 00:38:47.840 --> 00:38:51.172 check those inputs, to make sure that the name is valid, that it has a value 00:38:51.172 --> 00:38:52.880 and it's not just the user hitting Enter. 00:38:52.880 --> 00:38:56.060 It's going to allow me to ensure that it's a valid house, that it's 00:38:56.060 --> 00:38:59.210 Gryffindor or Hufflepuff or Ravenclaw or Slytherin 00:38:59.210 --> 00:39:02.900 or not just hitting Enter or some random value that the user types in. 00:39:02.900 --> 00:39:05.900 Because I'm passing "Name" and "House" to the student 00:39:05.900 --> 00:39:08.810 class, this particular function, I'm going 00:39:08.810 --> 00:39:11.430 to have more control over the correctness of my data. 00:39:11.430 --> 00:39:14.540 So let's now go up to the student class, which, up until now, 00:39:14.540 --> 00:39:16.130 I left as just dot, dot, dot. 00:39:16.130 --> 00:39:19.610 It turns out that, in the context of classes, 00:39:19.610 --> 00:39:23.900 there are a number of not just attributes or instance 00:39:23.900 --> 00:39:27.050 variables that you can put inside, but also methods. 00:39:27.050 --> 00:39:32.090 Classes come with certain methods, or functions inside of them, 00:39:32.090 --> 00:39:35.690 that you can define, and they just behave in a special way, 00:39:35.690 --> 00:39:37.880 by nature of how Python works. 00:39:37.880 --> 00:39:42.620 These functions allow you to determine behavior in a standard way. 00:39:42.620 --> 00:39:45.080 They are special methods in that sense. 00:39:45.080 --> 00:39:46.350 Now, what do I mean by this. 00:39:46.350 --> 00:39:48.080 Well, let me go back to VS Code here. 00:39:48.080 --> 00:39:53.000 And let me propose that I start to define a standard function 00:39:53.000 --> 00:39:56.900 called underscore underscore, or Dunder, as it's 00:39:56.900 --> 00:39:59.930 abbreviated, init, underscore underscore, 00:39:59.930 --> 00:40:02.930 and then I'm going to go ahead and do open parentheses, 00:40:02.930 --> 00:40:06.860 and then I'm going to put in here, literally, the word self. 00:40:06.860 --> 00:40:08.400 More on that in just a moment. 00:40:08.400 --> 00:40:11.810 But now, inside of this function, I'm going to have an opportunity 00:40:11.810 --> 00:40:16.790 to customize this class's objects. 00:40:16.790 --> 00:40:20.330 That is to say this underscore, underscore init method, 00:40:20.330 --> 00:40:25.190 or Dunder init method is specifically known as an instance method, 00:40:25.190 --> 00:40:26.690 and it's called exactly this. 00:40:26.690 --> 00:40:28.880 This is designed by the authors of Python. 00:40:28.880 --> 00:40:34.310 And if you want to initialize the contents of an object from a class, 00:40:34.310 --> 00:40:37.880 you define this method, and we'll see what it's about to do here. 00:40:37.880 --> 00:40:41.240 Let me go back to VS Code, and let me do something like this. 00:40:41.240 --> 00:40:47.750 self.name = name, and self.house = house. 00:40:47.750 --> 00:40:51.230 But I don't want to just init this object very generically. 00:40:51.230 --> 00:40:56.180 I want this method, called init, to take in not just self but name, 00:40:56.180 --> 00:40:58.380 house as well. 00:40:58.380 --> 00:40:59.908 Now, what in the world is going on? 00:40:59.908 --> 00:41:01.700 Because there's a lot of weird syntax here. 00:41:01.700 --> 00:41:03.380 There's this Dunder init method-- 00:41:03.380 --> 00:41:05.810 double underscore, init, double underscore. 00:41:05.810 --> 00:41:08.750 There's, all of a sudden, this parameter called self. 00:41:08.750 --> 00:41:12.470 And then there's this new syntax-- self.name and self.house. 00:41:12.470 --> 00:41:16.552 Now you're seeing really a manifestation of object-oriented programming. 00:41:16.552 --> 00:41:18.260 It's not all that different fundamentally 00:41:18.260 --> 00:41:20.552 from what we've been doing for weeks with dictionaries, 00:41:20.552 --> 00:41:23.120 by adding keys to dictionaries. 00:41:23.120 --> 00:41:28.340 But in this case, we're adding variables to objects, a.k.a. 00:41:28.340 --> 00:41:30.480 instance variables to objects. 00:41:30.480 --> 00:41:31.470 Now, what's going on? 00:41:31.470 --> 00:41:32.550 Let's do this in reverse. 00:41:32.550 --> 00:41:34.730 Let's go back to the line of code we wrote earlier. 00:41:34.730 --> 00:41:38.720 On line 15, I am treating the name of this class-- 00:41:38.720 --> 00:41:41.930 Student with a capital S-- as a function. 00:41:41.930 --> 00:41:44.600 And I am passing in two values-- 00:41:44.600 --> 00:41:45.920 "Name" and "House." 00:41:45.920 --> 00:41:48.710 What I've highlighted here on the screen, on line 15, 00:41:48.710 --> 00:41:51.260 is generally known as a constructor call. 00:41:51.260 --> 00:41:56.870 This is a line of code that is going to construct a student object for me. 00:41:56.870 --> 00:42:02.810 Using synonyms, it is going to instantiate a student object for me. 00:42:02.810 --> 00:42:05.600 And again, how is it going to create that object? 00:42:05.600 --> 00:42:09.440 It's going to use the student class as a template, as a mold of sorts 00:42:09.440 --> 00:42:12.290 so that every student is structured the same. 00:42:12.290 --> 00:42:13.880 Every student is going to have a name. 00:42:13.880 --> 00:42:15.463 Every student's going to have a house. 00:42:15.463 --> 00:42:20.900 But because I can pass in arguments to this Student function, capital S, 00:42:20.900 --> 00:42:26.670 I'm going to be able to customize the contents of that object. 00:42:26.670 --> 00:42:29.660 So if you think about the real world-- if you've ever been on a street 00:42:29.660 --> 00:42:34.675 or a neighborhood where all of the houses look the same but they might be 00:42:34.675 --> 00:42:37.550 painted differently; they might be decorated a little bit differently 00:42:37.550 --> 00:42:40.940 on the outside, all of those houses might have been built using the exact 00:42:40.940 --> 00:42:42.560 same blueprint-- 00:42:42.560 --> 00:42:43.890 a mold, if you will. 00:42:43.890 --> 00:42:48.073 But then you can specialize exactly the finer points of those houses. 00:42:48.073 --> 00:42:50.990 By painting the outside a different color or planting different trees, 00:42:50.990 --> 00:42:52.520 you can style them differently. 00:42:52.520 --> 00:42:57.750 Similar in spirit here, we have a Student blueprint 00:42:57.750 --> 00:43:01.590 that's always going to have now a name and a house, but it's up to you and me 00:43:01.590 --> 00:43:04.180 to pass in any name and any house that we want. 00:43:04.180 --> 00:43:06.040 Now, where is this function? 00:43:06.040 --> 00:43:09.120 The fact that I'm calling Student, capital, S and then a parenthesis 00:43:09.120 --> 00:43:11.252 and a closed parenthesis with arguments inside 00:43:11.252 --> 00:43:12.960 suggest that there's a function somewhere 00:43:12.960 --> 00:43:17.490 in the world that has been defined, with def, that's going to be called. 00:43:17.490 --> 00:43:20.430 Well, as you might have guessed by now, the function 00:43:20.430 --> 00:43:25.500 that will always be called, by definition of how Python classes work, 00:43:25.500 --> 00:43:29.980 is a function called double underscore, init, double underscore. 00:43:29.980 --> 00:43:30.480 Why? 00:43:30.480 --> 00:43:33.330 It's a crazy name, but it's what the authors of Python 00:43:33.330 --> 00:43:38.760 chose to just implement the initialization of an object in Python. 00:43:38.760 --> 00:43:40.900 Now, the only weird thing-- 00:43:40.900 --> 00:43:43.140 especially weird thing, I will admit, is this. 00:43:43.140 --> 00:43:49.110 It would be way clearer, to me, too, if the only two parameters for init we're 00:43:49.110 --> 00:43:50.670 just name, house. 00:43:50.670 --> 00:43:53.550 That's how we've defined every function thus far in the class. 00:43:53.550 --> 00:43:57.300 You just specify the parameters that you want the function to accept. 00:43:57.300 --> 00:44:00.330 And that lines up with what I'm doing on line 15. 00:44:00.330 --> 00:44:04.530 I am only passing in two things to the student function. 00:44:04.530 --> 00:44:07.832 But it turns out that the authors of Python 00:44:07.832 --> 00:44:09.540 need to give us a little bit of help here 00:44:09.540 --> 00:44:14.460 because suppose that you pass in "Name" and "House" to this init method. 00:44:14.460 --> 00:44:17.820 And a method is just a function inside of a class. 00:44:17.820 --> 00:44:20.310 What are you going to do with the name and the house? 00:44:20.310 --> 00:44:22.350 Literally, where are you going to put them? 00:44:22.350 --> 00:44:26.520 If you want to remember the name and the house for this student, 00:44:26.520 --> 00:44:29.170 you've got to be able to store those values somewhere. 00:44:29.170 --> 00:44:32.430 And how do you store them in the current object 00:44:32.430 --> 00:44:34.830 that has just been "instantiated?" 00:44:34.830 --> 00:44:37.620 Well, the authors of Python decided that the convention 00:44:37.620 --> 00:44:41.940 is going to be that this init method also, semi secretly, takes 00:44:41.940 --> 00:44:44.640 a third argument, that has to come first. 00:44:44.640 --> 00:44:46.650 By convention, it's called self, but you could 00:44:46.650 --> 00:44:48.233 call it technically anything you want. 00:44:48.233 --> 00:44:50.190 But the convention is to always call it self. 00:44:50.190 --> 00:44:53.520 And self, as its name implies, gives you access 00:44:53.520 --> 00:44:56.790 to the current object that was just created. 00:44:56.790 --> 00:44:58.020 What does that mean? 00:44:58.020 --> 00:45:02.160 Again, now, on line 14, now that it's moved down a little bit, 00:45:02.160 --> 00:45:04.080 this line here is a constructor. 00:45:04.080 --> 00:45:06.180 It constructs a student object. 00:45:06.180 --> 00:45:08.280 But there's nothing in that object initially. 00:45:08.280 --> 00:45:09.750 There's no name; there's no house. 00:45:09.750 --> 00:45:12.780 But the object exists in the computer's memory. 00:45:12.780 --> 00:45:16.560 It's up to, now, you to store the name and the house inside of that object. 00:45:16.560 --> 00:45:17.500 How do you do that? 00:45:17.500 --> 00:45:21.480 Well, Python will just automatically call this init method for you, 00:45:21.480 --> 00:45:27.480 and it's going to automatically pass in a reference to an argument that 00:45:27.480 --> 00:45:32.370 represents the current object that it just constructed in memory for you, 00:45:32.370 --> 00:45:34.710 and it's up to you to populate it with values. 00:45:34.710 --> 00:45:37.680 And what this means is that, inside of your init method, 00:45:37.680 --> 00:45:42.870 you can literally do self.name to create a new attribute, a.k.a. 00:45:42.870 --> 00:45:46.170 an instance variable, inside of that otherwise empty 00:45:46.170 --> 00:45:48.420 object and put this name inside of it. 00:45:48.420 --> 00:45:51.875 It allows you to do self.house and store that value of house. 00:45:51.875 --> 00:45:54.000 Now, you could call these things anything you want. 00:45:54.000 --> 00:45:54.750 They could be n. 00:45:54.750 --> 00:45:56.400 They could be h, as before. 00:45:56.400 --> 00:46:00.270 But that's really not very self-explanatory. 00:46:00.270 --> 00:46:04.260 Much better to do this kind of convention. self.name equals name. 00:46:04.260 --> 00:46:06.330 self.house equals house. 00:46:06.330 --> 00:46:11.700 And this is like installing into the otherwise empty object the value name 00:46:11.700 --> 00:46:16.080 and house and storing them in, really, identically named instance 00:46:16.080 --> 00:46:17.710 variables in the object. 00:46:17.710 --> 00:46:20.820 And again, an object is just an instance of a class. 00:46:20.820 --> 00:46:23.190 Now, I know that was a lot of vocabulary. 00:46:23.190 --> 00:46:24.880 That's a lot of weird syntax. 00:46:24.880 --> 00:46:28.620 So any questions on this init method, whose purpose in life, 00:46:28.620 --> 00:46:33.545 again, is to initialize an otherwise empty object when you first create it? 00:46:33.545 --> 00:46:36.810 AUDIENCE: So what is the difference between the init method and default 00:46:36.810 --> 00:46:37.497 constructor? 00:46:37.497 --> 00:46:38.830 DAVID J. MALAN: A good question. 00:46:38.830 --> 00:46:41.580 So in other languages-- if you programmed before. 00:46:41.580 --> 00:46:45.180 For instance, Java-- there are functions that are explicitly called 00:46:45.180 --> 00:46:47.910 constructors that construct an object. 00:46:47.910 --> 00:46:49.410 They initialize it with values. 00:46:49.410 --> 00:46:53.310 Python technically calls this init method the initialization method. 00:46:53.310 --> 00:46:54.960 It initializes the value. 00:46:54.960 --> 00:47:00.870 It's on line 15 now of my code, if I scroll back down, that I'm technically 00:47:00.870 --> 00:47:02.430 constructing the object. 00:47:02.430 --> 00:47:05.340 It turns out there's another special method in Python, 00:47:05.340 --> 00:47:07.290 that we won't talk about in detail today, 00:47:07.290 --> 00:47:11.130 called underscore underscore, new, underscore underscore 00:47:11.130 --> 00:47:13.380 that actually handles the process of creating 00:47:13.380 --> 00:47:15.450 an empty object in memory for us. 00:47:15.450 --> 00:47:17.830 But, generally speaking, you, the programmer, 00:47:17.830 --> 00:47:19.930 don't need to manipulate the new function. 00:47:19.930 --> 00:47:20.910 It just works for you. 00:47:20.910 --> 00:47:23.760 Instead, you define your own init method here 00:47:23.760 --> 00:47:27.060 and init function inside of your class, and that method 00:47:27.060 --> 00:47:29.927 initializes the contents of the object. 00:47:29.927 --> 00:47:32.760 So there's technically a distinction between constructing the object 00:47:32.760 --> 00:47:35.260 with new and initializing it with init. 00:47:35.260 --> 00:47:39.270 But in the world of Python, you pretty much only worry about the init method. 00:47:39.270 --> 00:47:42.270 Python generally does the other part for you. 00:47:42.270 --> 00:47:43.020 A good question. 00:47:43.020 --> 00:47:44.325 Others? 00:47:44.325 --> 00:47:48.150 AUDIENCE: What about if you want to store more than one name or more 00:47:48.150 --> 00:47:49.080 than one house? 00:47:49.080 --> 00:47:49.770 DAVID J. MALAN: A good question. 00:47:49.770 --> 00:47:52.680 If you want to store more than one name or more than one house, 00:47:52.680 --> 00:47:54.100 you can do this in different ways. 00:47:54.100 --> 00:47:57.930 You could create other attributes-- technically called instance variables-- 00:47:57.930 --> 00:48:01.710 like self.name1, self.name2. 00:48:01.710 --> 00:48:03.720 But we've seen, in the past, that that is not 00:48:03.720 --> 00:48:06.300 a very good design, just to have multiple variables to store 00:48:06.300 --> 00:48:07.260 multiple things. 00:48:07.260 --> 00:48:12.630 Maybe, instead, you have an instance variable called self.names, plural, 00:48:12.630 --> 00:48:15.978 and you set it equal to a list of names or a list of houses. 00:48:15.978 --> 00:48:19.020 Now, in this case, I don't think that really solves a problem because I'm 00:48:19.020 --> 00:48:22.950 trying to implement a student, singular, so it doesn't really make sense 00:48:22.950 --> 00:48:24.240 to have multiple first names. 00:48:24.240 --> 00:48:27.150 Maybe a nickname, maybe a last name, so we could add those, too. 00:48:27.150 --> 00:48:29.370 But I don't think we need multiple names per se 00:48:29.370 --> 00:48:31.320 and, in this case, multiple houses. 00:48:31.320 --> 00:48:34.500 But absolutely, you could do that using some of our familiar building blocks 00:48:34.500 --> 00:48:35.910 like lists. 00:48:35.910 --> 00:48:37.530 Other questions? 00:48:37.530 --> 00:48:40.300 AUDIENCE: How are classes or objects represented in memory? 00:48:40.300 --> 00:48:43.050 DAVID J. MALAN: How are classes and objects represented in memory? 00:48:43.050 --> 00:48:44.940 So the class is technically just code. 00:48:44.940 --> 00:48:48.330 It is the code on the top of my file-- lines 1 through fou4-- 00:48:48.330 --> 00:48:52.260 that defines that blueprint, that template, if you will. 00:48:52.260 --> 00:48:54.840 Objects are stored in the computer's memory 00:48:54.840 --> 00:48:56.350 by taking up some number of bytes. 00:48:56.350 --> 00:48:59.130 So you're probably familiar with bytes or kilobytes or megabytes. 00:48:59.130 --> 00:49:01.770 There's some chunk of bytes, probably all 00:49:01.770 --> 00:49:03.720 in the same location in the computer's memory 00:49:03.720 --> 00:49:07.920 or RAM, where those objects are stored. 00:49:07.920 --> 00:49:11.490 But that's what Python, the program, handles for you. 00:49:11.490 --> 00:49:14.940 Python the interpreter figures out where in the computer's memory to put it. 00:49:14.940 --> 00:49:18.030 You and I, the programmers, get to think and solve problems at this level. 00:49:18.030 --> 00:49:21.930 Python, the interpreter, handles those lower level details for you. 00:49:21.930 --> 00:49:25.170 How about one final question on classes and objects? 00:49:25.170 --> 00:49:28.110 AUDIENCE: So my question is if we can the same do 00:49:28.110 --> 00:49:31.630 the same thing with the dictionaries, so why to use classes? 00:49:31.630 --> 00:49:32.880 DAVID J. MALAN: Good question. 00:49:32.880 --> 00:49:34.770 If you can do the same things as you can with dictionaries, 00:49:34.770 --> 00:49:35.970 why should you use classes? 00:49:35.970 --> 00:49:38.400 Because we are just scratching the surface now of what 00:49:38.400 --> 00:49:39.840 you can do with classes. 00:49:39.840 --> 00:49:42.660 Allow me to go back, now, to my keyboard and show you 00:49:42.660 --> 00:49:44.370 more of what you can do with classes. 00:49:44.370 --> 00:49:47.760 But in short, you can do much more with classes. 00:49:47.760 --> 00:49:51.810 You can ensure the correctness of your data much more with classes. 00:49:51.810 --> 00:49:53.160 You can error-check things. 00:49:53.160 --> 00:49:57.390 And generally, you can design more complicated software more effectively. 00:49:57.390 --> 00:49:59.460 And we'll continue to see, today, features 00:49:59.460 --> 00:50:02.400 of Python and object-oriented programming more generally 00:50:02.400 --> 00:50:04.870 that allows us to do just that. 00:50:04.870 --> 00:50:09.330 So let me propose, in fact, that first, let's just tighten up this current 00:50:09.330 --> 00:50:13.920 implementation, which again has us with an init method that just declares two 00:50:13.920 --> 00:50:16.890 instance variables-- self.name and self.house, house, which, again, 00:50:16.890 --> 00:50:21.420 just creates those variables inside of the otherwise empty object and assigns 00:50:21.420 --> 00:50:22.110 them values-- 00:50:22.110 --> 00:50:23.700 name and house, respectively. 00:50:23.700 --> 00:50:25.980 Let me go ahead and just do one little thing here. 00:50:25.980 --> 00:50:27.780 I don't really need this student variable. 00:50:27.780 --> 00:50:31.740 Let me just tighten this up so that each time we improve or change the code, 00:50:31.740 --> 00:50:34.980 we're focusing, really, on just the minimal changes alone. 00:50:34.980 --> 00:50:37.258 So I've not fundamentally done anything different. 00:50:37.258 --> 00:50:39.300 I just got rid of the variable name, and I'm just 00:50:39.300 --> 00:50:43.110 returning the return value of this student function 00:50:43.110 --> 00:50:45.120 that's constructing my new object for me. 00:50:45.120 --> 00:50:48.840 So I'm just tightening things up as we've done many times in the past. 00:50:48.840 --> 00:50:53.310 Well, what if something goes wrong in creating this student? 00:50:53.310 --> 00:50:56.790 For instance, what if the user does not give us a name, and they just hit 00:50:56.790 --> 00:50:58.800 Enter when prompted for name. 00:50:58.800 --> 00:51:01.230 I don't want to put in my computer's memory 00:51:01.230 --> 00:51:04.380 a bogus student object that has no name. 00:51:04.380 --> 00:51:07.830 I'd ideally like to check for errors before I even create it 00:51:07.830 --> 00:51:09.570 so I don't create a nameless student. 00:51:09.570 --> 00:51:13.780 It would just be weird and probably a bug to have an object that has no name. 00:51:13.780 --> 00:51:16.200 Similarly, I don't want the user to be able to type 00:51:16.200 --> 00:51:18.720 in something random as their house. 00:51:18.720 --> 00:51:21.360 At least in the world of Harry Potter, there's really 00:51:21.360 --> 00:51:23.790 only four houses, at Hogwarts at least. 00:51:23.790 --> 00:51:25.680 There's, again, Gryffindor and Hufflepuff 00:51:25.680 --> 00:51:29.610 and Ravenclaw and Slytherin-- a list of four valid houses. 00:51:29.610 --> 00:51:34.290 It would be nice if I somehow validated that the user's input is indeed 00:51:34.290 --> 00:51:35.550 in that list. 00:51:35.550 --> 00:51:39.930 Now, I could do all of that validation in my get_student function. 00:51:39.930 --> 00:51:41.730 I could check, is the name empty? 00:51:41.730 --> 00:51:44.430 If so, don't create the student object. 00:51:44.430 --> 00:51:46.680 Is the house one of those four houses? 00:51:46.680 --> 00:51:49.080 If not, don't create the student object. 00:51:49.080 --> 00:51:52.740 But that would be rather decoupled from the student itself. 00:51:52.740 --> 00:51:57.630 get_student currently exists as just my own function in my student.py file. 00:51:57.630 --> 00:52:01.110 But classes-- and really, object-oriented programming-- more 00:52:01.110 --> 00:52:05.280 generally encourages you to encapsulate, inside 00:52:05.280 --> 00:52:09.730 of a class, all functionality related to that class. 00:52:09.730 --> 00:52:12.360 So if you want to validate that a name exists-- 00:52:12.360 --> 00:52:14.760 if you want to validate that a house is correct, 00:52:14.760 --> 00:52:20.040 that belongs just fundamentally in the class called student itself, 00:52:20.040 --> 00:52:22.590 not in some random function that you wrote elsewhere. 00:52:22.590 --> 00:52:24.870 Again, this is just methodology because, again, 00:52:24.870 --> 00:52:28.560 if we think about writing code that gets longer and longer, more and more 00:52:28.560 --> 00:52:31.260 complicated, it should make just intuitive sense that, 00:52:31.260 --> 00:52:34.230 if you keep all the house-- 00:52:34.230 --> 00:52:37.200 all of the name and all of the house-related code in the student, 00:52:37.200 --> 00:52:38.700 it's just better organization. 00:52:38.700 --> 00:52:41.568 Keep all of the related code together, and that's probably 00:52:41.568 --> 00:52:43.110 going to set you up for more success. 00:52:43.110 --> 00:52:45.150 And indeed, that's part of this methodology 00:52:45.150 --> 00:52:47.130 of object-oriented programming. 00:52:47.130 --> 00:52:51.930 Let me go ahead now and change my students classes 00:52:51.930 --> 00:52:53.880 init method to do this. 00:52:53.880 --> 00:52:56.360 If the name is blank-- 00:52:56.360 --> 00:52:59.350 so if not name-- and we've seen this kind of syntax before. 00:52:59.350 --> 00:53:02.680 If you say in Python, Pythonically, if not name, 00:53:02.680 --> 00:53:04.480 that's doing something like this. 00:53:04.480 --> 00:53:07.290 If name equals, equals, quote, unquote-- 00:53:07.290 --> 00:53:09.040 but I can do this a little more elegantly. 00:53:09.040 --> 00:53:11.950 Just say, if not name, would be the more Pythonic. 00:53:11.950 --> 00:53:15.575 Well, I want to return an error. 00:53:15.575 --> 00:53:17.200 I might want to do something like this. 00:53:17.200 --> 00:53:18.910 Print missing name. 00:53:18.910 --> 00:53:20.530 But this is not good enough. 00:53:20.530 --> 00:53:23.980 It does not suffice to just print out missing name 00:53:23.980 --> 00:53:26.080 and then let the rest of the code go through. 00:53:26.080 --> 00:53:27.788 All right, well, what could I do instead? 00:53:27.788 --> 00:53:30.788 In the past, we've seen another technique I could do sys.exit, 00:53:30.788 --> 00:53:33.580 and I could say something like missing name, and I could go up here 00:53:33.580 --> 00:53:34.750 and I could import sys. 00:53:34.750 --> 00:53:37.960 But this is a really obnoxious solution to the problem. 00:53:37.960 --> 00:53:41.230 Just because you or maybe a colleague messed up 00:53:41.230 --> 00:53:43.750 and called a function with an invalid name, 00:53:43.750 --> 00:53:45.670 you're going to quit my whole program? 00:53:45.670 --> 00:53:49.745 That's really, really extreme of a response, 00:53:49.745 --> 00:53:52.120 and you probably don't want to do that if your program is 00:53:52.120 --> 00:53:53.050 in the middle of running. 00:53:53.050 --> 00:53:54.633 You might want to clean some stuff up. 00:53:54.633 --> 00:53:57.940 You might want to save files you don't want to just exit a program sometimes 00:53:57.940 --> 00:54:01.190 in some arbitrary line, just because input was invalid. 00:54:01.190 --> 00:54:03.350 So I don't think we want to do that either. 00:54:03.350 --> 00:54:07.360 But we do, now, have a mechanism for signaling errors. 00:54:07.360 --> 00:54:09.500 Unfortunately, I can't do something like this. 00:54:09.500 --> 00:54:13.630 I could try returning none and say, uh-uh, this student does not exist. 00:54:13.630 --> 00:54:15.670 I'm going to hand you back none instead. 00:54:15.670 --> 00:54:16.930 But it's too late. 00:54:16.930 --> 00:54:21.400 If we scroll back down to where I'm creating the student, it's on line 17 00:54:21.400 --> 00:54:23.080 now where I've highlighted this code. 00:54:23.080 --> 00:54:25.540 The student has already been created. 00:54:25.540 --> 00:54:28.750 There is an object somewhere in the computer's memory 00:54:28.750 --> 00:54:30.370 that's structured as a student. 00:54:30.370 --> 00:54:32.710 It just doesn't have any values inside of it. 00:54:32.710 --> 00:54:35.560 But it's too late, therefore, to return none. 00:54:35.560 --> 00:54:36.670 That ship has sailed. 00:54:36.670 --> 00:54:37.950 The object exists. 00:54:37.950 --> 00:54:40.450 You can't just suddenly say, nope, nope, there is no object. 00:54:40.450 --> 00:54:41.620 There is an object. 00:54:41.620 --> 00:54:43.600 It's up to you to signal an error. 00:54:43.600 --> 00:54:45.350 And how do you signal an error? 00:54:45.350 --> 00:54:48.100 Well, we've actually seen this before, but we haven't had occasion 00:54:48.100 --> 00:54:49.600 to create our own errors. 00:54:49.600 --> 00:54:54.760 It turns out, in Python, there's another keyword related to exceptions 00:54:54.760 --> 00:54:58.570 that Python itself uses to raise all of those exceptions we've 00:54:58.570 --> 00:54:59.770 talked about in the past. 00:54:59.770 --> 00:55:04.480 When you've caught things like value errors or other such exceptions that 00:55:04.480 --> 00:55:09.070 come with Python, well, it turns out you, the programmer can raise-- 00:55:09.070 --> 00:55:12.890 that is create your own exceptions when something just really goes wrong-- 00:55:12.890 --> 00:55:15.760 not wrong enough that you want to quit and exit the whole program, 00:55:15.760 --> 00:55:18.970 but enough that you need to somehow alert the programmer 00:55:18.970 --> 00:55:20.590 that there has been an error. 00:55:20.590 --> 00:55:23.590 Something exceptional, in a very bad way-- something 00:55:23.590 --> 00:55:29.270 exceptional has happened, and let them try to catch that exception as needed. 00:55:29.270 --> 00:55:32.260 So let me go back to VS Code here and propose 00:55:32.260 --> 00:55:36.490 that, if the user passes in an invalid name-- it's just empty, 00:55:36.490 --> 00:55:37.840 so there's not a name. 00:55:37.840 --> 00:55:39.860 Well, what I really want to do is this. 00:55:39.860 --> 00:55:43.060 I want to raise a value error. 00:55:43.060 --> 00:55:45.190 And we've seen the value errors before. 00:55:45.190 --> 00:55:47.830 We've created value errors accidentally before. 00:55:47.830 --> 00:55:51.430 And generally, you and I have tried to catch them if they happen. 00:55:51.430 --> 00:55:55.390 Well, the flip side of this feature of exceptions in a language like Python 00:55:55.390 --> 00:55:58.120 is that you, the programmer, can also raise exceptions 00:55:58.120 --> 00:56:00.010 when something exceptional happens. 00:56:00.010 --> 00:56:01.810 And you can even be more precise. 00:56:01.810 --> 00:56:05.260 You don't have to raise a generic value error and let the programmer figure out 00:56:05.260 --> 00:56:06.100 what went wrong. 00:56:06.100 --> 00:56:10.300 You can treat value error and all exceptions in Python like functions 00:56:10.300 --> 00:56:14.590 and actually pass to them an explanatory message like, quote, unquote, 00:56:14.590 --> 00:56:17.890 "Missing name," so that at least the programmer, when they encounter 00:56:17.890 --> 00:56:19.450 this error, knows, oh, I messed up. 00:56:19.450 --> 00:56:22.840 I didn't make sure that the user has a name. 00:56:22.840 --> 00:56:25.130 And now, what do you want to do instead? 00:56:25.130 --> 00:56:28.720 Well, now, if you're the programmer, you could do something like this. 00:56:28.720 --> 00:56:34.690 You could try to create a student except if there's a value error. 00:56:34.690 --> 00:56:37.210 Then you could handle it in some way. 00:56:37.210 --> 00:56:39.490 And I'm going to wave my hand with a dot, dot, dot, 00:56:39.490 --> 00:56:40.790 at how you would handle it. 00:56:40.790 --> 00:56:44.650 But you would handle it using try and accept, just like we have in the past, 00:56:44.650 --> 00:56:46.660 and that would allow you, the programmer, 00:56:46.660 --> 00:56:48.170 to try to create the student. 00:56:48.170 --> 00:56:52.300 But if something goes wrong, OK, I'll handle it nonetheless. 00:56:52.300 --> 00:56:54.970 So what's new here, again, is this raise keyword, 00:56:54.970 --> 00:56:59.230 that just lets you and I actually raise our own exceptions 00:56:59.230 --> 00:57:00.460 to signal these errors. 00:57:00.460 --> 00:57:03.010 Well, let me go back to my code here, and I'm just 00:57:03.010 --> 00:57:06.190 going to go ahead and not bother trying or catching this error. 00:57:06.190 --> 00:57:09.100 For now, we'll just focus on raising it and assume 00:57:09.100 --> 00:57:12.370 that, from our recon exceptions, you could add try and accept 00:57:12.370 --> 00:57:13.840 as needed in places. 00:57:13.840 --> 00:57:16.930 Let me go back to the code here and propose that something else could 00:57:16.930 --> 00:57:18.430 go wrong with house. 00:57:18.430 --> 00:57:19.810 If there is a name, we're good. 00:57:19.810 --> 00:57:22.090 But if we're given a house but it's invalid, 00:57:22.090 --> 00:57:24.940 we should probably raise an exception for that, too. 00:57:24.940 --> 00:57:26.200 So what if we do this? 00:57:26.200 --> 00:57:32.200 If house is not in the list containing "Gryffindor," quote, unquote, 00:57:32.200 --> 00:57:34.720 "Hufflepuff," quote, unquote-- 00:57:34.720 --> 00:57:38.890 let's see, "Ravenclaw," quote, unquote, or "Slytherin," 00:57:38.890 --> 00:57:41.830 quote, unquote, then, with my colon, let's 00:57:41.830 --> 00:57:43.420 raise another type of value error. 00:57:43.420 --> 00:57:45.460 But rather than raise a generic value error, 00:57:45.460 --> 00:57:49.750 let's pass in an argument, quote, unquote, "Invalid house." 00:57:49.750 --> 00:57:52.690 And so here we now see a capability that we 00:57:52.690 --> 00:57:55.910 can do with classes that we can't with dictionaries. 00:57:55.910 --> 00:58:00.560 If you add an attribute to a dictionary, a key to a dictionary, 00:58:00.560 --> 00:58:02.090 it's going in no matter what. 00:58:02.090 --> 00:58:05.987 Even if the name is empty, even if the house is a completely random string 00:58:05.987 --> 00:58:07.820 of text that's not one of these four houses, 00:58:07.820 --> 00:58:09.350 it's going into that dictionary. 00:58:09.350 --> 00:58:12.710 But with a class, and by way of this init method, 00:58:12.710 --> 00:58:17.760 you and I can now control exactly what's going to be installed, if you will, 00:58:17.760 --> 00:58:19.040 inside of this object. 00:58:19.040 --> 00:58:22.460 You have a little more control now over correctness. 00:58:22.460 --> 00:58:26.600 And so now let me go ahead and scroll back down to my terminal window 00:58:26.600 --> 00:58:27.290 and clear it. 00:58:27.290 --> 00:58:29.480 Let me run Python of student.py. 00:58:29.480 --> 00:58:31.250 Let me type in something like Harry. 00:58:31.250 --> 00:58:33.950 Let me type in Gryffindor, Enter, and we see 00:58:33.950 --> 00:58:35.660 that, indeed, Harry is from Gryffindor. 00:58:35.660 --> 00:58:37.650 What if I made a mistake, though? 00:58:37.650 --> 00:58:40.760 What if I ran Python of student.py and typed Harry as the name, 00:58:40.760 --> 00:58:44.420 but this time typed in Number Four, Privet Drive, which 00:58:44.420 --> 00:58:47.480 is where he grew up, instead of his proper Hogwarts house. 00:58:47.480 --> 00:58:51.320 Let me hit Enter now, and now you see a value error. 00:58:51.320 --> 00:58:54.470 But this isn't one that Python generated for us, per se. 00:58:54.470 --> 00:58:56.150 I raised this error. 00:58:56.150 --> 00:58:59.660 And therefore, if I went in and wrote more code in my get_student function, 00:58:59.660 --> 00:59:04.650 I could also catch this error with our usual try except syntax. 00:59:04.650 --> 00:59:09.260 So all we have now is not just classes in our toolkit, but even more powers 00:59:09.260 --> 00:59:12.590 when it comes to exceptions, and not just catching them ourselves 00:59:12.590 --> 00:59:15.350 but raising them ourselves, too. 00:59:15.350 --> 00:59:21.700 Any questions now on this use of classes and init and now this ability 00:59:21.700 --> 00:59:26.020 to raise exceptions when something goes wrong inside of the initialization? 00:59:26.020 --> 00:59:29.430 AUDIENCE: So what if the user has a middle name-- 00:59:29.430 --> 00:59:31.300 name, middle name, and last name? 00:59:31.300 --> 00:59:32.740 How would you fix that? 00:59:32.740 --> 00:59:35.260 DAVID J. MALAN: Good question. 00:59:35.260 --> 00:59:38.900 If you wanted the student to have a first name, middle name, and last name, 00:59:38.900 --> 00:59:41.050 we could do this in a bunch of different ways. 00:59:41.050 --> 00:59:44.440 The simplest, though, if-- let me clear my screen here, and let 00:59:44.440 --> 00:59:46.030 me just temporarily do this. 00:59:46.030 --> 00:59:51.340 Let me propose that the init method take in a first argument, a middle argument, 00:59:51.340 --> 00:59:53.000 and a last argument. 00:59:53.000 --> 00:59:57.820 And then what I think I would do down here is ultimately have first = first, 00:59:57.820 --> 01:00:00.910 and then I would do the same thing for middle and last. 01:00:00.910 --> 01:00:05.560 So middle and middle, and then last and last. 01:00:05.560 --> 01:00:08.590 And then what I would have to do here is, 01:00:08.590 --> 01:00:11.200 when I actually ask the user for their name, 01:00:11.200 --> 01:00:12.730 I might need to really go all out. 01:00:12.730 --> 01:00:15.160 I might need to ask them first for their first name 01:00:15.160 --> 01:00:18.910 and store that in a variable called first, and therefore pass in first. 01:00:18.910 --> 01:00:21.820 I might similarly need to ask them for their middle name 01:00:21.820 --> 01:00:25.510 and store that in a variable and then pass in a second argument, middle. 01:00:25.510 --> 01:00:28.360 And then lastly, if you will, let me go ahead and create 01:00:28.360 --> 01:00:31.900 a third variable called last, get the input for their last name, 01:00:31.900 --> 01:00:34.030 and pass that in as well. 01:00:34.030 --> 01:00:38.140 I could instead just use one input and just ask them for their whole name. 01:00:38.140 --> 01:00:42.580 So type in David Malan, Enter, or David J. Malan-- all three of them, 01:00:42.580 --> 01:00:45.670 and maybe I could use Python's split function, 01:00:45.670 --> 01:00:47.588 maybe a regular expression to tease it apart. 01:00:47.588 --> 01:00:49.630 That's probably going to be messy because there's 01:00:49.630 --> 01:00:52.090 going to be people who don't have just two or three names. 01:00:52.090 --> 01:00:53.470 They might have four or five. 01:00:53.470 --> 01:00:55.960 So maybe sometimes it's better to have multiple prompts. 01:00:55.960 --> 01:00:58.300 But that's not a problem because, with a class, 01:00:58.300 --> 01:01:02.290 we have the expressiveness to take in more arguments if we want. 01:01:02.290 --> 01:01:04.040 We could even take a list if we wanted. 01:01:04.040 --> 01:01:06.915 But I think we'd probably want to have even more error checking then, 01:01:06.915 --> 01:01:11.470 not just for name but for first, and then maybe for middle, and then 01:01:11.470 --> 01:01:12.220 maybe for last. 01:01:12.220 --> 01:01:14.530 So it just is more and more code, though there would be 01:01:14.530 --> 01:01:17.030 ways to perhaps consolidate that, too. 01:01:17.030 --> 01:01:22.480 Let me undo all of that and see if there are other questions now on classes. 01:01:22.480 --> 01:01:24.310 AUDIENCE: I assume classes are something I 01:01:24.310 --> 01:01:25.935 might do at the beginning of a project. 01:01:25.935 --> 01:01:28.600 Can I just put them in a different file and import them 01:01:28.600 --> 01:01:32.065 into my project, or my main code as needed? 01:01:32.065 --> 01:01:33.190 DAVID J. MALAN: Absolutely. 01:01:33.190 --> 01:01:34.190 A really good question. 01:01:34.190 --> 01:01:36.065 You could imagine wanting to use this student 01:01:36.065 --> 01:01:40.660 class, not just in student.py but in other files or other projects of yours. 01:01:40.660 --> 01:01:44.440 And absolutely, you can create your own library of classes 01:01:44.440 --> 01:01:47.980 by putting the student class in your own module or package, 01:01:47.980 --> 01:01:50.590 per our discussion in the past about libraries more generally. 01:01:50.590 --> 01:01:52.240 And absolutely, you can do that. 01:01:52.240 --> 01:01:54.850 And later today, what we see is we've actually 01:01:54.850 --> 01:01:58.160 been using classes-- you and I-- before, in third party libraries. 01:01:58.160 --> 01:02:00.410 So you, too, can absolutely do the same. 01:02:00.410 --> 01:02:03.685 How about one more question on classes? 01:02:03.685 --> 01:02:06.550 AUDIENCE: Can you have optional variables in classes? 01:02:06.550 --> 01:02:09.670 And two, can you have your own error names, like-- 01:02:09.670 --> 01:02:13.240 let's be egotistical and say I want to raise Eric error? 01:02:13.240 --> 01:02:16.180 DAVID J. MALAN: Short answer, yes. 01:02:16.180 --> 01:02:18.760 These init functions are just like Python functions more 01:02:18.760 --> 01:02:21.010 generally, even though they're special in that they're 01:02:21.010 --> 01:02:23.682 going to get called automatically by Python for you. 01:02:23.682 --> 01:02:25.390 But if you wanted to make house optional, 01:02:25.390 --> 01:02:26.765 you could do something like this. 01:02:26.765 --> 01:02:31.780 You could give it a default value in the init function's signature 01:02:31.780 --> 01:02:34.270 so to speak-- in that first line of code on line two. 01:02:34.270 --> 01:02:36.885 And that would allow me to not have to pass in house. 01:02:36.885 --> 01:02:39.760 In this case, I'm going to continue to always pass in name and house, 01:02:39.760 --> 01:02:41.500 but you could make things optional. 01:02:41.500 --> 01:02:45.460 And yes, to your second question, if you wanted to have your own error message, 01:02:45.460 --> 01:02:50.812 like an Eric error, you could actually create your own Eric error exception. 01:02:50.812 --> 01:02:53.020 And we'll see, in a little bit, that there's actually 01:02:53.020 --> 01:02:58.060 a whole suite of exceptions that exist, and you, too, can invent those as well. 01:02:58.060 --> 01:03:01.390 Let me propose, though, that we now introduce 01:03:01.390 --> 01:03:06.250 one other aspect of this whereby we try printing out what a student looks like. 01:03:06.250 --> 01:03:08.920 At the moment, if I scroll back down to my main function, 01:03:08.920 --> 01:03:11.800 I'm still printing the student's name and house very manually. 01:03:11.800 --> 01:03:14.830 I'm going inside of the object, doing student.name, 01:03:14.830 --> 01:03:17.710 and I'm going inside of the object again and getting student.house, 01:03:17.710 --> 01:03:20.740 just to see where the student is from. 01:03:20.740 --> 01:03:23.650 But wouldn't it be nice if I could just print the student, 01:03:23.650 --> 01:03:25.570 like I've been printing for weeks-- 01:03:25.570 --> 01:03:29.470 any, int, or float, or str, or any other data type? 01:03:29.470 --> 01:03:32.500 Well, let's see what happens if I just try printing the student, 01:03:32.500 --> 01:03:36.167 instead of manually going inside and trying to create that sentence myself. 01:03:36.167 --> 01:03:39.250 Well, in my terminal window-- let me go ahead and run Python of student.py 01:03:39.250 --> 01:03:40.150 again. 01:03:40.150 --> 01:03:41.230 Let me type in Harry. 01:03:41.230 --> 01:03:42.550 Let me type in Gryffindor. 01:03:42.550 --> 01:03:50.350 And voila, Harry-- whoa, OK, main student object at 0x102733e80. 01:03:50.350 --> 01:03:51.910 Well, what is going on? 01:03:51.910 --> 01:03:53.702 Well, if you were to run the same code, you 01:03:53.702 --> 01:03:55.993 might actually see something different on your computer 01:03:55.993 --> 01:03:57.190 in terms of that number. 01:03:57.190 --> 01:04:01.960 But what you're really seeing is the underlying representation, as a string, 01:04:01.960 --> 01:04:03.640 of this specific object. 01:04:03.640 --> 01:04:06.880 In particular, you're seeing where in the computer's memory it is. 01:04:06.880 --> 01:04:12.730 This number, 30x102733e80, refers to, essentially, a specific location 01:04:12.730 --> 01:04:14.560 in the computer's memory or RAM. 01:04:14.560 --> 01:04:19.150 That's not really that interesting for me or you or, generally speaking, 01:04:19.150 --> 01:04:22.270 programmers, but it's just the default way of describing, 01:04:22.270 --> 01:04:25.780 via print, what this thing is. 01:04:25.780 --> 01:04:28.250 But I can override this as well. 01:04:28.250 --> 01:04:31.780 It turns out that there are other special methods in Python 01:04:31.780 --> 01:04:32.950 when it comes to classes-- 01:04:32.950 --> 01:04:36.370 not just underscore underscore, init, underscore underscore, 01:04:36.370 --> 01:04:40.540 but, continuing in that same pattern, underscore underscore, str, 01:04:40.540 --> 01:04:42.040 underscore underscore. 01:04:42.040 --> 01:04:47.020 So this, too, is a special method that, if you define it inside of your class, 01:04:47.020 --> 01:04:50.560 Python will just automatically call this function 01:04:50.560 --> 01:04:56.530 for you any time some other function wants to see your object as a string. 01:04:56.530 --> 01:04:59.770 Print wants to see your object as a string. 01:04:59.770 --> 01:05:02.950 But by default, if you don't have this method defined in your class, 01:05:02.950 --> 01:05:06.400 it's going to print out that very ugly esoteric incarnation thereof, 01:05:06.400 --> 01:05:10.420 where it says main__.Student object at 0x, dot, dot, dot. 01:05:10.420 --> 01:05:13.250 Well, how can I then define my own str function? 01:05:13.250 --> 01:05:17.050 Well, here, back in VS Code, let me propose that I go in 01:05:17.050 --> 01:05:23.410 and define not just __ init, but let me define a second function in this class 01:05:23.410 --> 01:05:25.030 here, as follows-- 01:05:25.030 --> 01:05:28.420 def __ str __. 01:05:28.420 --> 01:05:29.320 There are two. 01:05:29.320 --> 01:05:32.530 Even though the font in VS Code is putting the two underscore so close, 01:05:32.530 --> 01:05:34.360 it just looks like a longer underscore. 01:05:34.360 --> 01:05:37.570 There are indeed two there, on the left and the right just like for init. 01:05:37.570 --> 01:05:42.790 This one only takes one argument that, by convention, is always called self 01:05:42.790 --> 01:05:44.500 so that you have access to it. 01:05:44.500 --> 01:05:47.690 And then, indented below that after a colon, 01:05:47.690 --> 01:05:51.140 I'm going to go ahead and create a format string and return it. 01:05:51.140 --> 01:05:53.510 So let me go ahead and return-- 01:05:53.510 --> 01:05:56.390 how about something generic first like "a student." 01:05:56.390 --> 01:05:58.900 So I'm not going to bother even trying to figure out 01:05:58.900 --> 01:06:00.610 what this student's name or house is. 01:06:00.610 --> 01:06:02.890 I'm just going to always return "a student." 01:06:02.890 --> 01:06:08.650 Let me go back now to my earlier code, which has print (student) on line 16. 01:06:08.650 --> 01:06:13.030 Let me clear my terminal window and rerun Python of student.py, Enter. 01:06:13.030 --> 01:06:14.950 Type in Harry, type in Gryffindor. 01:06:14.950 --> 01:06:17.320 Last time, I saw that very cryptic output. 01:06:17.320 --> 01:06:20.940 This time, I see, more generically, "a student." 01:06:20.940 --> 01:06:23.390 More readable but not very enlightening. 01:06:23.390 --> 01:06:24.560 Which student is this? 01:06:24.560 --> 01:06:31.790 Well, notice that the double underscore str method takes in this self argument 01:06:31.790 --> 01:06:36.710 by default. It's just the way the Python authors designed this method. 01:06:36.710 --> 01:06:41.840 It will always be passed a reference to the current student object. 01:06:41.840 --> 01:06:43.020 What do I mean by that? 01:06:43.020 --> 01:06:45.920 When this line of code on line 6 is called, 01:06:45.920 --> 01:06:48.680 print, because it's hoping it's going to get a string, 01:06:48.680 --> 01:06:52.730 is going to trigger the underscore underscore, str, underscore 01:06:52.730 --> 01:06:54.470 underscore method to be called. 01:06:54.470 --> 01:06:58.430 And Python, for you, automatically is going to pass into that method 01:06:58.430 --> 01:07:01.220 a reference to the object that's trying to be 01:07:01.220 --> 01:07:05.280 printed so that you, the programmer, can do something like this. 01:07:05.280 --> 01:07:08.000 Here's an f string with double quotes as usual. 01:07:08.000 --> 01:07:10.460 I'm going to use some curly braces and say print out 01:07:10.460 --> 01:07:14.240 self.name from self.house. 01:07:14.240 --> 01:07:17.450 So there's nothing new in what I've just done. 01:07:17.450 --> 01:07:19.940 It's just an f string-- an f on the beginning, 01:07:19.940 --> 01:07:22.700 two double quotes, a couple of pairs of curly braces. 01:07:22.700 --> 01:07:28.760 But because, automatically, this str method gets passed self, so to speak, 01:07:28.760 --> 01:07:32.330 a reference to the current object, I can go inside of that object 01:07:32.330 --> 01:07:33.080 and grab the name. 01:07:33.080 --> 01:07:36.020 I can go inside that object again and grab the house. 01:07:36.020 --> 01:07:38.780 So now, when I go back to my terminal window-- 01:07:38.780 --> 01:07:41.030 previously it just printed out a student. 01:07:41.030 --> 01:07:44.420 But now, if I run Python of student.py, Enter-- 01:07:44.420 --> 01:07:48.080 type in Harry, type in Gryffindor, and one more time hit Enter, 01:07:48.080 --> 01:07:50.750 Harry is again from Gryffindor. 01:07:50.750 --> 01:07:52.490 But if I run this yet again-- 01:07:52.490 --> 01:07:57.110 let's, for instance, do Draco is from Slytherin, Enter. 01:07:57.110 --> 01:07:58.610 Draco's from Slytherin. 01:07:58.610 --> 01:08:04.280 Now it's customized to the specific object that we're trying to print. 01:08:04.280 --> 01:08:08.270 Questions on this function here-- 01:08:08.270 --> 01:08:11.675 this Dunder str method. 01:08:11.675 --> 01:08:14.540 AUDIENCE: Is there anything else that the underscore underscore, 01:08:14.540 --> 01:08:16.649 str method can do? 01:08:16.649 --> 01:08:19.399 The other question is, what's the difference between str and repr. 01:08:19.399 --> 01:08:20.732 DAVID J. MALAN: A good question. 01:08:20.732 --> 01:08:24.439 So there are many other methods that come with Python classes 01:08:24.439 --> 01:08:26.060 that start with underscore underscore. 01:08:26.060 --> 01:08:27.950 We're just scratching the surface, and we'll pretty much 01:08:27.950 --> 01:08:29.090 focus primarily on these. 01:08:29.090 --> 01:08:30.830 But yes, there are many others, and we'll 01:08:30.830 --> 01:08:33.649 see at least one other in just a little bit. 01:08:33.649 --> 01:08:37.370 Among the others is one called repr, which is 01:08:37.370 --> 01:08:39.590 a representation of the Python object. 01:08:39.590 --> 01:08:43.010 Generally speaking, the underscore underscore, repr, 01:08:43.010 --> 01:08:45.800 underscore underscore method is meant for developers' eyes. 01:08:45.800 --> 01:08:49.160 It typically has more information than "Harry from Gryffindor." 01:08:49.160 --> 01:08:53.090 It would also say what type of object it is, like a student, capital 01:08:53.090 --> 01:08:56.600 S, whereas underscore underscore, str, underscore underscore is generally 01:08:56.600 --> 01:08:58.130 meant for users-- 01:08:58.130 --> 01:09:01.500 the users of the program, and it's meant to be even more user-friendly. 01:09:01.500 --> 01:09:04.220 But both of those can be overridden as you see fit. 01:09:04.220 --> 01:09:08.060 Well, let me propose now that we pick up where we've left off on student 01:09:08.060 --> 01:09:11.960 and just add even more functionality, but not just these special methods 01:09:11.960 --> 01:09:15.170 like double underscore init and double underscore str. 01:09:15.170 --> 01:09:18.770 Let's create our own methods because therein lies the real power 01:09:18.770 --> 01:09:21.859 and flexibility of classes if you and I as the programmers 01:09:21.859 --> 01:09:25.290 can invent new functionality that's specific to students. 01:09:25.290 --> 01:09:29.120 For instance, students at Hogwarts, over the time in school, 01:09:29.120 --> 01:09:31.520 learn how to cast a certain type of spell. 01:09:31.520 --> 01:09:34.080 So when they say, Expecto Patronum, something 01:09:34.080 --> 01:09:36.080 comes out of their wand that typically resembles 01:09:36.080 --> 01:09:37.455 an animal or something like that. 01:09:37.455 --> 01:09:40.020 It's a special spell that they have to practice and practice. 01:09:40.020 --> 01:09:44.060 So let's see if we can't store, not just the student's name and their house, 01:09:44.060 --> 01:09:47.029 but also their "patronus," what, actually, they 01:09:47.029 --> 01:09:48.625 conjure when using this spell. 01:09:48.625 --> 01:09:50.750 Well, let me go ahead and clear my terminal window. 01:09:50.750 --> 01:09:56.600 And in the top of my code here, in the init method of Student, let me go ahead 01:09:56.600 --> 01:10:00.860 and start expecting a third argument, in addition to self, which automatically 01:10:00.860 --> 01:10:02.840 gets passed in, called patronus. 01:10:02.840 --> 01:10:06.140 And I'm not going to worry, for now, on validating 01:10:06.140 --> 01:10:10.040 the patronus from an official list of valid patronuses, or patroni. 01:10:10.040 --> 01:10:13.040 I'm instead going to go ahead and just blindly assign 01:10:13.040 --> 01:10:16.460 it to self.patronus = patronus, and we're 01:10:16.460 --> 01:10:19.070 going to let the user type whatever they want for now. 01:10:19.070 --> 01:10:21.350 But I could certainly add more error checking 01:10:21.350 --> 01:10:25.580 if I wanted to limit the patronus to a specific list of them here. 01:10:25.580 --> 01:10:29.000 Let me go ahead now and prompt the user for this patronus, 01:10:29.000 --> 01:10:33.650 as by-- in my get_student function-- defining a variable 01:10:33.650 --> 01:10:36.380 called patronus or anything else, prompting the user 01:10:36.380 --> 01:10:38.330 for input for their patronus. 01:10:38.330 --> 01:10:42.720 And now I'm going to go ahead and pass in that third variable here. 01:10:42.720 --> 01:10:46.037 So again, similar in spirit to just adding more and more attributes 01:10:46.037 --> 01:10:47.870 to the class, I'm going to pass in all three 01:10:47.870 --> 01:10:49.798 of these values instead of just two. 01:10:49.798 --> 01:10:52.340 I'm not going to do anything interesting with that value yet. 01:10:52.340 --> 01:10:55.580 But just to make sure I haven't made things worse by breaking my code, 01:10:55.580 --> 01:10:57.860 let me run Python of student.py. 01:10:57.860 --> 01:10:59.090 I'll type in Harry. 01:10:59.090 --> 01:11:00.410 I'll type in Gryffindor. 01:11:00.410 --> 01:11:02.510 And it turns out his patronus was a stag, . 01:11:02.510 --> 01:11:06.110 And hit Enter I haven't seen what his patronus is in my output 01:11:06.110 --> 01:11:08.690 because I didn't change my str method yet. 01:11:08.690 --> 01:11:10.620 But at least I don't have any syntax errors. 01:11:10.620 --> 01:11:12.950 So at least I've not made anything worse. 01:11:12.950 --> 01:11:16.400 But suppose, now, I want to have functionality, not just 01:11:16.400 --> 01:11:19.700 for initializing a student and printing out a student. 01:11:19.700 --> 01:11:23.680 If my class is really meant to be a student, what I can do 01:11:23.680 --> 01:11:27.430 is not just remember information about data about students. 01:11:27.430 --> 01:11:31.300 What's powerful about classes, unlike dictionaries alone, 01:11:31.300 --> 01:11:36.700 is that classes can have, not just variables or instance variables-- 01:11:36.700 --> 01:11:38.620 those attributes we keep creating. 01:11:38.620 --> 01:11:42.460 They can also have functions built in, a.k.a. 01:11:42.460 --> 01:11:43.100 methods. 01:11:43.100 --> 01:11:45.940 When a function is inside of a class, it's called a "method," 01:11:45.940 --> 01:11:47.680 but it's still just a function. 01:11:47.680 --> 01:11:51.010 At this point, we've seen two functions already-- two methods-- 01:11:51.010 --> 01:11:54.940 called a double underscore init and double underscore str, 01:11:54.940 --> 01:11:57.940 but those are special methods in that they just work. 01:11:57.940 --> 01:12:01.400 If you define them, Python calls them automatically for you. 01:12:01.400 --> 01:12:04.940 But what if you wanted to create more functionality for a student 01:12:04.940 --> 01:12:08.980 so that your class really represents this real world, or maybe "fantasy" 01:12:08.980 --> 01:12:13.360 world notion of a student, where students not only have names and houses 01:12:13.360 --> 01:12:16.390 and patronuses; they also have functionality. 01:12:16.390 --> 01:12:22.540 They have actions they can perform like casting a charm, a spell, magically. 01:12:22.540 --> 01:12:26.320 Could we implement, therefore, a function called charm, that 01:12:26.320 --> 01:12:30.280 actually uses their magical knowledge? 01:12:30.280 --> 01:12:33.800 Well, let's go ahead and define our very own function as follows. 01:12:33.800 --> 01:12:37.570 Let me clear my terminal window, scroll back up to my student class. 01:12:37.570 --> 01:12:40.930 And instead of creating yet another function that's special, 01:12:40.930 --> 01:12:45.430 with double underscores, I'm going to invent my own function, or method, 01:12:45.430 --> 01:12:46.900 inside of this class. 01:12:46.900 --> 01:12:51.520 I want to give Harry and Hermione and all of the other students 01:12:51.520 --> 01:12:55.000 the ability to cast charms, so I'm going to define a function that I 01:12:55.000 --> 01:12:57.290 can completely, on my own, call charm. 01:12:57.290 --> 01:12:59.110 I could call this function anything I want. 01:12:59.110 --> 01:13:01.720 But because it's a method inside of a class, 01:13:01.720 --> 01:13:06.070 the convention is that it's always going to take at least one argument, called 01:13:06.070 --> 01:13:11.000 self, by convention so that you have access to the current object, 01:13:11.000 --> 01:13:13.090 even if you don't plan to use it, per se. 01:13:13.090 --> 01:13:16.690 All right, let me go ahead and propose that we implement charm in such a way 01:13:16.690 --> 01:13:21.730 that the method returns an emoji that's appropriate for each student's 01:13:21.730 --> 01:13:23.140 patronus. 01:13:23.140 --> 01:13:25.750 How to implement this-- well, inside of the charm method, 01:13:25.750 --> 01:13:29.050 let's go ahead and match on self.patronus, 01:13:29.050 --> 01:13:31.420 which is the instance variable containing a string that 01:13:31.420 --> 01:13:33.460 represents each student's patronus. 01:13:33.460 --> 01:13:38.200 And in the case that it matches a stag, for instance, for Harry, let's go ahead 01:13:38.200 --> 01:13:40.330 and return maybe the closest emoji-- 01:13:40.330 --> 01:13:41.740 this horse here. 01:13:41.740 --> 01:13:44.620 How about in the case of an otter? 01:13:44.620 --> 01:13:49.690 Well, in that case, let's go ahead and return, oh, maybe the closest match 01:13:49.690 --> 01:13:52.570 to the otter, which might be this emoji here. 01:13:52.570 --> 01:13:57.020 And let's see, in the case of a-- for Ron, rather than Hermione-- 01:13:57.020 --> 01:14:02.800 a Jack Russell terrier, let's go ahead and return-- 01:14:02.800 --> 01:14:04.630 don't have as many options here. 01:14:04.630 --> 01:14:08.740 Why don't we go ahead and return the cutest available dog in that case. 01:14:08.740 --> 01:14:14.260 And in the case of no patronus recognized, 01:14:14.260 --> 01:14:16.360 as might cover someone like Draco, let's go ahead 01:14:16.360 --> 01:14:19.750 and use a default case using the underscore as in the past, 01:14:19.750 --> 01:14:22.792 and let's go ahead and return for this-- oh, what should happen 01:14:22.792 --> 01:14:24.250 if someone doesn't have a patronus? 01:14:24.250 --> 01:14:26.560 Why don't we just see a magical wand that 01:14:26.560 --> 01:14:28.955 seems to fizzle out, as in this case? 01:14:28.955 --> 01:14:31.330 All right, well, now, rather than just print the student, 01:14:31.330 --> 01:14:33.370 let's go about printing their actual patronus. 01:14:33.370 --> 01:14:36.010 So I'm going to go down to my main function here. 01:14:36.010 --> 01:14:39.250 I'm going to still get a student, using the get_student function. 01:14:39.250 --> 01:14:44.200 But rather than print student, let's go ahead and declare "Expecto Patronum!" 01:14:44.200 --> 01:14:46.810 printing out just that as pure text. 01:14:46.810 --> 01:14:50.770 And now let's go ahead and print out, not the student but, rather, 01:14:50.770 --> 01:14:54.400 the return value of their own charm method. 01:14:54.400 --> 01:14:58.600 So let me go back down to my terminal window and run Python of student.py 01:14:58.600 --> 01:14:59.710 and Enter. 01:14:59.710 --> 01:15:01.270 Name-- let's start with Harry. 01:15:01.270 --> 01:15:03.520 He lives in Gryffindor. 01:15:03.520 --> 01:15:05.080 Patronus is a stag. 01:15:05.080 --> 01:15:06.310 And let's see-- 01:15:06.310 --> 01:15:08.110 Expecto Patronum! 01:15:08.110 --> 01:15:10.710 And of course, we'd see the stag emoji. 01:15:10.710 --> 01:15:13.330 What about someone like Draco, who, at least in the books, 01:15:13.330 --> 01:15:15.327 doesn't have a known patronus? 01:15:15.327 --> 01:15:17.410 Well, let's go ahead and clear my terminal window, 01:15:17.410 --> 01:15:22.630 rerun Python of student.py, and this time, let's type in Draco for name, 01:15:22.630 --> 01:15:25.747 Slytherin for house, and Patronus is unknown. 01:15:25.747 --> 01:15:27.580 So I'm just going to go ahead and hit Enter. 01:15:27.580 --> 01:15:29.650 And now, Expecto Patronum! 01:15:29.650 --> 01:15:33.400 And it just of sizzles instead. 01:15:33.400 --> 01:15:39.960 Questions now on what I've done by implementing this charm method. 01:15:39.960 --> 01:15:43.275 AUDIENCE: Can we call a method outside the function? 01:15:43.275 --> 01:15:44.400 DAVID J. MALAN: Absolutely. 01:15:44.400 --> 01:15:45.660 And we're seeing that already. 01:15:45.660 --> 01:15:48.150 If I scroll back down to my main function 01:15:48.150 --> 01:15:49.980 which is outside of the class-- 01:15:49.980 --> 01:15:51.730 because it's all the way down in the file; 01:15:51.730 --> 01:15:55.110 it's all left aligned, just like the class, notice on line 28, 01:15:55.110 --> 01:15:57.600 I'm calling student.charm. 01:15:57.600 --> 01:16:02.010 And I'm accessing a method that's inside of the student object, 01:16:02.010 --> 01:16:05.950 even though my main function is outside of the class. 01:16:05.950 --> 01:16:12.370 And what's different between our init function, or method, and our str method 01:16:12.370 --> 01:16:13.480 that we've seen before-- 01:16:13.480 --> 01:16:17.260 those are called automatically when you try to create a student 01:16:17.260 --> 01:16:18.790 or when you try to print a student. 01:16:18.790 --> 01:16:21.130 For the first time, we're now seeing a method 01:16:21.130 --> 01:16:26.080 that I invented inside of the student class that I can call anywhere I want. 01:16:26.080 --> 01:16:28.690 And if I'm calling it outside of the class like I am here, 01:16:28.690 --> 01:16:31.090 I just use the name of the variable containing 01:16:31.090 --> 01:16:33.760 that student object, and I just do dot charm, 01:16:33.760 --> 01:16:38.800 just like I could access the individual attributes or instance variables 01:16:38.800 --> 01:16:41.300 inside of that object as well. 01:16:41.300 --> 01:16:46.015 Other questions on classes and this new method, charm? 01:16:46.015 --> 01:16:49.960 AUDIENCE: In the parentheses, you put self. 01:16:49.960 --> 01:16:51.550 It's supposed to be an initializer. 01:16:51.550 --> 01:16:56.740 But in the top part, you didn't define it the same way. 01:16:56.740 --> 01:17:00.482 Would you have to put the class on-- why you didn't put the class right there? 01:17:00.482 --> 01:17:02.440 DAVID J. MALAN: So notice the indentation here. 01:17:02.440 --> 01:17:06.490 So on line 1, I've of course said class Student colon, and then everything 01:17:06.490 --> 01:17:08.260 below that is indented at the moment. 01:17:08.260 --> 01:17:12.550 That means that my init method, my str, method and this new charm method 01:17:12.550 --> 01:17:15.760 are all inside of part of that class. 01:17:15.760 --> 01:17:19.390 By convention then, each of these methods inside of a class 01:17:19.390 --> 01:17:22.930 should expect that Python will automatically 01:17:22.930 --> 01:17:26.740 pass in at least one argument to every method in a class, 01:17:26.740 --> 01:17:31.240 and that argument will always be a reference to the current object, 01:17:31.240 --> 01:17:34.990 the Harry object, or the Draco object that is currently 01:17:34.990 --> 01:17:37.640 trying to cast a charm or be printed. 01:17:37.640 --> 01:17:40.900 So whenever you create a method inside of a class 01:17:40.900 --> 01:17:44.380 you should always have it take at least one argument called self 01:17:44.380 --> 01:17:48.700 and then zero or more other arguments that are entirely up to you. init, 01:17:48.700 --> 01:17:51.970 for instance takes three more. name, house, patronus, str 01:17:51.970 --> 01:17:54.820 takes zero more charm, takes zero more. 01:17:54.820 --> 01:17:58.420 But the convention is to always call that default argument "self," 01:17:58.420 --> 01:18:04.230 and Python just automatically gives you access to the current object. 01:18:04.230 --> 01:18:05.400 Other questions? 01:18:05.400 --> 01:18:08.610 How about one more, on classes and charms? 01:18:08.610 --> 01:18:13.140 AUDIENCE: Sir, I just wanted to ask that you have put them all in double quotes. 01:18:13.140 --> 01:18:17.820 So a does it take the emojis as a form of strings or something else? 01:18:17.820 --> 01:18:21.570 DAVID J. MALAN: Yes, if unfamiliar, an emoji is just a character. 01:18:21.570 --> 01:18:25.830 It's part of a mapping of numbers to letters known as Unicode. 01:18:25.830 --> 01:18:29.310 And so whenever you see these emoji on the screen, 01:18:29.310 --> 01:18:32.100 like the horse or the otter or the dog here, 01:18:32.100 --> 01:18:35.280 specifically, that's really just a key from your keyboard. 01:18:35.280 --> 01:18:37.200 You and I on Macs and PCs can't typically 01:18:37.200 --> 01:18:39.060 type it because you see English characters 01:18:39.060 --> 01:18:40.530 or some other human language. 01:18:40.530 --> 01:18:46.470 But emoji are increasingly available via menus and dropdown and, in my case, 01:18:46.470 --> 01:18:47.220 Copy Paste. 01:18:47.220 --> 01:18:50.037 On Macs and PCs and Android devices and iPhones, 01:18:50.037 --> 01:18:52.620 they are just characters, even though they look like pictures. 01:18:52.620 --> 01:18:55.050 You can think of it like a graphical font almost. 01:18:55.050 --> 01:18:55.990 So they're just text. 01:18:55.990 --> 01:18:58.740 And indeed, if you put them between double quotes or single quotes 01:18:58.740 --> 01:19:04.140 you can print emoji just like you can any other human character as well. 01:19:04.140 --> 01:19:09.420 Well, let me propose, now, that we remove this patronus code just 01:19:09.420 --> 01:19:13.050 to simplify our world and focus on some of the other core 01:19:13.050 --> 01:19:14.420 capabilities of classes. 01:19:14.420 --> 01:19:16.170 So at the risk of disappointing, I'm going 01:19:16.170 --> 01:19:19.110 to get rid of all of these beautiful emoji and charms, 01:19:19.110 --> 01:19:23.070 and I'm going to go ahead and stop asking the user now for their patronus. 01:19:23.070 --> 01:19:25.920 And I'm going to stop passing it into init here. 01:19:25.920 --> 01:19:28.770 And I'm going to stop doing this here. 01:19:28.770 --> 01:19:34.530 And I'm going to instead just go ahead and restore our use of print student 01:19:34.530 --> 01:19:37.770 here, and I'm going to go ahead and get rid of patronus down here. 01:19:37.770 --> 01:19:41.740 So just essentially undo all of the fun charms we just created. 01:19:41.740 --> 01:19:47.520 So we're now back at the point in the story where we have a student class, 01:19:47.520 --> 01:19:49.230 with only two methods-- 01:19:49.230 --> 01:19:50.640 init and str. 01:19:50.640 --> 01:19:52.590 The first of those takes, of course, self 01:19:52.590 --> 01:19:56.340 as the first argument as it always will, plus two more now-- name and house, 01:19:56.340 --> 01:19:57.420 no more patronus. 01:19:57.420 --> 01:19:59.250 We're validating name up here. 01:19:59.250 --> 01:20:01.050 We're validating house down here. 01:20:01.050 --> 01:20:03.570 And then we're assigning name and house, respectively, 01:20:03.570 --> 01:20:06.840 to two instance variables called name and house also. 01:20:06.840 --> 01:20:10.080 But we use self to get access to the current object, 01:20:10.080 --> 01:20:11.970 to store those values therein. 01:20:11.970 --> 01:20:15.810 We then still have our str method here, which takes one argument-- 01:20:15.810 --> 01:20:17.430 by default, self, and that's it. 01:20:17.430 --> 01:20:20.940 And that function is going to be called automatically any time 01:20:20.940 --> 01:20:23.820 you want to convert a student object to a string, 01:20:23.820 --> 01:20:26.313 just like print might want to do here. 01:20:26.313 --> 01:20:28.980 So let me go ahead and just make sure I haven't broken anything. 01:20:28.980 --> 01:20:30.960 Let me run Python of student.py. 01:20:30.960 --> 01:20:32.130 I'll type in Harry. 01:20:32.130 --> 01:20:34.260 I'll type in Gryffindor, Enter. 01:20:34.260 --> 01:20:35.640 OK, we're back in business. 01:20:35.640 --> 01:20:39.390 Gone are the charms and patronus, but at least I'm back to a situation 01:20:39.390 --> 01:20:41.440 where I have names and houses. 01:20:41.440 --> 01:20:45.630 But it turns out, at the moment, our use of classes 01:20:45.630 --> 01:20:51.300 is not very robust, even though we have this mechanism, very cleverly, 01:20:51.300 --> 01:20:54.240 if I may, in our init method of making sure 01:20:54.240 --> 01:20:58.320 that we're validating name and house, making sure that name is not blank, 01:20:58.320 --> 01:21:03.840 and making sure that house is a valid house among those four Hogwarts houses. 01:21:03.840 --> 01:21:06.420 It turns out that classes will still let me 01:21:06.420 --> 01:21:09.750 get at those attributes, those so-called instance variables, 01:21:09.750 --> 01:21:11.940 using dot notation anyway. 01:21:11.940 --> 01:21:15.720 Let me scroll down then and try to do this a little adversarially. 01:21:15.720 --> 01:21:19.720 Suppose that online 16 I go ahead and call get_student, 01:21:19.720 --> 01:21:23.760 which exists as before, and then I store the return value in a student 01:21:23.760 --> 01:21:26.010 variable-- again, on line 16. 01:21:26.010 --> 01:21:31.500 That will ensure that get_student gets called, which calls input and input. 01:21:31.500 --> 01:21:34.140 And then it calls the student constructor, 01:21:34.140 --> 01:21:36.730 which invokes, automatically, this init method. 01:21:36.730 --> 01:21:39.660 So by way of how we've laid out my code, we're 01:21:39.660 --> 01:21:42.960 going to ensure that name is not blank and house is definitely 01:21:42.960 --> 01:21:44.130 one of those four values. 01:21:44.130 --> 01:21:47.770 My error correction-- or error checking is in place. 01:21:47.770 --> 01:21:51.910 But if I'm a little adversarial, I can still circumvent it. 01:21:51.910 --> 01:21:56.760 Suppose that-- fine, going to require me to type in Harry and Gryffindor? 01:21:56.760 --> 01:21:59.310 I'm going to go ahead and type in student.house 01:21:59.310 --> 01:22:02.768 equals, quote, unquote, "Number Four, Privet Drive," 01:22:02.768 --> 01:22:04.560 and you're not going to be able to stop me. 01:22:04.560 --> 01:22:05.310 Why? 01:22:05.310 --> 01:22:07.920 Well, it turns out, with classes and objects 01:22:07.920 --> 01:22:11.580 thereof, you and I can still access those instance variables 01:22:11.580 --> 01:22:13.290 using this familiar dot notation. 01:22:13.290 --> 01:22:16.320 That's how we began the story of classes-- just setting these attributes 01:22:16.320 --> 01:22:17.070 ourselves. 01:22:17.070 --> 01:22:19.770 But you can also read these attributes themselves 01:22:19.770 --> 01:22:21.960 and change them later if you want. 01:22:21.960 --> 01:22:27.060 And this will effectively circumvent the if condition and the other if condition 01:22:27.060 --> 01:22:31.200 in our init method because that is only called when you first 01:22:31.200 --> 01:22:33.330 create the student object. 01:22:33.330 --> 01:22:35.760 There's nothing stopping me, at the moment, 01:22:35.760 --> 01:22:39.520 from just changing the house or the name after. 01:22:39.520 --> 01:22:43.770 So if I now clear my terminal window and run Python of student.py, 01:22:43.770 --> 01:22:47.430 I'll still type in Harry and Gryffindor to meet my requirements 01:22:47.430 --> 01:22:49.110 that the house be one of those four. 01:22:49.110 --> 01:22:52.600 But when it's printed, notice, I've still overridden it. 01:22:52.600 --> 01:22:57.300 So it seems that, while classes do allow us a little more control over the data 01:22:57.300 --> 01:23:01.440 we're storing, it doesn't necessarily prevent the user-- 01:23:01.440 --> 01:23:04.820 or rather the programmer-- be it myself or maybe a colleague, 01:23:04.820 --> 01:23:07.010 from still messing things up. 01:23:07.010 --> 01:23:10.310 So here, too, in the spirit of programming a little more 01:23:10.310 --> 01:23:15.140 defensively, allow me to introduce another feature of Python as well-- 01:23:15.140 --> 01:23:17.400 namely properties. 01:23:17.400 --> 01:23:20.570 So a property is really just an attribute 01:23:20.570 --> 01:23:23.960 that has even more defense mechanisms put into place, 01:23:23.960 --> 01:23:29.420 a little more functionality implemented by you to prevent programmers, like me 01:23:29.420 --> 01:23:32.280 and you, from messing things up like these attributes. 01:23:32.280 --> 01:23:35.570 So again, a property is going to be an attribute that you and I just 01:23:35.570 --> 01:23:37.280 have more control over. 01:23:37.280 --> 01:23:37.940 How? 01:23:37.940 --> 01:23:41.000 We just write a little more code, using some Python conventions. 01:23:41.000 --> 01:23:45.560 And how we're going to do that is going to use, in just a moment, a feature-- 01:23:45.560 --> 01:23:49.310 a keyword known as @property, which is technically a function. 01:23:49.310 --> 01:23:51.110 Property is a function in Python. 01:23:51.110 --> 01:23:55.987 But we're about to see some new @ syntax that allows you to decorate functions. 01:23:55.987 --> 01:23:57.320 And this, too, is a term of art. 01:23:57.320 --> 01:24:00.110 In the world of Python, you can have decorators, 01:24:00.110 --> 01:24:04.380 which are functions that modify the behavior of other functions, 01:24:04.380 --> 01:24:07.910 if you will, and we'll leave it at that without going too much into the weeds. 01:24:07.910 --> 01:24:10.490 And we'll see, by, example how you can use these decorators, 01:24:10.490 --> 01:24:12.750 specifically to define properties. 01:24:12.750 --> 01:24:14.930 So let me go back to VS Code here. 01:24:14.930 --> 01:24:18.020 And let me propose that I do this. 01:24:18.020 --> 01:24:20.600 I'm going to go ahead and create-- 01:24:20.600 --> 01:24:25.520 how about a property called house as follows. 01:24:25.520 --> 01:24:32.030 Inside of my student class, I'm going to go ahead-- and below my init method 01:24:32.030 --> 01:24:35.180 and below my str method, I'm going to go ahead and define 01:24:35.180 --> 01:24:40.220 a function called house that takes, as it always must, one argument at least, 01:24:40.220 --> 01:24:41.270 called self. 01:24:41.270 --> 01:24:46.680 And what I'm going to do now is return self.house. 01:24:46.680 --> 01:24:48.830 So I'm just going to define a method called 01:24:48.830 --> 01:24:54.590 house, whose sole purpose in life is to return the value of house. 01:24:54.590 --> 01:24:58.580 But I'm going to define one other method, curiously also called house, 01:24:58.580 --> 01:25:02.600 but that's going to take into, as arguments, two values-- 01:25:02.600 --> 01:25:06.830 self as always and also a value called house. 01:25:06.830 --> 01:25:08.495 And I'm going to now do this. 01:25:11.600 --> 01:25:15.170 I'm going to do self.house = house. 01:25:15.170 --> 01:25:16.460 Now, what have I done? 01:25:16.460 --> 01:25:18.830 Well, let me just temporarily add some comments here. 01:25:18.830 --> 01:25:21.380 In a moment, we're going to start referring to this generally 01:25:21.380 --> 01:25:22.310 as a getter. 01:25:22.310 --> 01:25:25.310 And down here, I'm going to refer to this as a setter. 01:25:25.310 --> 01:25:28.070 And this is terminology frequently see in the world of Java. 01:25:28.070 --> 01:25:29.990 Some of you have programmed in Java before. 01:25:29.990 --> 01:25:33.080 But as the names imply, a getter is a function 01:25:33.080 --> 01:25:36.530 for a class that gets some attributes. 01:25:36.530 --> 01:25:41.480 A setter is a function in some class that sets some value. 01:25:41.480 --> 01:25:45.350 And now, even though we're not done, and there's a bit of a mistake in the code 01:25:45.350 --> 01:25:49.110 I've already written, intuitively, what we're going to do is this. 01:25:49.110 --> 01:25:52.190 We're trying to prevent programmers, myself included, 01:25:52.190 --> 01:25:54.770 from circumventing my error checking that I 01:25:54.770 --> 01:25:56.870 put into place for name and house. 01:25:56.870 --> 01:25:58.168 How can I do that? 01:25:58.168 --> 01:26:00.710 Well, we don't have that many building blocks in programming. 01:26:00.710 --> 01:26:04.760 We have things like variables for data, and we have functions for actions. 01:26:04.760 --> 01:26:06.210 Well, why don't we do this? 01:26:06.210 --> 01:26:11.150 Why don't we somehow require that, in order to access an attribute, 01:26:11.150 --> 01:26:12.590 you go through some function. 01:26:12.590 --> 01:26:15.800 And let's require that, in order to set some attribute, 01:26:15.800 --> 01:26:17.510 you go through some function. 01:26:17.510 --> 01:26:23.150 And conventionally, those functions are called a getter function and a setter 01:26:23.150 --> 01:26:23.900 function. 01:26:23.900 --> 01:26:27.740 And why are we using functions or, in this case, methods inside of a class? 01:26:27.740 --> 01:26:31.220 Well, once you have functions, those are just actions or verbs 01:26:31.220 --> 01:26:32.780 that you and I can create ourselves. 01:26:32.780 --> 01:26:36.410 We can put any error correction I want in these functions 01:26:36.410 --> 01:26:39.620 because it's code that's going to get executed top to bottom. 01:26:39.620 --> 01:26:46.760 So how can I now prevent the user from setting the house to an invalid value? 01:26:46.760 --> 01:26:51.710 Let me borrow some logic from before rather than blindly do this-- just set 01:26:51.710 --> 01:26:56.510 self.house equal to the house value that's passed in-- let's add our error 01:26:56.510 --> 01:26:57.390 checking there. 01:26:57.390 --> 01:27:05.990 So if house is not in the following list of Gryffindor or Hufflepuff 01:27:05.990 --> 01:27:11.900 or Ravenclaw or Slytherin, just as before, 01:27:11.900 --> 01:27:15.770 let's go ahead and raise a value error, just to signify that, uh-uh, something 01:27:15.770 --> 01:27:16.520 has gone wrong. 01:27:16.520 --> 01:27:17.540 I'll be more explicit. 01:27:17.540 --> 01:27:21.740 I'll include a message like, "invalid house," quote, unquote. 01:27:21.740 --> 01:27:27.380 Otherwise, I'm going to proceed on, now, line 21 to set self.house to house. 01:27:27.380 --> 01:27:31.640 So I've just copied, if you will, or retyped my error checking inside 01:27:31.640 --> 01:27:33.530 of this so-called setter function. 01:27:33.530 --> 01:27:34.880 Now, why have I done that? 01:27:34.880 --> 01:27:38.660 Well, to be clear, whenever the user or the programmer 01:27:38.660 --> 01:27:43.580 writes code like this, student.house equals, what's 01:27:43.580 --> 01:27:47.390 about to happen magically is Python will not just 01:27:47.390 --> 01:27:51.140 let the programmer access student house directly-- that attribute, 01:27:51.140 --> 01:27:52.940 that instance variable, a.k.a. 01:27:52.940 --> 01:27:54.080 self house. 01:27:54.080 --> 01:27:58.040 It's instead going to magically automatically call 01:27:58.040 --> 01:28:00.020 this setter function for me. 01:28:00.020 --> 01:28:01.920 How does Python know to do that? 01:28:01.920 --> 01:28:06.860 Well, if it's see that, on the left-hand side, there is self.house, 01:28:06.860 --> 01:28:13.340 where house is the name of the getter or setter, and then it sees an equal sign, 01:28:13.340 --> 01:28:16.817 indicating assignment, that's just enough of a visual clue to say, 01:28:16.817 --> 01:28:17.400 wait a minute. 01:28:17.400 --> 01:28:20.450 I'm not going to let you access that attribute directly. 01:28:20.450 --> 01:28:23.010 I'm going to use the setter instead. 01:28:23.010 --> 01:28:23.510 Why? 01:28:23.510 --> 01:28:25.490 Because the equal sign means I'm trying to set. 01:28:25.490 --> 01:28:29.460 I'm trying to assign a value from right to left into that attribute. 01:28:29.460 --> 01:28:31.700 So what Python's is going to do automatically 01:28:31.700 --> 01:28:33.290 is call this function for me. 01:28:33.290 --> 01:28:37.910 And that's amazing because now I can execute code-- an algorithm to check, 01:28:37.910 --> 01:28:39.830 do I want to let the user-- 01:28:39.830 --> 01:28:43.250 the programmer set that attribute to that value? 01:28:43.250 --> 01:28:45.143 If not, I'm going to raise a value error, 01:28:45.143 --> 01:28:47.060 and you're just not going to be able to do it. 01:28:47.060 --> 01:28:48.690 If so fine. 01:28:48.690 --> 01:28:50.610 I'll go ahead and set it for you. 01:28:50.610 --> 01:28:53.180 But in order to do this, we need a little more syntax. 01:28:53.180 --> 01:28:56.600 And I'm going to get rid of my comment, and I'm going to use that decorator. 01:28:56.600 --> 01:29:00.800 I need to tell Python to treat this method as a getter. 01:29:00.800 --> 01:29:03.290 And then the syntax for the setter is a little different. 01:29:03.290 --> 01:29:05.690 You now say house.setter. 01:29:05.690 --> 01:29:08.720 I wish one was getter and the other was setter. 01:29:08.720 --> 01:29:10.670 That's not the way they designed it. 01:29:10.670 --> 01:29:15.500 When you want to define a getter, you just say @property above the function. 01:29:15.500 --> 01:29:20.060 And you name the function exactly like you would like the property 01:29:20.060 --> 01:29:22.220 to be called-- quote, unquote, "house." 01:29:22.220 --> 01:29:25.640 Once you do that, you can now use a new decorator 01:29:25.640 --> 01:29:28.820 that's automatically created for you called @house, 01:29:28.820 --> 01:29:29.960 because I called it house. 01:29:29.960 --> 01:29:32.630 And then you literally say, @house.setter. 01:29:32.630 --> 01:29:36.530 And this whole line, on line 17, is a clue to Python 01:29:36.530 --> 01:29:39.950 that here comes a function, whose name is identical-- 01:29:39.950 --> 01:29:43.730 but notice that it takes two arguments-- both self, so you have access 01:29:43.730 --> 01:29:46.430 to the contents of the object, and house, which is just 01:29:46.430 --> 01:29:51.230 going to be a str that comes from the programmer from the human input return 01:29:51.230 --> 01:29:55.160 value so that you can set that value as well. 01:29:55.160 --> 01:29:58.580 But there's one fix I need to make now, here. 01:29:58.580 --> 01:30:00.840 Everything else, I think, is still good. 01:30:00.840 --> 01:30:02.450 However watch this. 01:30:02.450 --> 01:30:05.700 I no longer need this error check here. 01:30:05.700 --> 01:30:06.530 Why? 01:30:06.530 --> 01:30:10.580 Because, if I scroll back down to my code here, 01:30:10.580 --> 01:30:17.450 I claimed a moment ago that code like this, with student.house equals, 01:30:17.450 --> 01:30:20.870 is going to automatically get Python to call my setter for me. 01:30:20.870 --> 01:30:21.860 Guess what? 01:30:21.860 --> 01:30:27.080 Even up here, in my init method, calling self.house equals 01:30:27.080 --> 01:30:31.430 is also going to call my setter method, which is amazing 01:30:31.430 --> 01:30:37.310 because now I can keep all of my error checking in one place in the setter, 01:30:37.310 --> 01:30:42.050 and it will now get called either when I create the object for the first time, 01:30:42.050 --> 01:30:45.770 because of init, or even if the programmer 01:30:45.770 --> 01:30:50.570 tries to circumvent that init method and change the value of this attribute, 01:30:50.570 --> 01:30:52.250 my setter will also get called. 01:30:52.250 --> 01:30:57.200 My setter will get called any time I access .house. 01:30:57.200 --> 01:30:59.660 But there's one fix I need to make. 01:30:59.660 --> 01:31:04.160 Unfortunately, I have collided names. 01:31:04.160 --> 01:31:09.680 Right now, if we go up here, on line 5, this is an instance variable. 01:31:09.680 --> 01:31:13.550 It's a string inside of my self, inside of the current student object, 01:31:13.550 --> 01:31:14.300 called name. 01:31:14.300 --> 01:31:18.170 And this is another instance variable called house. 01:31:18.170 --> 01:31:22.520 Unfortunately, if I have an instance variable called name and house, 01:31:22.520 --> 01:31:25.970 I cannot also have functions called house. 01:31:25.970 --> 01:31:27.140 They're going to collide. 01:31:27.140 --> 01:31:28.260 You've got to decide. 01:31:28.260 --> 01:31:30.470 Do you want the variable to be called house? 01:31:30.470 --> 01:31:32.930 Or do you want the function to be called house? 01:31:32.930 --> 01:31:35.420 Unfortunately, you can't have both because now Python 01:31:35.420 --> 01:31:37.250 is going to confuse one for the other. 01:31:37.250 --> 01:31:40.490 So the conventional fix for this is to do this-- 01:31:40.490 --> 01:31:46.460 to have the setter not store the value that's passed in self.house, 01:31:46.460 --> 01:31:50.330 but to use an almost identical name, but to use a little indicator that 01:31:50.330 --> 01:31:52.430 means you know doing this correctly. 01:31:52.430 --> 01:31:54.890 You typically, by convention, put an underscore 01:31:54.890 --> 01:31:58.250 in front of the instance variable's name. 01:31:58.250 --> 01:32:02.400 And when you return it up here, you similarly put an underscore. 01:32:02.400 --> 01:32:07.730 So now, technically, my instance variable is called _house, 01:32:07.730 --> 01:32:12.990 but my property, which is a fancier attribute, if you will, 01:32:12.990 --> 01:32:16.130 is called house alone. 01:32:16.130 --> 01:32:19.860 Huge amount of syntax, I know, but it's a very powerful feature. 01:32:19.860 --> 01:32:23.930 And again, this is why you can graduate from dictionaries alone and have 01:32:23.930 --> 01:32:26.720 so much more functionality at your disposal. 01:32:26.720 --> 01:32:30.110 Let me go ahead and clear my terminal window and run Python of student.py, 01:32:30.110 --> 01:32:31.670 Enter, name. 01:32:31.670 --> 01:32:33.920 All right, let's go ahead and type in Harry. 01:32:33.920 --> 01:32:35.840 Let's go ahead and type in Gryffindor. 01:32:35.840 --> 01:32:37.610 Crossing my fingers as always. 01:32:37.610 --> 01:32:40.280 And now, look, "Invalid house." 01:32:40.280 --> 01:32:41.460 This is a good thing. 01:32:41.460 --> 01:32:41.960 Why? 01:32:41.960 --> 01:32:45.170 Because, notice, in my main function, I'm 01:32:45.170 --> 01:32:50.330 still trying, maliciously, if you will, to change Harry's house 01:32:50.330 --> 01:32:52.190 to not be one of the four valid ones. 01:32:52.190 --> 01:32:56.030 I'm trying to change it to his childhood home of Number Four, Privet Drive. 01:32:56.030 --> 01:33:00.410 But because Python knows that, wait a minute, you're trying to assign-- 01:33:00.410 --> 01:33:01.910 that is, set a value-- 01:33:01.910 --> 01:33:05.420 and that value, a.k.a. house, is now defined 01:33:05.420 --> 01:33:09.560 as a property you're going to have to go through the setter function instead 01:33:09.560 --> 01:33:12.170 to even let you change that value. 01:33:12.170 --> 01:33:15.410 And because I have this raise ValueError. 01:33:15.410 --> 01:33:18.110 If the house is not as intended, you're not 01:33:18.110 --> 01:33:20.670 going to be allowed to change it to an invalid value. 01:33:20.670 --> 01:33:23.660 So I'm protecting the data on the way in, through the init method, 01:33:23.660 --> 01:33:27.900 and I'm even defending the data if you try to override it there. 01:33:27.900 --> 01:33:30.410 So I think the only solution for me, the programmer, 01:33:30.410 --> 01:33:32.450 is, don't try to break my own code. 01:33:32.450 --> 01:33:35.280 Let me remove that line because it's just not going to work. 01:33:35.280 --> 01:33:38.450 Let me run Python of student.py and, again, type in Harry; 01:33:38.450 --> 01:33:42.110 type in Gryffindor, Enter, and Harry's indeed from Gryffindor. 01:33:42.110 --> 01:33:46.910 If I did something incorrect, like Harry from Number Four, 01:33:46.910 --> 01:33:51.740 Privet Drive, Enter, we're again going to see the value error 01:33:51.740 --> 01:33:56.300 because my code just doesn't let that value in via manual input now 01:33:56.300 --> 01:33:59.740 or via that adversarial change. 01:33:59.740 --> 01:34:01.160 All right, that was a lot. 01:34:01.160 --> 01:34:06.413 But any question on properties? 01:34:06.413 --> 01:34:08.330 AUDIENCE: Why we are using getter then setter? 01:34:08.330 --> 01:34:12.770 It's just for the purpose so that we can find 01:34:12.770 --> 01:34:15.008 that method, that function in our code. 01:34:15.008 --> 01:34:18.050 DAVID J. MALAN: The reason that I'm going through the trouble of defining 01:34:18.050 --> 01:34:23.600 this getter or setter is because I want to make sure that programmers cannot do 01:34:23.600 --> 01:34:24.440 things like this. 01:34:24.440 --> 01:34:26.720 If I'm going through the trouble of validating 01:34:26.720 --> 01:34:29.930 the attributes for these student objects, 01:34:29.930 --> 01:34:33.230 I don't want you to be able to go in there and just change them at will. 01:34:33.230 --> 01:34:35.820 I want to have some control over that object 01:34:35.820 --> 01:34:38.910 so that you can just trust that it's going to be correct as designed. 01:34:38.910 --> 01:34:42.230 So using a getter and setter really just enables 01:34:42.230 --> 01:34:46.640 Python to automatically detect when you're trying to manually set a value. 01:34:46.640 --> 01:34:49.527 The equal sign and the dot, as I've highlighted here, 01:34:49.527 --> 01:34:51.860 is enough of a clue to Python to realize, wait a minute, 01:34:51.860 --> 01:34:53.120 you're trying to set a value. 01:34:53.120 --> 01:34:55.945 Let me see if this class has a setter defined. 01:34:55.945 --> 01:34:58.070 And if so, I'm going to call that, and I'm not just 01:34:58.070 --> 01:35:01.350 going to blindly assign the value from right to left. 01:35:01.350 --> 01:35:03.770 So it's just giving me more control. 01:35:03.770 --> 01:35:06.245 Other questions on properties. 01:35:06.245 --> 01:35:10.670 AUDIENCE: When we use getters, we just have just one argument. 01:35:10.670 --> 01:35:14.510 And if we use setters, it's always going to be two arguments? 01:35:14.510 --> 01:35:15.520 Is that normal? 01:35:15.520 --> 01:35:16.520 DAVID J. MALAN: Correct. 01:35:16.520 --> 01:35:19.040 It's always going to be one argument-- self 01:35:19.040 --> 01:35:23.840 for the getter, two arguments for the setter-- self and something else. 01:35:23.840 --> 01:35:26.600 And the intuition for that is, if you're getting a value, 01:35:26.600 --> 01:35:30.080 you don't need to pass anything else in because you already know the object. 01:35:30.080 --> 01:35:31.520 It's called student in this case. 01:35:31.520 --> 01:35:33.990 So you're just going to get the value of that property. 01:35:33.990 --> 01:35:36.680 But if you want to set the property to something else, 01:35:36.680 --> 01:35:38.320 you've got to pass in that argument. 01:35:38.320 --> 01:35:40.820 You've got to pass in the value to which you want to set it. 01:35:40.820 --> 01:35:42.500 So it's always 0 or 1. 01:35:42.500 --> 01:35:48.740 However, you see it as 1 or 2 because, again, any function inside of a class, 01:35:48.740 --> 01:35:49.250 a.k.a. 01:35:49.250 --> 01:35:53.480 a method, is going to be automatically passed self so that you have access 01:35:53.480 --> 01:35:56.400 to that current object in memory. 01:35:56.400 --> 01:35:58.925 How about one other question on properties? 01:35:58.925 --> 01:36:02.987 AUDIENCE: Why didn't we use the same underscore house init method? 01:36:02.987 --> 01:36:04.320 DAVID J. MALAN: A good question. 01:36:04.320 --> 01:36:07.970 So even though I'm using the underscore house here, in my setter, 01:36:07.970 --> 01:36:10.340 and the underscore house here, in my getter, 01:36:10.340 --> 01:36:13.170 I deliberately did not use it up here. 01:36:13.170 --> 01:36:18.560 The reason for that is that, by using self.house and this equal sign, 01:36:18.560 --> 01:36:21.590 that's the same pattern that I want Python to recognize. 01:36:21.590 --> 01:36:24.410 I want Python to automatically call the setter, 01:36:24.410 --> 01:36:28.010 even when I'm passing in the house via the init method. 01:36:28.010 --> 01:36:32.660 If I were to change this to do this, that would circumvent the setter, 01:36:32.660 --> 01:36:36.270 and now there's no error checking in init whatsoever. 01:36:36.270 --> 01:36:37.550 So it's such a fine line. 01:36:37.550 --> 01:36:41.270 The only thing standing between us and error checking or no error checking 01:36:41.270 --> 01:36:43.970 is the presence or absence of this underscore. 01:36:43.970 --> 01:36:45.830 But that's typically the convention. 01:36:45.830 --> 01:36:48.590 By not using the underscore there, make sure 01:36:48.590 --> 01:36:50.900 that even that assignment goes through the setter 01:36:50.900 --> 01:36:53.630 so that, honestly, I, don't have to copy paste the same error 01:36:53.630 --> 01:36:54.800 checking in two places. 01:36:54.800 --> 01:36:56.600 I can put it just in the setter. 01:36:56.600 --> 01:37:00.020 So it's a better design, and that's why I manually retyped it at first, 01:37:00.020 --> 01:37:02.390 but then I deleted it from init. 01:37:02.390 --> 01:37:05.840 Well, allow me to propose that we make one other change to this file. 01:37:05.840 --> 01:37:09.900 Might as well go ahead and define a property for name as well. 01:37:09.900 --> 01:37:12.650 And let me go ahead and do this-- maybe above the house property 01:37:12.650 --> 01:37:16.250 just to keep things in the same order as I defined them earlier. 01:37:16.250 --> 01:37:18.570 Let me give myself another property. 01:37:18.570 --> 01:37:20.330 This one is going to be called name. 01:37:20.330 --> 01:37:22.790 It's going to take one argument called self, as always. 01:37:22.790 --> 01:37:26.842 And this one, very similarly, is just going to return self._name. 01:37:26.842 --> 01:37:28.550 So I'm going to anticipate that I'm going 01:37:28.550 --> 01:37:32.360 to have to rename name also so that I don't have that same collision as 01:37:32.360 --> 01:37:33.080 before. 01:37:33.080 --> 01:37:36.740 But now let me go ahead and define another setter-- 01:37:36.740 --> 01:37:37.940 this one for name. 01:37:37.940 --> 01:37:40.640 So the convention is @name.setter. 01:37:40.640 --> 01:37:41.600 Why name? 01:37:41.600 --> 01:37:45.140 Because the property I just created is called name. 01:37:45.140 --> 01:37:50.030 So the getter and setter work in conjunction in this way, if you will. 01:37:50.030 --> 01:37:53.480 Let me go down under that name setter and define another function, 01:37:53.480 --> 01:37:54.590 also called name. 01:37:54.590 --> 01:37:57.000 But the key thing here is that it's not identical. 01:37:57.000 --> 01:38:01.190 It's not the exact same function name and the exact same number of arguments. 01:38:01.190 --> 01:38:03.700 The setter, again, takes a second argument. 01:38:03.700 --> 01:38:05.450 And I can call it anything I want, but I'm 01:38:05.450 --> 01:38:07.950 going to call it name because that's what's being passed in. 01:38:07.950 --> 01:38:10.250 And I'm going to put my error checking here. 01:38:10.250 --> 01:38:14.750 If not name, just like we used to do, let's go ahead and raise a value error, 01:38:14.750 --> 01:38:19.970 and let's put an explanatory message like "Missing name," quote, unquote. 01:38:19.970 --> 01:38:26.450 Otherwise, let's go ahead and update self._name to equal name. 01:38:26.450 --> 01:38:29.690 And I don't have to change init except to get rid 01:38:29.690 --> 01:38:33.530 of this duplicate error checking now because, again, 01:38:33.530 --> 01:38:38.360 if I use self.name equals here and self.house equals here 01:38:38.360 --> 01:38:41.120 with no underscore, both of those assignments 01:38:41.120 --> 01:38:44.600 are going to go through my two setter functions now. 01:38:44.600 --> 01:38:46.640 Before we run this, let me go ahead and remove 01:38:46.640 --> 01:38:50.000 this adversarial code, which we know won't work because we're catching it. 01:38:50.000 --> 01:38:53.600 Let me go back down to my terminal window and run Python of student.py, 01:38:53.600 --> 01:38:54.140 Enter. 01:38:54.140 --> 01:38:55.160 Let's type in Harry. 01:38:55.160 --> 01:38:56.330 Let's type in Gryffindor. 01:38:56.330 --> 01:38:57.900 And that seems to work. 01:38:57.900 --> 01:39:00.650 Let's try though, again, to run Python of student.py 01:39:00.650 --> 01:39:03.920 with Harry from Number Four, Privet Drive. 01:39:03.920 --> 01:39:05.640 This will not work. 01:39:05.640 --> 01:39:08.520 A value error with invalid house, because that's not 01:39:08.520 --> 01:39:09.990 one of the four Hogwarts houses. 01:39:09.990 --> 01:39:12.240 And now, for good measure, let's run it one more time. 01:39:12.240 --> 01:39:13.780 And let's not even give it a name. 01:39:13.780 --> 01:39:15.570 Let's just hit Enter when prompted. 01:39:15.570 --> 01:39:17.072 I can type anything for the house. 01:39:17.072 --> 01:39:19.155 I'll go ahead and still give it Gryffindor, Enter. 01:39:19.155 --> 01:39:24.150 And now we get another value error, but this one is for missing name. 01:39:24.150 --> 01:39:27.720 So we seem, now, to have all the more of a defense mechanism in place 01:39:27.720 --> 01:39:30.030 to ensure that name is as we expect. 01:39:30.030 --> 01:39:32.220 It's got to have some value that's not blank. 01:39:32.220 --> 01:39:33.720 And house is as we expect. 01:39:33.720 --> 01:39:36.210 It's got to have one of those four values. 01:39:36.210 --> 01:39:39.210 But at the risk of bursting everyone's bubble 01:39:39.210 --> 01:39:43.170 and making you wonder, why did we just go through all of that, 01:39:43.170 --> 01:39:50.080 unfortunately Python really focuses on conventions, not hard constraints. 01:39:50.080 --> 01:39:51.510 And by that, I mean this. 01:39:51.510 --> 01:39:56.430 If I go back into my main function after I've gotten a student on line 30 01:39:56.430 --> 01:40:01.590 and I try to adversarially do something like this-- student.house equals 01:40:01.590 --> 01:40:04.830 "Number Four, Privet Drive," we know this 01:40:04.830 --> 01:40:08.610 won't work because my setter for house is going to catch this. 01:40:08.610 --> 01:40:09.540 Watch again. 01:40:09.540 --> 01:40:12.060 Python of student.py. 01:40:12.060 --> 01:40:13.230 Let's type in Harry. 01:40:13.230 --> 01:40:15.750 Let's type in Gryffindor, which will at least pass 01:40:15.750 --> 01:40:18.180 our check that's induced by init. 01:40:18.180 --> 01:40:23.250 But line 31 is going to trigger the same setter to be called, 01:40:23.250 --> 01:40:26.790 and we're going to raise a value error saying "Invalid house." 01:40:26.790 --> 01:40:29.460 Unfortunately, and if some of you are already 01:40:29.460 --> 01:40:33.570 thinking a little adversarially, tragically, look what you can do. 01:40:33.570 --> 01:40:36.690 You can change .house to be ._house. 01:40:36.690 --> 01:40:37.230 Why? 01:40:37.230 --> 01:40:41.640 Well, the instance variable is now called _house. 01:40:41.640 --> 01:40:44.340 The property is called house, no underscore. 01:40:44.340 --> 01:40:50.265 But the underlying attribute implemented as an instance variable is still called 01:40:50.265 --> 01:40:51.000 _house. 01:40:51.000 --> 01:40:54.750 And tragically, Python of student.py. 01:40:54.750 --> 01:40:56.190 Let's type in Harry. 01:40:56.190 --> 01:40:58.570 Let's type in Gryffindor, which is correct. 01:40:58.570 --> 01:41:00.180 But watch what happens now. 01:41:00.180 --> 01:41:01.530 Oh, my God. 01:41:01.530 --> 01:41:03.210 We slip through. 01:41:03.210 --> 01:41:06.900 So what was the point of all of this emphasis from me 01:41:06.900 --> 01:41:10.770 on doing things the "right way," the Python quick way by having this getter 01:41:10.770 --> 01:41:11.520 and setter? 01:41:11.520 --> 01:41:14.430 Well, unlike languages like Java, that just 01:41:14.430 --> 01:41:17.430 prevent you from doing things like this, Python 01:41:17.430 --> 01:41:20.250 itself allows you to specify that certain instance variables can 01:41:20.250 --> 01:41:23.880 be public and accessible to anyone's code, or protected, 01:41:23.880 --> 01:41:27.990 or private, which means that no one else should be able to change these values. 01:41:27.990 --> 01:41:31.320 In the world of Python, it's just the honor system. 01:41:31.320 --> 01:41:33.630 It's not baked into the language itself that there's 01:41:33.630 --> 01:41:36.720 a notion of visibility, public or private or even somewhere in between 01:41:36.720 --> 01:41:37.440 protected. 01:41:37.440 --> 01:41:39.430 Instead, you're on the honor system. 01:41:39.430 --> 01:41:42.870 And the convention generally is, if an instance 01:41:42.870 --> 01:41:46.950 variable starts with an underscore, please don't touch it. 01:41:46.950 --> 01:41:47.910 Just don't. 01:41:47.910 --> 01:41:50.970 That's on you if you touch that variable and break things. 01:41:50.970 --> 01:41:53.190 The underscore is meant to signify a convention 01:41:53.190 --> 01:41:55.798 that this is meant to be "private," but it really just 01:41:55.798 --> 01:41:57.090 means, please don't touch this. 01:41:57.090 --> 01:41:59.160 Sometimes, if there's two underscores, which you can use, 01:41:59.160 --> 01:42:01.740 too, that's an even greater effort by programmers to say, 01:42:01.740 --> 01:42:03.190 really don't touch this. 01:42:03.190 --> 01:42:06.390 But technically speaking, there's nothing stopping you or me 01:42:06.390 --> 01:42:10.080 from circumventing all of these mechanisms, these properties, 01:42:10.080 --> 01:42:11.340 these getters and setters. 01:42:11.340 --> 01:42:13.470 We're ultimately just on the honor system 01:42:13.470 --> 01:42:17.490 not to do so when we see instance variables prefixed with one, 01:42:17.490 --> 01:42:19.260 or perhaps even two underscores. 01:42:19.260 --> 01:42:21.790 All right, so this is a lot all at once-- 01:42:21.790 --> 01:42:24.090 this Introduction to object-oriented programming. 01:42:24.090 --> 01:42:27.360 But it might come as quite a surprise that, even though we 01:42:27.360 --> 01:42:31.650 might have identified OOP by name in weeks past, 01:42:31.650 --> 01:42:36.687 we've all been using classes and objects for weeks now in this class. 01:42:36.687 --> 01:42:40.020 In fact, if you think back on one of the very first things we did in this class, 01:42:40.020 --> 01:42:43.300 we used integers and just got integers from the user. 01:42:43.300 --> 01:42:44.980 But if you haven't already-- 01:42:44.980 --> 01:42:48.720 if you go and dig into the documentation for integers, 01:42:48.720 --> 01:42:51.120 which, again, lives at this URL here, you 01:42:51.120 --> 01:42:57.390 would actually find that int itself is and has been for weeks a class. 01:42:57.390 --> 01:43:01.740 And in fact, this is the signature of the constructor call for an int, 01:43:01.740 --> 01:43:06.330 whereby you pass in x, like a number, quote, unquote, "50" or, quote, 01:43:06.330 --> 01:43:09.660 unquote, something else-- you pass in optionally the base-- 01:43:09.660 --> 01:43:12.960 10 for decimal, 2 for binary or anything else. 01:43:12.960 --> 01:43:17.400 And that int function will actually return to you, all this time, 01:43:17.400 --> 01:43:20.820 an object of type int. 01:43:20.820 --> 01:43:23.280 That is to say int is a class. 01:43:23.280 --> 01:43:26.460 It is a template, a blueprint for creating integers in memory. 01:43:26.460 --> 01:43:30.330 And any time you and I have converted a string, for, instance to an int, 01:43:30.330 --> 01:43:35.160 you and I have been creating an object of type int that was calling, 01:43:35.160 --> 01:43:39.098 apparently, the underscore underscore, init, underscore underscore method, 01:43:39.098 --> 01:43:40.890 that someone else-- the authors of Python-- 01:43:40.890 --> 01:43:43.710 wrote to give us back that proper integer. 01:43:43.710 --> 01:43:47.730 Besides that, if you can believe it, strs, strings in Python 01:43:47.730 --> 01:43:51.190 have been classes since the first week of this class as well. 01:43:51.190 --> 01:43:53.190 If you look up the documentation for a str, 01:43:53.190 --> 01:43:57.750 which lives at a similar URL there, you will find that, when you instantiate-- 01:43:57.750 --> 01:43:59.310 that is, create a str-- 01:43:59.310 --> 01:44:03.390 it takes, optionally, a parameter called object here, 01:44:03.390 --> 01:44:05.880 the default value of which is just, quote, unquote, 01:44:05.880 --> 01:44:09.190 which allows you to create, in effect, an empty string, a blank string, 01:44:09.190 --> 01:44:09.690 if you will. 01:44:09.690 --> 01:44:13.740 But any time you and I have created strs or even used explicitly 01:44:13.740 --> 01:44:18.540 the str function, you are getting back an object of type str. 01:44:18.540 --> 01:44:23.580 Any time you and I have forced a string to lowercase per the documentation, 01:44:23.580 --> 01:44:29.250 using syntax like this, you and I have been taking an object of type str 01:44:29.250 --> 01:44:34.350 and forcing it all to lowercase by calling a method called lower, 01:44:34.350 --> 01:44:39.180 a method that the authors of Python built into the str class, 01:44:39.180 --> 01:44:42.270 but it's been there from the get-go, so this notion of methods is not 01:44:42.270 --> 01:44:43.023 even new today. 01:44:43.023 --> 01:44:44.940 You wouldn't have been doing it for this long. 01:44:44.940 --> 01:44:48.630 If you've ever called strip to remove the leading and the trailing whitespace 01:44:48.630 --> 01:44:53.370 from a string in Python, you are calling another method that came with Python-- 01:44:53.370 --> 01:44:54.960 written by the authors of Python. 01:44:54.960 --> 01:44:57.210 And even though we didn't call it a class at the time, 01:44:57.210 --> 01:45:00.900 a str, all this time, has been a class. 01:45:00.900 --> 01:45:04.770 And instances of strings are, themselves, objects. 01:45:04.770 --> 01:45:07.500 And those objects come therefore with these functions 01:45:07.500 --> 01:45:10.680 built in-- a.k.a. methods that allow us to do things like force 01:45:10.680 --> 01:45:14.010 to lowercase and strip whitespace from the beginning and end. 01:45:14.010 --> 01:45:15.180 Let's do another. 01:45:15.180 --> 01:45:20.730 list-- any time you've created a list, either syntactically with square 01:45:20.730 --> 01:45:24.760 brackets or literally with L-I-S-T, open parentheses, closed parentheses, 01:45:24.760 --> 01:45:27.570 which is also possible, you have been using a class. 01:45:27.570 --> 01:45:31.050 If you go to the documentation for list, at this similar URL 01:45:31.050 --> 01:45:35.310 here, or more specifically, the tutorial on lists here in Python, 01:45:35.310 --> 01:45:38.400 you will see that a list is and has been, 01:45:38.400 --> 01:45:42.150 since the early weeks of this class, a class itself. 01:45:42.150 --> 01:45:47.370 And that list class takes, as part of its initialization, 01:45:47.370 --> 01:45:52.200 an optional iterable, something that can be iterated over-- like 1, 2, 3, 01:45:52.200 --> 01:45:55.350 or some list of values, and you can then get back 01:45:55.350 --> 01:45:58.620 a list containing those same iterable values. 01:45:58.620 --> 01:46:01.860 If you've ever appended something to a list in this class, 01:46:01.860 --> 01:46:03.780 as I have myself in the past, you've been 01:46:03.780 --> 01:46:08.610 using a method called append that comes with the list class that, per the x 01:46:08.610 --> 01:46:11.760 here, takes an argument that allows you to append something 01:46:11.760 --> 01:46:14.040 to the current list, a.k.a. 01:46:14.040 --> 01:46:16.350 Self in the context of that method. 01:46:16.350 --> 01:46:17.610 We can do this all day long. 01:46:17.610 --> 01:46:20.820 If you've used a dictionary or a dict in Python-- 01:46:20.820 --> 01:46:24.330 I've actually, all this time, been calling them dict objects, 01:46:24.330 --> 01:46:25.710 and that's for a reason. 01:46:25.710 --> 01:46:28.470 dict itself is a class in Python, if you pull up 01:46:28.470 --> 01:46:30.390 its official documentation here. 01:46:30.390 --> 01:46:34.590 And you'll see that it is defined, indeed, as itself a class. 01:46:34.590 --> 01:46:36.532 And that class comes with methods as well. 01:46:36.532 --> 01:46:38.490 And so any time we've manipulated dictionaries, 01:46:38.490 --> 01:46:43.120 we've been underneath the hood, using all of those same methods. 01:46:43.120 --> 01:46:45.420 And in fact, we can see this if we're really curious. 01:46:45.420 --> 01:46:48.580 Let me go back over here to VS Code. 01:46:48.580 --> 01:46:51.450 And let me go ahead and create a new file that, very simply, does 01:46:51.450 --> 01:46:53.717 something play around with data types. 01:46:53.717 --> 01:46:56.050 And let me go ahead and create a new file, for instance, 01:46:56.050 --> 01:47:01.860 called, say, type.py, just so that I can poke around inside of some values. 01:47:01.860 --> 01:47:05.560 And in type.py, I'm just going to go ahead and do this. 01:47:05.560 --> 01:47:09.570 I'm going to print out whatever the type is of, say, the number 50. 01:47:09.570 --> 01:47:12.720 And This is a function you've not necessarily seen me use already, 01:47:12.720 --> 01:47:15.690 and it's not one you would frequently use in your own code. 01:47:15.690 --> 01:47:18.150 There are other ways to detect, if you need to, 01:47:18.150 --> 01:47:19.840 what the type is of a variable. 01:47:19.840 --> 01:47:24.990 But in this case, type of 50 is just going to tell me and then print out 01:47:24.990 --> 01:47:27.040 what the data type is of that value. 01:47:27.040 --> 01:47:29.640 Now, hopefully, all of us could guess that 50 is indeed 01:47:29.640 --> 01:47:30.660 going to be an integer-- 01:47:30.660 --> 01:47:32.885 that is, an int, but we can see it in this way. 01:47:32.885 --> 01:47:35.760 And this, too, is what's powerful about knowing a bit of programming. 01:47:35.760 --> 01:47:37.635 If you want to know the answer to a question, 01:47:37.635 --> 01:47:39.430 just try it out, like I am here. 01:47:39.430 --> 01:47:42.330 So let me go ahead and run Python of type.py, Enter. 01:47:42.330 --> 01:47:43.410 And there it is. 01:47:43.410 --> 01:47:48.670 When you print out the type of the number 50, you'll see on the screen, 01:47:48.670 --> 01:47:51.840 in this cryptic syntax, class 'int.' 01:47:51.840 --> 01:47:54.730 This is not something that you probably want to show to the user. 01:47:54.730 --> 01:47:57.840 But if you yourself just want to poke around and see what's going on 01:47:57.840 --> 01:48:00.390 or maybe use that information somehow, it's 01:48:00.390 --> 01:48:04.030 certainly at your disposal to use this type function for that. 01:48:04.030 --> 01:48:05.790 Let's change it around a little bit. 01:48:05.790 --> 01:48:09.570 Instead of passing as the argument to type 50, as an int, 01:48:09.570 --> 01:48:12.510 let's type something also familiar, like "hello, world," 01:48:12.510 --> 01:48:14.370 in double or single quotes. 01:48:14.370 --> 01:48:16.740 Let me go back to my terminal window, clear the screen, 01:48:16.740 --> 01:48:18.420 and run Python of type.py again. 01:48:18.420 --> 01:48:20.550 And now, voila, there it is. 01:48:20.550 --> 01:48:23.940 All this time, a str is also a class. 01:48:23.940 --> 01:48:25.550 We can do this a few more times, For. 01:48:25.550 --> 01:48:26.050 Instance. 01:48:26.050 --> 01:48:30.090 Let's go ahead and change "hello, world" to just an empty list-- 01:48:30.090 --> 01:48:32.520 open square bracket, closed square bracket. 01:48:32.520 --> 01:48:34.583 And this is starting to look a little cryptic, 01:48:34.583 --> 01:48:36.000 but, again, notice what I'm doing. 01:48:36.000 --> 01:48:37.920 In square brackets is an empty list. 01:48:37.920 --> 01:48:39.310 We've done that before. 01:48:39.310 --> 01:48:42.810 That is the sole argument to this new type function. 01:48:42.810 --> 01:48:45.880 And that's just being passed to the print function 01:48:45.880 --> 01:48:49.680 so that the return value of type is the argument to print. 01:48:49.680 --> 01:48:53.760 So if I now run this code, Python of type.py, there it is. 01:48:53.760 --> 01:48:55.650 A list is a class, too. 01:48:55.650 --> 01:48:59.190 You might recall that I said that you can also create an empty list 01:48:59.190 --> 01:49:02.770 by literally doing list (). 01:49:02.770 --> 01:49:05.580 This is a bit of an inconsistency, as we can now 01:49:05.580 --> 01:49:11.160 identify that int and str and now list-- they're technically all lowercase. 01:49:11.160 --> 01:49:15.930 And I went to great lengths of creating my student class to have that capital 01:49:15.930 --> 01:49:17.790 S. That's a convention. 01:49:17.790 --> 01:49:22.440 Because int and stir and list and others come with Python, 01:49:22.440 --> 01:49:26.640 they decided to make their built-in data types-- even though they're classes-- 01:49:26.640 --> 01:49:27.600 all lowercase. 01:49:27.600 --> 01:49:30.960 But the convention, the recommendation in the Python community when creating 01:49:30.960 --> 01:49:34.740 your classes is to capitalize the first letter, as I did, 01:49:34.740 --> 01:49:41.730 in something like Student, capital S. But list () is identical to really just 01:49:41.730 --> 01:49:43.230 two empty square brackets. 01:49:43.230 --> 01:49:47.640 If I clear my screen and run type.py again, you see the exact same thing. 01:49:47.640 --> 01:49:48.960 The class is called list. 01:49:48.960 --> 01:49:50.250 Let's do one more. 01:49:50.250 --> 01:49:54.240 Let me change the list to be not square brackets but curly braces. 01:49:54.240 --> 01:49:55.320 We've done this before. 01:49:55.320 --> 01:49:59.140 Any time I've done two curly braces with nothing in between, this, of course, 01:49:59.140 --> 01:50:02.670 is an empty dictionary, or a dict object in Python. 01:50:02.670 --> 01:50:03.960 Well, we can see that now. 01:50:03.960 --> 01:50:06.720 Let me clear my screen, run Python of type.py, Enter, 01:50:06.720 --> 01:50:08.880 and there it is-- class 'dict." 01:50:08.880 --> 01:50:10.650 It's been there this whole time. 01:50:10.650 --> 01:50:13.560 We just didn't call it a class until today. 01:50:13.560 --> 01:50:15.840 I can similarly do this one explicitly. 01:50:15.840 --> 01:50:19.710 Instead of two curly braces, let's write out dict with two parentheses. 01:50:19.710 --> 01:50:22.770 Now we have a lot of parentheses again, like with list. 01:50:22.770 --> 01:50:26.460 But this is just making even more clear that the type of a dict object 01:50:26.460 --> 01:50:29.560 is indeed the class, dict, itself. 01:50:29.560 --> 01:50:33.570 So this is to say that, as new as a lot of today's idea and syntax, 01:50:33.570 --> 01:50:36.900 might be you've actually been using it, perhaps unbeknownst to you, 01:50:36.900 --> 01:50:37.950 for weeks now. 01:50:37.950 --> 01:50:40.608 We now just have terminology to describe what 01:50:40.608 --> 01:50:42.150 it is we've been doing all this time. 01:50:42.150 --> 01:50:45.390 And you now have the expressiveness, with some practice, 01:50:45.390 --> 01:50:49.980 to create your own classes, inside of which are your own instance variables, 01:50:49.980 --> 01:50:54.900 perhaps wrapped with those properties and your own instance methods. 01:50:54.900 --> 01:50:58.350 But it turns out there's other types of methods in the world. 01:50:58.350 --> 01:51:00.600 Thus far, I've been deliberate in calling 01:51:00.600 --> 01:51:05.580 all of our variables instance variables and all of our methods instance 01:51:05.580 --> 01:51:06.330 methods. 01:51:06.330 --> 01:51:11.550 It turns out there's other types of variables and methods out there, 01:51:11.550 --> 01:51:14.100 and one of those is called class methods. 01:51:14.100 --> 01:51:19.530 It turns out that sometimes it's not really necessary or sensible 01:51:19.530 --> 01:51:23.530 to associate a function with objects of a class, 01:51:23.530 --> 01:51:26.190 but rather with the class itself. 01:51:26.190 --> 01:51:31.050 An instance, or an object of a class, is a very specific incarnation thereof. 01:51:31.050 --> 01:51:34.525 Again, on that neighborhood that has a lot of identical looking buildings, 01:51:34.525 --> 01:51:37.650 but they're all a little bit different because of different paint and such, 01:51:37.650 --> 01:51:40.890 sometimes you might have functionality related 01:51:40.890 --> 01:51:44.760 to each of those houses that isn't distinct or unique for any 01:51:44.760 --> 01:51:45.540 of the houses. 01:51:45.540 --> 01:51:48.570 It's functionality that's going to be exactly the same no 01:51:48.570 --> 01:51:50.430 matter the house in question. 01:51:50.430 --> 01:51:52.770 Same in the world of object-oriented programming. 01:51:52.770 --> 01:51:55.590 Sometimes you want some functionality, some action 01:51:55.590 --> 01:52:00.720 to be associated with the class itself, no matter what the specific object's 01:52:00.720 --> 01:52:03.540 own values or instance variables are. 01:52:03.540 --> 01:52:07.320 And for that, we have a keyword called @classmethod. 01:52:07.320 --> 01:52:09.660 This is another decorator-- really, another 01:52:09.660 --> 01:52:14.610 function-- that you can use to specify that this method is not, 01:52:14.610 --> 01:52:17.430 by default, implicitly an instance method that 01:52:17.430 --> 01:52:20.020 has access to self, the object itself. 01:52:20.020 --> 01:52:22.950 This is a class method that's not going to have access to self, 01:52:22.950 --> 01:52:25.380 but it does know what class it's inside. 01:52:25.380 --> 01:52:26.830 So what do I mean by this? 01:52:26.830 --> 01:52:28.620 Well, let me go back to VS Code here. 01:52:28.620 --> 01:52:32.520 And let me propose that we create a new file this time implementing 01:52:32.520 --> 01:52:36.180 the notion of a-- the sorting hat, from the world of Harry Potter as well, 01:52:36.180 --> 01:52:37.260 to stay on theme. 01:52:37.260 --> 01:52:39.870 I'm going to go ahead and run code of hat.py. 01:52:39.870 --> 01:52:43.230 And in hat.py, let's implement the notion of the sorting hat. 01:52:43.230 --> 01:52:45.810 If unfamiliar in the books and in the films 01:52:45.810 --> 01:52:48.540 there is literally a pointy hat that, when 01:52:48.540 --> 01:52:52.140 a student put it's on their head, that sorting hat, so to speak, 01:52:52.140 --> 01:52:54.930 decides what house the student is in-- whether it's 01:52:54.930 --> 01:52:56.620 Gryffindor or something else. 01:52:56.620 --> 01:53:00.690 So let's implement, in code, this notion of a sorting hat such that, 01:53:00.690 --> 01:53:04.470 when we pass to the sorting hat the name of a student, like, quote, unquote, 01:53:04.470 --> 01:53:07.290 "Harry" this sorting hat, implemented in code, 01:53:07.290 --> 01:53:11.250 will tell us what house that student should be in. 01:53:11.250 --> 01:53:12.960 Well, let's go ahead and do this. 01:53:12.960 --> 01:53:17.910 In hat.py, first, let's go ahead and define a class called hat, 01:53:17.910 --> 01:53:20.542 and then let's get back to implementing it itself. 01:53:20.542 --> 01:53:23.250 And I find this to be a helpful technique, not just with teaching 01:53:23.250 --> 01:53:24.600 but when writing code. 01:53:24.600 --> 01:53:26.520 I know I want a hat class. 01:53:26.520 --> 01:53:28.770 I don't necessarily know what I want it to do yet, 01:53:28.770 --> 01:53:31.290 so I'm going to create this placeholder, dot, dot, dot, 01:53:31.290 --> 01:53:32.980 so I'll come back to that. 01:53:32.980 --> 01:53:36.210 Let's now try to use this class as though it existed. 01:53:36.210 --> 01:53:39.450 And from there, I perhaps can realize exactly what 01:53:39.450 --> 01:53:42.820 functionality that class needs to have to support my use case. 01:53:42.820 --> 01:53:45.930 Let me go ahead and create a variable called hat in all lowercase 01:53:45.930 --> 01:53:48.720 and instantiate a hat object. 01:53:48.720 --> 01:53:52.260 So no matter what the hat class ends up looking like, 01:53:52.260 --> 01:53:55.350 this is the common syntax for instantiating 01:53:55.350 --> 01:53:57.120 an object of a certain class. 01:53:57.120 --> 01:54:00.150 In the past, we saw student, all lowercase, 01:54:00.150 --> 01:54:03.930 equals capital Student, open parenthesis, close parentheses, 01:54:03.930 --> 01:54:06.600 and then eventually, we added in things like name and house. 01:54:06.600 --> 01:54:10.590 For now, let's assume that the hat is much simpler than a student, 01:54:10.590 --> 01:54:12.700 and it only has sorting capabilities. 01:54:12.700 --> 01:54:15.990 So I'm not going to even pass any arguments there, too. 01:54:15.990 --> 01:54:21.900 Let me assume that the sorting hat has one function-- one method inside of it 01:54:21.900 --> 01:54:22.810 called, sort. 01:54:22.810 --> 01:54:30.870 And so if I do hat.sort ("Harry"), let's propose that that prints out what house 01:54:30.870 --> 01:54:32.650 that student should be in. 01:54:32.650 --> 01:54:33.750 So that's it. 01:54:33.750 --> 01:54:35.760 I'm going to encapsulate-- 01:54:35.760 --> 01:54:38.130 that is tuck away inside of a hat class-- 01:54:38.130 --> 01:54:42.450 all of this requisite functionality, and I'm going to print out onto the screen 01:54:42.450 --> 01:54:43.680 what hat-- 01:54:43.680 --> 01:54:46.410 what house Harry belongs in. 01:54:46.410 --> 01:54:51.390 Now I think I need to get into the weeds of actually initializing this class. 01:54:51.390 --> 01:54:53.250 Well, let me go ahead and do this. 01:54:53.250 --> 01:54:56.010 If I don't care to parameterize hat-- 01:54:56.010 --> 01:55:00.280 I just want to, for instance sort values, 01:55:00.280 --> 01:55:03.120 let's go ahead and define this function, sort, first. 01:55:03.120 --> 01:55:06.480 So let's define sort, as taking a first argument, self, 01:55:06.480 --> 01:55:10.320 which is always going to be the case when defining an instance method as 01:55:10.320 --> 01:55:10.950 before. 01:55:10.950 --> 01:55:14.970 But the sort method clearly takes one argument from the programmer, me-- 01:55:14.970 --> 01:55:16.800 namely the student's name. 01:55:16.800 --> 01:55:18.930 And again, we've seen this dichotomy before. 01:55:18.930 --> 01:55:23.310 Even though I'm trying to pass in one argument, when I define the method, 01:55:23.310 --> 01:55:26.370 it's got to take that many arguments, plus one more-- 01:55:26.370 --> 01:55:29.430 self which is always going to be automatically passed in by Python 01:55:29.430 --> 01:55:30.810 first. 01:55:30.810 --> 01:55:32.500 What do I want to do? 01:55:32.500 --> 01:55:34.515 Well, let's go ahead and do something like this. 01:55:37.380 --> 01:55:43.563 Print, this name-- how about "is in," "some house." 01:55:43.563 --> 01:55:45.480 I'm going to, again, use some placeholder code 01:55:45.480 --> 01:55:49.200 for myself because I'm not quite sure how to finish implementing this sorting 01:55:49.200 --> 01:55:49.780 hat. 01:55:49.780 --> 01:55:52.920 But I think that's enough to just test where my code is at now. 01:55:52.920 --> 01:55:56.850 Let me go ahead and run Python of hat.py and hit Enter. 01:55:56.850 --> 01:55:59.460 And it looks like, indeed, Harry is in some house. 01:55:59.460 --> 01:56:03.150 We're not done yet because it's clearly not doing anything interesting, 01:56:03.150 --> 01:56:06.990 but it at least is running correctly with no errors. 01:56:06.990 --> 01:56:11.340 Well, let's go ahead now and decide where-- 01:56:11.340 --> 01:56:14.730 what house Harry should actually be in by introducing a bit of randomness 01:56:14.730 --> 01:56:16.830 and choosing a house randomly. 01:56:16.830 --> 01:56:18.720 While I can do this in a few ways, Let. 01:56:18.720 --> 01:56:20.070 Me go ahead and do this. 01:56:20.070 --> 01:56:22.770 I need to have a list of houses somewhere. 01:56:22.770 --> 01:56:24.055 So where can I put that? 01:56:24.055 --> 01:56:25.930 I could solve this problem in different ways. 01:56:25.930 --> 01:56:27.220 Let me propose that I do this. 01:56:27.220 --> 01:56:30.030 Let me define a method called init, as I've done before, 01:56:30.030 --> 01:56:32.460 that takes in self, but no other arguments. 01:56:32.460 --> 01:56:35.800 And whenever the sorting hat is instantiated, let's do this. 01:56:35.800 --> 01:56:41.160 Let's create a houses instance variable, plural, that equals this list-- 01:56:41.160 --> 01:56:48.180 Gryffindor, Hufflepuff, Ravenclaw, Slytherin, 01:56:48.180 --> 01:56:50.640 so the exact same list that we've used before, 01:56:50.640 --> 01:56:54.540 and I'm storing it in an instance variable inside of this class. 01:56:54.540 --> 01:56:57.900 I'm not taking any arguments beyond self to init, 01:56:57.900 --> 01:57:01.840 but I just need this list of values somewhere, for instance. 01:57:01.840 --> 01:57:03.460 So what can I do here? 01:57:03.460 --> 01:57:07.645 Well, let me go ahead and replace some house with the actual house. 01:57:07.645 --> 01:57:08.770 Well, what could I do here? 01:57:08.770 --> 01:57:10.800 Well, I want to put a house there. 01:57:10.800 --> 01:57:13.290 Well, let's go ahead and create a variable called house. 01:57:13.290 --> 01:57:17.490 And if you think back to our discussion of libraries, in the random module, 01:57:17.490 --> 01:57:23.010 there is a function called choice that, if you pass in a list of choices, 01:57:23.010 --> 01:57:27.540 like self.houses, that will pick a random house out of those four. 01:57:27.540 --> 01:57:29.950 And then on line 7, I can pass it in. 01:57:29.950 --> 01:57:33.120 If I want to tighten this up, let me just go ahead and highlight that code, 01:57:33.120 --> 01:57:34.320 get rid of the variable. 01:57:34.320 --> 01:57:35.765 It's technically unnecessary. 01:57:35.765 --> 01:57:37.890 And because the line of code is still pretty short, 01:57:37.890 --> 01:57:40.650 I'm OK with just putting it all in one line. 01:57:40.650 --> 01:57:44.200 But I could certainly use the variable like I did a moment ago. 01:57:44.200 --> 01:57:45.240 So what have I done? 01:57:45.240 --> 01:57:51.930 In my init function, I have defined a initialization of the object 01:57:51.930 --> 01:57:55.080 that stores in self.houses the list of four houses. 01:57:55.080 --> 01:57:57.780 And then, in sort, I'm accessing that same list, 01:57:57.780 --> 01:58:01.230 but I'm randomly choosing the set of houses there. 01:58:01.230 --> 01:58:03.000 Now, why have I done it in this way? 01:58:03.000 --> 01:58:04.973 This, too, is general convention. 01:58:04.973 --> 01:58:07.140 Any time you have a list of things that-- who knows? 01:58:07.140 --> 01:58:09.210 Maybe will change over time. 01:58:09.210 --> 01:58:12.750 Places like Harvard have constructed new houses over the years, 01:58:12.750 --> 01:58:15.180 so you might have to change the list of available houses. 01:58:15.180 --> 01:58:18.300 It didn't happen in seven books or eight films of Harry Potter. 01:58:18.300 --> 01:58:20.400 But you could imagine maybe Hogwarts eventually 01:58:20.400 --> 01:58:25.500 has a fifth house, so there's generally some value in putting list of constants 01:58:25.500 --> 01:58:28.530 toward the top of your file, toward the top of the class so it's just 01:58:28.530 --> 01:58:31.080 obvious what the list of values is. 01:58:31.080 --> 01:58:33.960 You don't want to necessarily tuck it away in some function, 01:58:33.960 --> 01:58:37.590 like sort, especially if you might want to use that function-- 01:58:37.590 --> 01:58:41.370 sorry, especially if you want to use that list in multiple functions, not 01:58:41.370 --> 01:58:41.920 just sort. 01:58:41.920 --> 01:58:43.795 But if I kept adding to this class, you might 01:58:43.795 --> 01:58:46.300 want to use that same list of houses in multiple functions. 01:58:46.300 --> 01:58:51.757 So let's keep it in the object itself by storing it in self.houses. 01:58:51.757 --> 01:58:54.840 All right, well, we're about to change the course of history here perhaps. 01:58:54.840 --> 01:58:58.860 Let me do Python of hat.py, and I think we're about to assign Harry 01:58:58.860 --> 01:59:02.050 to one of those four houses randomly. 01:59:02.050 --> 01:59:04.260 Huh, NameError. 01:59:04.260 --> 01:59:06.240 Name 'random' is not defined. 01:59:06.240 --> 01:59:09.630 Well, wait a minute, where did I go wrong here? 01:59:09.630 --> 01:59:14.730 Thinking back to our class on libraries, why did my code break and not tell me 01:59:14.730 --> 01:59:16.830 where Harry is to be? 01:59:16.830 --> 01:59:19.910 AUDIENCE: You did not import the random library. 01:59:19.910 --> 01:59:20.910 DAVID J. MALAN: Exactly. 01:59:20.910 --> 01:59:23.760 If the random library or module is something I want to use, 01:59:23.760 --> 01:59:26.800 I need to tell Python that at the top of my file. 01:59:26.800 --> 01:59:30.150 So let me go up here and do import random. 01:59:30.150 --> 01:59:32.910 And then, below that, let me go ahead and clear my terminal window 01:59:32.910 --> 01:59:33.630 and try again. 01:59:33.630 --> 01:59:39.240 Python of hat.py, crossing my fingers, seeing where Harry is going to end up. 01:59:39.240 --> 01:59:43.230 And, OK, Harry as of now is officially in Hufflepuff, 01:59:43.230 --> 01:59:45.720 despite everything you've read or seen. 01:59:45.720 --> 01:59:47.080 Well, let's run this again. 01:59:47.080 --> 01:59:51.030 Let me clear my window and run Python of hat.py, and now he's in Ravenclaw. 01:59:51.030 --> 01:59:52.770 That's consistent with using random. 01:59:52.770 --> 01:59:54.570 Let's clear that and run it again. 01:59:54.570 --> 01:59:56.670 He's still in Ravenclaw, but that could happen, 01:59:56.670 --> 01:59:58.045 even though there's four choices. 01:59:58.045 --> 01:59:58.830 Let's do it again. 01:59:58.830 --> 02:00:00.450 Hufflepuff-- back in Hufflepuff. 02:00:00.450 --> 02:00:02.860 We can't seem to get the right answer. 02:00:02.860 --> 02:00:05.580 Now he's in Gryffindor, albeit randomly. 02:00:05.580 --> 02:00:08.430 So we seem to have a program that, based on these limited tests, 02:00:08.430 --> 02:00:12.370 seems to be assigning Harry to a house randomly. 02:00:12.370 --> 02:00:16.620 Now I'm somewhat lazily just letting sort print out this value. 02:00:16.620 --> 02:00:21.600 I could do something else, like return a string, and then let me, on line 13, 02:00:21.600 --> 02:00:23.140 do the printing for me. 02:00:23.140 --> 02:00:28.200 But for now, I think we have an example of a class called hat that, 02:00:28.200 --> 02:00:31.320 nonetheless, applies some of our lessons learned thus far today, 02:00:31.320 --> 02:00:34.530 where I've created a class-- because a sorting hat is, frankly-- 02:00:34.530 --> 02:00:37.440 well, I was about to say real world entity, but really 02:00:37.440 --> 02:00:39.090 a fantasy world entity. 02:00:39.090 --> 02:00:42.990 And indeed, that's a, perhaps, common heuristic or mental model to have. 02:00:42.990 --> 02:00:47.250 When should you use a class to represent something in your code? 02:00:47.250 --> 02:00:51.840 Very often, when you're trying to represent some real world entity 02:00:51.840 --> 02:00:57.000 or fantasy world entity, like a student, which is something in the real world, 02:00:57.000 --> 02:00:59.820 like a sorting hat, which, OK, doesn't exist, 02:00:59.820 --> 02:01:03.390 but hat's certainly do, so quite reasonable to have a class for hat. 02:01:03.390 --> 02:01:07.710 And that's not always the case that classes represent real world entities. 02:01:07.710 --> 02:01:13.495 But we've seen thus far that int and stir and list and dict-- these 02:01:13.495 --> 02:01:15.870 are all structures that you might have in the real world. 02:01:15.870 --> 02:01:18.610 We have integers and strings of text and other things. 02:01:18.610 --> 02:01:22.050 So it rather makes sense to represent even those things, more technically, 02:01:22.050 --> 02:01:23.830 using a class as well. 02:01:23.830 --> 02:01:27.480 You could use just a dictionary to represent a student or a hat. 02:01:27.480 --> 02:01:32.100 But again, with classes come all this and even more functionality. 02:01:32.100 --> 02:01:37.980 But I honestly am not using classes in, really, the "right way" here. 02:01:37.980 --> 02:01:38.640 Why? 02:01:38.640 --> 02:01:41.850 Well, in the world of Harry Potter there really is only, 02:01:41.850 --> 02:01:43.980 to my knowledge, one sorting hat. 02:01:43.980 --> 02:01:48.190 And yet, here I have gone and implemented a class called hat. 02:01:48.190 --> 02:01:51.540 And again, a class is like a blueprint, a template, 02:01:51.540 --> 02:01:55.813 a mold that allows you to create one or more objects thereof. 02:01:55.813 --> 02:01:58.230 Now, most of my programs Thus far have been pretty simple, 02:01:58.230 --> 02:01:59.940 and I've just created one student. 02:01:59.940 --> 02:02:02.400 But certainly, if I spent more time and wrote more code, 02:02:02.400 --> 02:02:04.800 you could imagine writing one program that 02:02:04.800 --> 02:02:07.410 has a list of students-- many more students 02:02:07.410 --> 02:02:09.900 than just the one we keep demonstrating. 02:02:09.900 --> 02:02:11.830 Yet it would be a little weird-- 02:02:11.830 --> 02:02:14.490 it's a little inconsistent with the real or the fantasy 02:02:14.490 --> 02:02:19.230 world of Harry Potter to instantiate one, two, three or more sorting hats. 02:02:19.230 --> 02:02:20.910 There really is just one. 02:02:20.910 --> 02:02:23.070 Really one singleton, if you will, which is 02:02:23.070 --> 02:02:25.660 a term of art in a lot of contexts of programming. 02:02:25.660 --> 02:02:29.340 So let me propose that we actually improve the design of the sorting hat 02:02:29.340 --> 02:02:34.290 so that we don't have to instantiate a sorting hat because right now this 02:02:34.290 --> 02:02:39.990 is kind of allowing me to do something like hat 1 = hat, hat 2 = hat, hat 3 =, 02:02:39.990 --> 02:02:40.680 and so forth. 02:02:40.680 --> 02:02:42.510 I don't really need that capability. 02:02:42.510 --> 02:02:46.440 I really just need to represent the sorting hat with a class, 02:02:46.440 --> 02:02:48.340 but I don't really need to instantiate it. 02:02:48.340 --> 02:02:48.840 Why? 02:02:48.840 --> 02:02:49.923 Because it already exists. 02:02:49.923 --> 02:02:50.850 I need just one. 02:02:50.850 --> 02:02:54.250 So it turns out, in Python, that, up until now, 02:02:54.250 --> 02:02:57.060 we've been using, as I keep calling them, instance methods-- 02:02:57.060 --> 02:03:01.620 writing functions inside of classes that are automatically passed a reference 02:03:01.620 --> 02:03:03.570 to self, the current object. 02:03:03.570 --> 02:03:05.590 But sometimes you just don't need that. 02:03:05.590 --> 02:03:08.280 Sometimes it suffices to just know what the class is 02:03:08.280 --> 02:03:12.100 and assume that there might not even be any objects of that class. 02:03:12.100 --> 02:03:16.860 So in this sense, you can use a class really as a container for data 02:03:16.860 --> 02:03:21.480 and/or functionality that is just somehow conceptually related-- 02:03:21.480 --> 02:03:24.000 things related to a sorting hat. 02:03:24.000 --> 02:03:27.300 And there's this other decorator or function called @classmethod 02:03:27.300 --> 02:03:28.960 that allows us to do just this. 02:03:28.960 --> 02:03:30.600 So let me go back to my code here. 02:03:30.600 --> 02:03:36.240 And let me propose that, if I'm not going to instantiate multiple houses, 02:03:36.240 --> 02:03:39.120 I don't really need this init method because that's really 02:03:39.120 --> 02:03:43.470 meant to initialize specific objects from that blueprint, that template, 02:03:43.470 --> 02:03:44.280 that mold. 02:03:44.280 --> 02:03:45.930 So let me get rid of this. 02:03:45.930 --> 02:03:49.020 But if I get rid of this, I no longer have access to self. 02:03:49.020 --> 02:03:53.970 But that's OK because it turns out, in addition to their existing class 02:03:53.970 --> 02:03:57.630 methods, there are also what we might call class variables. 02:03:57.630 --> 02:04:01.990 And class variables exist within the class itself. 02:04:01.990 --> 02:04:05.030 And there's just one copy of that variable 02:04:05.030 --> 02:04:07.160 for all of the objects thereof. 02:04:07.160 --> 02:04:11.480 They all share, if you will, the same variable-- be it an int or str 02:04:11.480 --> 02:04:12.960 or, in this case, a list. 02:04:12.960 --> 02:04:20.240 So what I've done here is define, inside of my hat class, in a class variable 02:04:20.240 --> 02:04:21.410 called houses-- 02:04:21.410 --> 02:04:24.290 I don't say self because self is no longer relevant. 02:04:24.290 --> 02:04:26.000 Self refers to specific objects. 02:04:26.000 --> 02:04:29.360 I want a variable inside of this class, a.k.a. 02:04:29.360 --> 02:04:32.000 A class variable that equals that list. 02:04:32.000 --> 02:04:34.910 Because it's inside of this hat, now, class, 02:04:34.910 --> 02:04:38.420 I can use that list in any of my functions. 02:04:38.420 --> 02:04:40.160 I've only got one now, called sort. 02:04:40.160 --> 02:04:43.010 But if I had more, it would be accessible to all of those methods 02:04:43.010 --> 02:04:43.890 as well. 02:04:43.890 --> 02:04:47.000 And with sort, it also doesn't really make sense 02:04:47.000 --> 02:04:50.450 to sort within a specific sorting hat because, again, I 02:04:50.450 --> 02:04:51.710 only want there to be one. 02:04:51.710 --> 02:04:57.080 So I can actually specify that this is class method by saying @classmethod. 02:04:57.080 --> 02:04:59.300 And I don't pass in self anymore. 02:04:59.300 --> 02:05:04.440 I actually, by convention, pass in a reference to the class itself. 02:05:04.440 --> 02:05:06.440 It's typically written as cls. 02:05:06.440 --> 02:05:06.980 Why? 02:05:06.980 --> 02:05:12.140 Well, if you wrote C-L-A-S-S, that would actually conflict with the keyword 02:05:12.140 --> 02:05:14.010 "class" that we keep using up here. 02:05:14.010 --> 02:05:18.330 So the world realized that, oops, we can't reuse that same phrase here. 02:05:18.330 --> 02:05:20.060 So let's just call this class. 02:05:20.060 --> 02:05:22.770 This is useful in some contexts including this one. 02:05:22.770 --> 02:05:23.270 Why? 02:05:23.270 --> 02:05:25.340 Well, notice what I can now do. 02:05:25.340 --> 02:05:28.130 I can now change self to be just class. 02:05:28.130 --> 02:05:28.730 Why? 02:05:28.730 --> 02:05:33.920 Because houses now-- not an instance variable, accessible via self.houses. 02:05:33.920 --> 02:05:38.270 It is now a class variable, accessible via class.houses, 02:05:38.270 --> 02:05:41.600 or technically cls.houses in this case. 02:05:41.600 --> 02:05:43.880 But now the final flourish is this. 02:05:43.880 --> 02:05:50.420 Now, I don't have to instantiate any hat objects as I used to on here, line 13. 02:05:50.420 --> 02:05:54.030 I can just use functionality that comes with this class. 02:05:54.030 --> 02:05:56.000 So I'm going to delete that line altogether. 02:05:56.000 --> 02:06:03.620 I'm going to capitalize the hat on this new line 13 and just say hat.sort, 02:06:03.620 --> 02:06:04.550 ("Harry"). 02:06:04.550 --> 02:06:05.970 So what have I done? 02:06:05.970 --> 02:06:09.740 I've not bothered instantiating an object of type, hat. 02:06:09.740 --> 02:06:13.995 I am just accessing a class method inside of the hat class 02:06:13.995 --> 02:06:14.870 that-- you know what? 02:06:14.870 --> 02:06:16.410 Is just going to work. 02:06:16.410 --> 02:06:17.900 This is how class methods work. 02:06:17.900 --> 02:06:22.250 You use the name of the class, capital letter and all, dot method name, 02:06:22.250 --> 02:06:24.260 passing in any arguments you want. 02:06:24.260 --> 02:06:28.220 Python is going to automatically pass in some variable via which 02:06:28.220 --> 02:06:31.820 you can refer to that class in that function 02:06:31.820 --> 02:06:33.680 that you've implemented inside of that class 02:06:33.680 --> 02:06:35.330 so that I can do something like this. 02:06:35.330 --> 02:06:39.030 It's not that I want a variable called houses locally in this function, 02:06:39.030 --> 02:06:43.730 I want the variable called houses that's associated with this current class 02:06:43.730 --> 02:06:48.380 so I can still access this same list that I defined on line 6. 02:06:48.380 --> 02:06:52.700 And now, if I go back down here to my terminal and run Python of hat.py, 02:06:52.700 --> 02:06:56.090 Enter, Harry is still in Hufflepuff once more. 02:06:56.090 --> 02:06:58.250 Harry is still in Hufflepuff once more. 02:06:58.250 --> 02:07:02.440 Harry is back in Gryffindor, at least randomly. 02:07:02.440 --> 02:07:07.540 Questions, now, on these class variables or these class methods, 02:07:07.540 --> 02:07:12.640 which are in contrast with instance variables and instance methods. 02:07:12.640 --> 02:07:15.070 And the one thing, at least, that's a little strange 02:07:15.070 --> 02:07:18.880 here is that, even though there's a decorator called @classmethod, 02:07:18.880 --> 02:07:22.480 there is not one called @instancemethod. 02:07:22.480 --> 02:07:26.860 A method is just automatically a so-called "instant method" when 02:07:26.860 --> 02:07:28.750 you define it without any decorator. 02:07:28.750 --> 02:07:31.507 AUDIENCE: Can you have a class inside another class? 02:07:31.507 --> 02:07:32.507 DAVID J. MALAN: You can. 02:07:32.507 --> 02:07:34.870 You can define one class inside of another. 02:07:34.870 --> 02:07:37.270 Generally speaking, this isn't done, but there 02:07:37.270 --> 02:07:39.610 are cases where it can be helpful, especially 02:07:39.610 --> 02:07:41.830 for larger, more sophisticated programs. 02:07:41.830 --> 02:07:44.770 So yes, it is possible. 02:07:44.770 --> 02:07:45.970 Other questions. 02:07:45.970 --> 02:07:49.180 AUDIENCE: The question was about the self.houses. 02:07:49.180 --> 02:07:56.275 When we remove it and we pass data, variable is created itself, 02:07:56.275 --> 02:07:58.660 s why we remove the self? 02:07:58.660 --> 02:08:00.850 DAVID J. MALAN: So in the previous examples-- 02:08:00.850 --> 02:08:03.820 both of the hat demonstration and also all of the student 02:08:03.820 --> 02:08:07.930 demonstrations-- we were creating a student 02:08:07.930 --> 02:08:11.830 object by calling Student, capital S, open parenthesis, close parenthesis, 02:08:11.830 --> 02:08:14.320 with, eventually, name and a house passed in. 02:08:14.320 --> 02:08:18.040 And then we were using the double underscore init method 02:08:18.040 --> 02:08:23.920 to initialize the self.name and the self.house instance variables 02:08:23.920 --> 02:08:26.560 therein to those respective values. 02:08:26.560 --> 02:08:28.960 In this latest version of the sorting hat, 02:08:28.960 --> 02:08:33.370 I haven't bothered with self anywhere, only because, conceptually, I 02:08:33.370 --> 02:08:36.910 don't need or want there to be multiple hats in the world. 02:08:36.910 --> 02:08:42.250 I'm just using the class as a container to bundle up this list of houses, 02:08:42.250 --> 02:08:43.690 this sorting functionality. 02:08:43.690 --> 02:08:46.090 Maybe eventually all add more functionality to it. 02:08:46.090 --> 02:08:47.080 But that's it. 02:08:47.080 --> 02:08:50.050 And so sometimes you can use object-oriented programming 02:08:50.050 --> 02:08:53.650 in this somewhat different way when you want there to be functionality 02:08:53.650 --> 02:08:56.530 but it's not specific to any one specific hat. 02:08:56.530 --> 02:09:00.640 It's specific to the sorting hat itself. 02:09:00.640 --> 02:09:05.170 How about one other question now, on these class variables or methods-- just 02:09:05.170 --> 02:09:08.210 another way of using object-oriented programming but to solve 02:09:08.210 --> 02:09:09.730 a somewhat different problem? 02:09:09.730 --> 02:09:12.130 AUDIENCE: Well, what's the difference between the class 02:09:12.130 --> 02:09:16.027 hat and a function of hat? 02:09:16.027 --> 02:09:17.360 DAVID J. MALAN: A good question. 02:09:17.360 --> 02:09:19.690 So why are we using a class at all and not just 02:09:19.690 --> 02:09:23.140 having a file called hat.py with a variable called 02:09:23.140 --> 02:09:25.690 houses and a function called sort? 02:09:25.690 --> 02:09:27.370 Why are we adding this complexity? 02:09:27.370 --> 02:09:30.160 In this particular case, we don't necessarily need to. 02:09:30.160 --> 02:09:32.380 I could absolutely go in here. 02:09:32.380 --> 02:09:34.060 I could get rid of the class. 02:09:34.060 --> 02:09:36.850 I could undo this indentation. 02:09:36.850 --> 02:09:38.890 I could get rid of this decorator. 02:09:38.890 --> 02:09:40.750 And I could get rid of hat dot. 02:09:40.750 --> 02:09:43.480 And I could just do this and additionally 02:09:43.480 --> 02:09:45.400 let's say, let's get rid of class here. 02:09:45.400 --> 02:09:47.050 Let's get rid of class here. 02:09:47.050 --> 02:09:51.058 And now run Python of hat.py, Enter, and it still works. 02:09:51.058 --> 02:09:53.350 Put Harry in the wrong house, but that's what we have-- 02:09:53.350 --> 02:09:54.430 what happens randomly. 02:09:54.430 --> 02:09:55.690 That's fine, too. 02:09:55.690 --> 02:09:58.960 What we're introducing today, by way of object-oriented programming, 02:09:58.960 --> 02:10:01.420 is just a different way of modeling the world. 02:10:01.420 --> 02:10:04.120 It's not really compelling with an example like this, 02:10:04.120 --> 02:10:06.350 frankly, that's relatively simple. 02:10:06.350 --> 02:10:07.328 It's not very complex. 02:10:07.328 --> 02:10:08.620 There's not much functionality. 02:10:08.620 --> 02:10:12.070 Honestly, the version that we just typed up-- these 10 lines-- this is fine. 02:10:12.070 --> 02:10:13.400 This solves this problem. 02:10:13.400 --> 02:10:16.912 But as our code gets longer, as we start collaborating with other people, 02:10:16.912 --> 02:10:19.870 as the problems we're trying to solve with code get more sophisticated, 02:10:19.870 --> 02:10:23.317 you're going to find that your code gets messy quickly. 02:10:23.317 --> 02:10:26.650 And you're going to find that you have a huge number of functions, for instance, 02:10:26.650 --> 02:10:27.700 in one file. 02:10:27.700 --> 02:10:31.180 And some of them are related to each other, but some of them are not. 02:10:31.180 --> 02:10:33.815 Well, at that point, wouldn't it be nice to just organize them 02:10:33.815 --> 02:10:34.690 a little differently? 02:10:34.690 --> 02:10:37.690 And in the world of Harry Potter, let's have a class for student; 02:10:37.690 --> 02:10:41.140 let's have a class for Professor; let's have a class for the sorting hat; 02:10:41.140 --> 02:10:42.920 let's have a class for something else. 02:10:42.920 --> 02:10:45.820 And so once your world gets much more complicated than some 02:10:45.820 --> 02:10:49.000 of the demonstrations we do here in class when we want to focus 02:10:49.000 --> 02:10:52.690 on individual ideas, object-oriented programming is just a way 02:10:52.690 --> 02:10:55.300 of encapsulating related data-- 02:10:55.300 --> 02:10:58.510 that is, variables-- related functionality-- that is, methods-- 02:10:58.510 --> 02:11:00.790 inside of things that have names. 02:11:00.790 --> 02:11:02.840 These things are called classes. 02:11:02.840 --> 02:11:04.940 So it's just another way to solve problems. 02:11:04.940 --> 02:11:07.330 And when we focused on libraries a couple of weeks back, 02:11:07.330 --> 02:11:10.000 that, too, was another solution to the same problem. 02:11:10.000 --> 02:11:13.480 You could define your own modules or packages, put some of your data 02:11:13.480 --> 02:11:15.910 and/or functionality in there, and that's fine, too. 02:11:15.910 --> 02:11:19.210 And sometimes which one you should use overlaps. 02:11:19.210 --> 02:11:22.540 If you're familiar with Venn diagrams, the overlapping region 02:11:22.540 --> 02:11:25.990 might mean that you could use a class; you could use a module or a package; 02:11:25.990 --> 02:11:28.720 you could just use a single local file. 02:11:28.720 --> 02:11:31.120 Over time, you'll develop an instinct and maybe even 02:11:31.120 --> 02:11:35.480 a personal preference for which tool to use. 02:11:35.480 --> 02:11:39.620 All right, let me propose, now, that we apply this same idea of a class method 02:11:39.620 --> 02:11:41.810 to clean up one other thing as well. 02:11:41.810 --> 02:11:47.240 Let me close that hat.py and reopen student.py as we left it earlier, 02:11:47.240 --> 02:11:50.700 and let me go ahead and simplify it just a little bit. 02:11:50.700 --> 02:11:53.963 I'm going to go ahead and get rid of the properties, 02:11:53.963 --> 02:11:55.880 not because there's anything, wrong with them, 02:11:55.880 --> 02:11:58.610 but just because I want us to focus on some of the key ideas 02:11:58.610 --> 02:12:00.540 when we began with this program. 02:12:00.540 --> 02:12:02.940 So I'm going to go ahead and keep main as well. 02:12:02.940 --> 02:12:06.800 I'm not going to adversarially try to change Henry's address there. 02:12:06.800 --> 02:12:09.870 I'm going to instead go ahead, though, and just print the student. 02:12:09.870 --> 02:12:12.950 But this is the thing I want to focus on here. 02:12:12.950 --> 02:12:17.750 This, in our previous student examples, was a missed opportunity 02:12:17.750 --> 02:12:19.340 to clean up my code. 02:12:19.340 --> 02:12:20.910 Well, what do I mean by that? 02:12:20.910 --> 02:12:22.970 Well, up here at the top of this file-- even 02:12:22.970 --> 02:12:25.160 though I've simplified it, but getting rid of the properties 02:12:25.160 --> 02:12:26.910 and all of that error checking-- because I 02:12:26.910 --> 02:12:29.930 want to focus on the essence of this class now-- just the student's name 02:12:29.930 --> 02:12:32.070 and the house and the printing thereof. 02:12:32.070 --> 02:12:36.200 This is, by nature of classes in object-oriented programming, 02:12:36.200 --> 02:12:40.880 theoretically, all of my student-specific functionality. 02:12:40.880 --> 02:12:45.300 That is to say, if I have functionality and data related to a student, you, 02:12:45.300 --> 02:12:47.780 the programmer, my colleague, would assume 02:12:47.780 --> 02:12:50.090 that it's all bundled up, encapsulated, so 02:12:50.090 --> 02:12:52.010 to speak, inside of the student class. 02:12:52.010 --> 02:12:55.400 And yet, if you scroll down further, what is this? 02:12:55.400 --> 02:12:58.670 There's a function called get_student that just exists elsewhere 02:12:58.670 --> 02:13:02.390 in this file that prompts the user for a name, prompts the user for a house, 02:13:02.390 --> 02:13:05.300 creates the student object, and then returns. 02:13:05.300 --> 02:13:06.710 That's not wrong. 02:13:06.710 --> 02:13:07.400 It works. 02:13:07.400 --> 02:13:10.250 And we saw many, many times it kept working. 02:13:10.250 --> 02:13:13.400 But this is a little weird because, if this 02:13:13.400 --> 02:13:16.430 is a function that helps you get a student, 02:13:16.430 --> 02:13:20.120 helps you get the name of a student and the house of a student, why isn't 02:13:20.120 --> 02:13:22.350 that functionality in the class itself? 02:13:22.350 --> 02:13:25.487 After all, as my code gets more and more complicated and does more things, 02:13:25.487 --> 02:13:27.320 I'm going to be looking at the student class 02:13:27.320 --> 02:13:28.975 for all student-related functionality. 02:13:28.975 --> 02:13:31.850 I'm not going to be scrolling down, expecting that, oh, maybe there's 02:13:31.850 --> 02:13:35.900 some other student functionality just randomly later in this file. 02:13:35.900 --> 02:13:37.160 So it's not wrong. 02:13:37.160 --> 02:13:40.820 But this is, again, evidence of maybe bad design-- 02:13:40.820 --> 02:13:42.770 not so much with this small program. 02:13:42.770 --> 02:13:45.680 But this is an example, again, of code smell. 02:13:45.680 --> 02:13:47.218 Something smells a little off here. 02:13:47.218 --> 02:13:49.010 This is probably going to get us in trouble 02:13:49.010 --> 02:13:52.020 by separating related functionality. 02:13:52.020 --> 02:13:55.730 So again it's a design principle, not a correctness concern. 02:13:55.730 --> 02:13:59.180 But class methods allow us to address this, too. 02:13:59.180 --> 02:14:01.020 Let me go ahead and do this. 02:14:01.020 --> 02:14:03.860 I'm going to delete get_student all together, leaving 02:14:03.860 --> 02:14:06.680 only main as my other function here. 02:14:06.680 --> 02:14:09.740 And inside of my student class, I'm going to do this. 02:14:09.740 --> 02:14:13.580 I'm going to define a function, even more simply called, get. 02:14:13.580 --> 02:14:16.490 And by nature of how class methods work, it's 02:14:16.490 --> 02:14:18.830 going to take in the name of the class itself 02:14:18.830 --> 02:14:21.050 or a reference thereto as an argument. 02:14:21.050 --> 02:14:24.300 And I'm going to move the functionality from get_student into the student 02:14:24.300 --> 02:14:24.800 class. 02:14:24.800 --> 02:14:29.960 And I'm going to do this-- name equals input, quote, unquote, name, house 02:14:29.960 --> 02:14:32.900 equals input, quote, unquote, house. 02:14:32.900 --> 02:14:35.510 And then what this function is going to do 02:14:35.510 --> 02:14:42.530 is return a new student object by calling class, which, again, is just 02:14:42.530 --> 02:14:45.470 an automatically passed-in reference to the class 02:14:45.470 --> 02:14:48.890 itself, passing in name and house. 02:14:48.890 --> 02:14:54.122 And I will admit this syntax seems a little strange that now I'm calling cls 02:14:54.122 --> 02:14:55.580 and I'm passing in these arguments. 02:14:55.580 --> 02:14:57.320 But let me do one final fix here. 02:14:57.320 --> 02:14:59.270 Let me go to the top of this function and more 02:14:59.270 --> 02:15:02.330 explicitly say this is a class method. 02:15:02.330 --> 02:15:05.760 This solves a potential chicken and the egg problem, so to speak, 02:15:05.760 --> 02:15:08.480 whereby one needs to come before the other, potentially. 02:15:08.480 --> 02:15:10.460 So what am I doing here? 02:15:10.460 --> 02:15:15.110 Inside of my student class, I now have a function called get. 02:15:15.110 --> 02:15:18.540 It is, I shall claim, a class method what does that mean. 02:15:18.540 --> 02:15:24.200 It just means I can call this method without instantiating a student 02:15:24.200 --> 02:15:25.340 object first. 02:15:25.340 --> 02:15:28.538 Therein lies the potential chicken and the egg problem. 02:15:28.538 --> 02:15:31.580 And if unfamiliar, that's an expression, meaning, well, and did the world 02:15:31.580 --> 02:15:34.520 have chickens first that laid eggs, or was there an egg 02:15:34.520 --> 02:15:38.180 that then yielded the chickens, but how did the egg get there? 02:15:38.180 --> 02:15:40.003 It's this weird, circular problem. 02:15:40.003 --> 02:15:41.420 And that's what we're facing here. 02:15:41.420 --> 02:15:47.600 It would be weird if you had to create a student object in order to call get, 02:15:47.600 --> 02:15:51.020 in order to get another student object. 02:15:51.020 --> 02:15:52.070 That sounds messy. 02:15:52.070 --> 02:15:56.060 Let's just get a student via a class method 02:15:56.060 --> 02:16:00.350 that, by definition, does not require you to create a student object first. 02:16:00.350 --> 02:16:04.370 Just like the hat, in its final form, we use the hat class 02:16:04.370 --> 02:16:07.040 to just say Hat, capital H, dot sort. 02:16:07.040 --> 02:16:08.660 We didn't need to create a hat first. 02:16:08.660 --> 02:16:10.920 We just used the class itself. 02:16:10.920 --> 02:16:12.980 So what am I going to do here now? 02:16:12.980 --> 02:16:14.240 Let me go down to main. 02:16:14.240 --> 02:16:18.020 And instead of saying get_student, notice what I can now do. 02:16:18.020 --> 02:16:22.850 Student.get, and everything else can stay the same. 02:16:22.850 --> 02:16:26.630 All I've done now is I've migrated all of my logic 02:16:26.630 --> 02:16:29.750 from get_student, which was this own standalone function, 02:16:29.750 --> 02:16:32.510 but clearly related to students by name. 02:16:32.510 --> 02:16:36.670 I've moved the same code, really, to inside 02:16:36.670 --> 02:16:40.660 of the student class in a more simply named function called get. 02:16:40.660 --> 02:16:42.730 But I could still call it get_student if I want. 02:16:42.730 --> 02:16:46.000 It just seems a little redundant to call it get_student in a student class, 02:16:46.000 --> 02:16:47.480 so I'm simplifying. 02:16:47.480 --> 02:16:51.400 So I have a method called get, but I'm calling it a class method 02:16:51.400 --> 02:16:53.540 to avoid that chicken and the egg problem. 02:16:53.540 --> 02:16:59.170 I want to be able to call a get without having a student object in my universe 02:16:59.170 --> 02:17:00.010 already. 02:17:00.010 --> 02:17:03.250 And the syntax for that is @classmethod. 02:17:03.250 --> 02:17:06.549 The convention is to give this method at least one argument, 02:17:06.549 --> 02:17:09.520 by convention called cls for class, which is just going 02:17:09.520 --> 02:17:11.320 to be a reference to the class itself. 02:17:11.320 --> 02:17:14.709 Lines 11 and 12 are identical to what they've always been. 02:17:14.709 --> 02:17:17.889 And get_student-- the only new syntax here is this, 02:17:17.889 --> 02:17:21.700 but this, again, is one of the features of object-oriented programming. 02:17:21.700 --> 02:17:29.020 You can now instantiate a student object by just using cls that's passed in. 02:17:29.020 --> 02:17:31.780 I technically could use Student, capital S, 02:17:31.780 --> 02:17:34.540 but it turns out I'm doing what's more conventional because this 02:17:34.540 --> 02:17:38.680 will both solve and avoid problems down the line with more complicated code. 02:17:38.680 --> 02:17:43.138 This line here, on line 13, just means create an object of the current class. 02:17:43.138 --> 02:17:43.930 What class is that? 02:17:43.930 --> 02:17:45.010 Well, whatever cls is. 02:17:45.010 --> 02:17:48.670 Well, that, by definition of how it all works, is going to be student. 02:17:48.670 --> 02:17:53.440 And I want you to initialize it, as always, with name and house. 02:17:53.440 --> 02:17:56.860 So now, scrolling down, my code is this. 02:17:56.860 --> 02:17:59.080 And this is just nice to read. 02:17:59.080 --> 02:18:02.980 You perhaps have to acquire a taste for this-- and I sound a little odd saying, 02:18:02.980 --> 02:18:03.969 this is nice to read. 02:18:03.969 --> 02:18:07.330 But indeed, student.get just tells me what's going on. 02:18:07.330 --> 02:18:08.590 I'm going to get a student. 02:18:08.590 --> 02:18:12.160 I don't need a separate function written by me called get_student in the file 02:18:12.160 --> 02:18:12.950 itself. 02:18:12.950 --> 02:18:16.090 The get functionality is built into the class. 02:18:16.090 --> 02:18:18.620 All my student-related code now is together. 02:18:18.620 --> 02:18:22.240 So let me go down to my terminal window and run Python of student.py, Enter. 02:18:22.240 --> 02:18:23.290 Let's type in Harry. 02:18:23.290 --> 02:18:24.610 Let's type in Gryffindor. 02:18:24.610 --> 02:18:26.170 And we're back to where we began. 02:18:26.170 --> 02:18:31.750 But, but, but everything related to students, now, is in this here class. 02:18:31.750 --> 02:18:35.709 The only other thing in the file is main and this conditional 02:18:35.709 --> 02:18:39.670 that we always use to avoid accidentally executing main when we're making 02:18:39.670 --> 02:18:42.049 a module or a package or the like. 02:18:42.049 --> 02:18:44.770 So again, a solution to a problem-- 02:18:44.770 --> 02:18:48.070 not a big one in the case of a relatively small program, but one 02:18:48.070 --> 02:18:50.260 that you will eventually encounter as your programs 02:18:50.260 --> 02:18:55.299 get longer and longer, with more and more entities to represent. 02:18:55.299 --> 02:19:00.220 Questions now on this use of a class method. 02:19:00.220 --> 02:19:03.520 MICHAEL: Does the class have to be defined before the main function, 02:19:03.520 --> 02:19:05.684 in terms of the order of the program? 02:19:05.684 --> 02:19:07.309 DAVID J. MALAN: A really good question. 02:19:07.309 --> 02:19:08.480 So when in doubt, let's try this. 02:19:08.480 --> 02:19:09.855 So let's try to change the order. 02:19:09.855 --> 02:19:12.400 Let's move main to the top, which I've often encouraged. 02:19:12.400 --> 02:19:15.400 So let's go ahead and, above the class, do this. 02:19:15.400 --> 02:19:18.190 And notice now that, technically, line two 02:19:18.190 --> 02:19:22.660 is mentioning student, which does not exist until line 6 and below. 02:19:22.660 --> 02:19:25.870 Let me go ahead and clear my terminal and run Python of student.py. 02:19:25.870 --> 02:19:27.070 So far, so good. 02:19:27.070 --> 02:19:29.950 Harry-- Gryffindor, OK. 02:19:29.950 --> 02:19:31.660 Indeed, Harry's from Gryffindor. 02:19:31.660 --> 02:19:34.750 The reason, Michael, it does not matter in this case 02:19:34.750 --> 02:19:38.020 is because we're not actually calling main until the very end. 02:19:38.020 --> 02:19:41.590 And just as in the past, that means that Python has a chance to read everything, 02:19:41.590 --> 02:19:42.830 top to bottom, left to right. 02:19:42.830 --> 02:19:44.290 So everything exists. 02:19:44.290 --> 02:19:48.670 I would say, generally classes are defined at the top of the file. 02:19:48.670 --> 02:19:53.110 However, it would be even maybe cleaner to move the Classes definition 02:19:53.110 --> 02:19:56.290 to its own file and then import it, so essentially 02:19:56.290 --> 02:19:59.950 to make reusable code by putting it into your own module or package 02:19:59.950 --> 02:20:02.290 so that not just this program but many others 02:20:02.290 --> 02:20:04.760 can use that definition of student as well. 02:20:04.760 --> 02:20:08.950 Other questions now on classes, class methods, or the like. 02:20:08.950 --> 02:20:16.060 AUDIENCE: I wanted to ask, is there a way to declare all the possible-- 02:20:16.060 --> 02:20:18.790 all the possible attributes of the class? 02:20:18.790 --> 02:20:22.200 Because it looks so inconsistent. 02:20:22.200 --> 02:20:23.950 DAVID J. MALAN: Well, so my takeaway there 02:20:23.950 --> 02:20:26.407 is this is Python's approach to these principles. 02:20:26.407 --> 02:20:28.990 Different languages, like Java, just take a different approach 02:20:28.990 --> 02:20:30.890 but have very similar features. 02:20:30.890 --> 02:20:32.690 The syntax just tends to vary. 02:20:32.690 --> 02:20:36.430 And this is how the Python community chose to implement this idea. 02:20:36.430 --> 02:20:39.970 The right mental model, ultimately, is that these instance variables, 02:20:39.970 --> 02:20:46.150 instant methods belong to or operate on specific objects-- 02:20:46.150 --> 02:20:48.970 a specific student, a specific hat. 02:20:48.970 --> 02:20:53.710 Class variables and class methods operate on the entire class 02:20:53.710 --> 02:20:57.340 itself or, in turn, all objects of that class, which 02:20:57.340 --> 02:21:01.210 we've not seen a demonstration of, but it's a higher level concept. 02:21:01.210 --> 02:21:03.790 So it turns out, besides these class methods, 02:21:03.790 --> 02:21:06.850 which are distinct from those instance methods, which, to be fair, 02:21:06.850 --> 02:21:10.600 do not have their own decorator-- they just are, by default, instance method, 02:21:10.600 --> 02:21:13.660 there's yet other types of Methods You can have in classes in Python. 02:21:13.660 --> 02:21:16.090 They tend to be called static methods, and they, too, 02:21:16.090 --> 02:21:19.240 come with another decorator called @static method, which 02:21:19.240 --> 02:21:20.860 is a rabbit hole we won't go down. 02:21:20.860 --> 02:21:23.350 But realize that there is yet other functionality 02:21:23.350 --> 02:21:25.960 that you can leverage within object-oriented programming. 02:21:25.960 --> 02:21:30.250 But what we thought we'd do is focus really on some final core features 02:21:30.250 --> 02:21:33.380 that you see not just in Python but other languages as well. 02:21:33.380 --> 02:21:36.940 And perhaps one of the most compelling features of object-oriented programming 02:21:36.940 --> 02:21:40.570 that we haven't yet used explicitly-- though it turns out we've seen 02:21:40.570 --> 02:21:42.670 implicitly over the past weeks-- 02:21:42.670 --> 02:21:44.830 is this notion of inheritance. 02:21:44.830 --> 02:21:47.470 It turns out, via object-oriented programming, 02:21:47.470 --> 02:21:50.860 there's actually an opportunity to design your classes 02:21:50.860 --> 02:21:56.350 in a hierarchical fashion, whereby you can have one class inherit from 02:21:56.350 --> 02:22:02.050 or borrow attributes-- that is, methods or variables from another class 02:22:02.050 --> 02:22:04.520 if they all have those in common. 02:22:04.520 --> 02:22:06.310 So what do I mean by this here? 02:22:06.310 --> 02:22:10.900 Well, let me propose that we implement, over in VS Code here, 02:22:10.900 --> 02:22:13.450 a brand new file called wizard.py. 02:22:13.450 --> 02:22:16.780 Let me go ahead and run code of wizard.py. 02:22:16.780 --> 02:22:21.550 And then let's start as before, defining a class called student. 02:22:21.550 --> 02:22:25.605 And let's go ahead and first define the underscore underscore, init method, 02:22:25.605 --> 02:22:28.480 which of course, is minimally going to take an argument traditionally 02:22:28.480 --> 02:22:29.200 called self. 02:22:29.200 --> 02:22:32.590 And in this case, let's also have it take as before a name and a house. 02:22:32.590 --> 02:22:35.080 And then in this init method, let's go ahead 02:22:35.080 --> 02:22:39.430 and assign the instance variables-- self.name = name, 02:22:39.430 --> 02:22:42.400 and self.house = house. 02:22:42.400 --> 02:22:44.590 Let's assume that there's some other functionality 02:22:44.590 --> 02:22:46.300 in this class as well-- dot, dot, dot. 02:22:46.300 --> 02:22:49.630 But let's move on now to implementing the notion of a professor 02:22:49.630 --> 02:22:51.410 in the wizarding world as well. 02:22:51.410 --> 02:22:55.840 So for this class, let's call it Professor. 02:22:55.840 --> 02:22:58.180 And a professor, let's say, is also going 02:22:58.180 --> 02:23:00.620 to have its own initialization method. 02:23:00.620 --> 02:23:01.900 So __ init. 02:23:01.900 --> 02:23:03.190 It's going to take self-- 02:23:03.190 --> 02:23:04.990 always as the first argument. 02:23:04.990 --> 02:23:06.710 A professor also has a name. 02:23:06.710 --> 02:23:08.380 So we'll pass that in second, too. 02:23:08.380 --> 02:23:11.170 And even though some professors are heads of houses, 02:23:11.170 --> 02:23:13.270 let's assume that a professor is really identified 02:23:13.270 --> 02:23:16.720 by their name and their subject area-- the class that they teach. 02:23:16.720 --> 02:23:19.240 So we'll call this third argument, subject. 02:23:19.240 --> 02:23:23.020 Now, as before, let's go ahead and assign self.name = name, 02:23:23.020 --> 02:23:27.310 and let's assign self.subject = subject here. 02:23:27.310 --> 02:23:30.730 And as before, let's assume that there's some more functionality associated 02:23:30.730 --> 02:23:32.290 with professors as well. 02:23:32.290 --> 02:23:36.820 Well, what do you notice already here in my definitions of students 02:23:36.820 --> 02:23:38.530 and professors? 02:23:38.530 --> 02:23:42.940 Typically, we're a bit reluctant to allow for any redundancy in our code. 02:23:42.940 --> 02:23:47.230 And here, I feel like my init method is taking a name for students; 02:23:47.230 --> 02:23:49.840 my init method is also taking a name for a professor; 02:23:49.840 --> 02:23:53.680 and I have these identical lines of code, like self.name = name. 02:23:53.680 --> 02:23:56.410 And this is only going to get exacerbated if I now go and add 02:23:56.410 --> 02:23:57.280 some error checking. 02:23:57.280 --> 02:24:01.143 So for instance, how about if not name, we 02:24:01.143 --> 02:24:03.310 should probably be in the habit of raising something 02:24:03.310 --> 02:24:07.420 like a value error in an explanatory message, like "Missing name." 02:24:07.420 --> 02:24:08.380 And you know what? 02:24:08.380 --> 02:24:10.960 If a professor is missing their name, I should probably 02:24:10.960 --> 02:24:12.500 copy, paste that code down here. 02:24:12.500 --> 02:24:14.860 And that's where red flags should be going off, 02:24:14.860 --> 02:24:17.140 whereby, as soon as you start copy pasting code, 02:24:17.140 --> 02:24:21.430 there's probably a better way so that we can write the code once and perhaps 02:24:21.430 --> 02:24:22.690 reuse it in some way. 02:24:22.690 --> 02:24:26.120 And here, too, object-oriented programming offers a solution. 02:24:26.120 --> 02:24:29.230 It turns out that object-oriented programming in Python 02:24:29.230 --> 02:24:34.660 also supports inheritance, whereby you can define multiple classes that 02:24:34.660 --> 02:24:36.350 somehow relate to one another. 02:24:36.350 --> 02:24:39.040 They don't need to exist in parallel in this way. 02:24:39.040 --> 02:24:41.570 There could actually be some hierarchy between them. 02:24:41.570 --> 02:24:43.540 So for instance, in the wizarding world, we 02:24:43.540 --> 02:24:46.810 could argue that both a student and a professor are, at the end of the day, 02:24:46.810 --> 02:24:47.500 Wizards. 02:24:47.500 --> 02:24:50.890 So maybe what we should really define is a third class, 02:24:50.890 --> 02:24:53.170 for instance, called wizard, that has any 02:24:53.170 --> 02:24:57.125 of the common attributes for students and professors alike. 02:24:57.125 --> 02:24:59.000 And for now, we've kept it relatively simple. 02:24:59.000 --> 02:25:01.870 The only thing they have in common is a name and a name, 02:25:01.870 --> 02:25:03.680 in student and professor, respectively. 02:25:03.680 --> 02:25:06.610 So why don't we minimally factor that out first? 02:25:06.610 --> 02:25:08.275 All right, so let me go ahead here. 02:25:08.275 --> 02:25:10.900 And just to keep things organized, at the top of my file, let's 02:25:10.900 --> 02:25:13.060 define a third class called Wizard. 02:25:13.060 --> 02:25:16.400 And a wizard will have its own initialization method. 02:25:16.400 --> 02:25:20.770 So def __init__(self), as always. 02:25:20.770 --> 02:25:23.530 And a wizard, let's say for now, is only going 02:25:23.530 --> 02:25:26.830 to be initialized with their name in this way. 02:25:26.830 --> 02:25:29.990 And now, I'm going to go ahead and do some of that error checking. 02:25:29.990 --> 02:25:33.920 So if not name will raise a value error in the wizard class. 02:25:33.920 --> 02:25:37.240 Otherwise, we'll go ahead and do self name equals name, 02:25:37.240 --> 02:25:40.870 and, heck, dot, dot, dot, maybe some other functionality as well. 02:25:40.870 --> 02:25:44.240 But not a subject, which is specific to professors, and not a house, 02:25:44.240 --> 02:25:46.390 which I've claimed is specific to students. 02:25:46.390 --> 02:25:49.570 Now, I think we can begin to maybe remove 02:25:49.570 --> 02:25:52.040 some of the redundancies in our other classes here. 02:25:52.040 --> 02:25:55.630 So for instance, down with student, why don't I 02:25:55.630 --> 02:25:59.950 go ahead and remove this error checking here and remove this error-- 02:25:59.950 --> 02:26:03.370 this assignment of self.name = name because I'm already 02:26:03.370 --> 02:26:04.513 doing that in Wizard. 02:26:04.513 --> 02:26:07.180 And similarly, down here, in Professor, why don't I do the same? 02:26:07.180 --> 02:26:08.810 Let's get rid of the error checking. 02:26:08.810 --> 02:26:12.280 Let's get rid of self.name = name because, again, I'm doing that already 02:26:12.280 --> 02:26:14.270 up there for Wizard as well. 02:26:14.270 --> 02:26:17.140 But at the moment, even though they're all in the same file, 02:26:17.140 --> 02:26:21.880 I haven't told Python that a student is a wizard and a professor is a wizard. 02:26:21.880 --> 02:26:24.190 So I really need to link these two together. 02:26:24.190 --> 02:26:28.690 And the way you can prescribe inheritance, whereby one class should 02:26:28.690 --> 02:26:31.270 inherit from another, or conversely, one class 02:26:31.270 --> 02:26:34.750 should descend from another-- we can do this. 02:26:34.750 --> 02:26:36.340 I can say class Student. 02:26:36.340 --> 02:26:39.610 But before the colon, I can go in and say in parentheses, 02:26:39.610 --> 02:26:45.130 a student inherits from, or is a subclass of wizard, which, conversely, 02:26:45.130 --> 02:26:47.890 is the superclass of the student class. 02:26:47.890 --> 02:26:50.810 So this just means that, when I define a student class, 02:26:50.810 --> 02:26:55.180 go ahead and inherit all of the characteristics of a wizard as well. 02:26:55.180 --> 02:26:58.090 And I'm going to do the same thing for Professor. 02:26:58.090 --> 02:27:01.360 So (Wizard) after the class name Professor, 02:27:01.360 --> 02:27:05.030 and that's going to give me access to some of that same functionality. 02:27:05.030 --> 02:27:08.680 But because my student class and my professor class 02:27:08.680 --> 02:27:10.615 still have their same init methods, those 02:27:10.615 --> 02:27:12.490 are the methods that are going to get called. 02:27:12.490 --> 02:27:16.120 Whenever I create a student in code or I create a professor in code, 02:27:16.120 --> 02:27:18.820 I need to somehow explicitly say that I also 02:27:18.820 --> 02:27:23.110 want to use the functionality in the Wizard class's init method. 02:27:23.110 --> 02:27:26.120 And the way to do this in Python is as follows. 02:27:26.120 --> 02:27:28.150 Let me go into my init method for Student, 02:27:28.150 --> 02:27:31.180 and let me call super, with no arguments, which 02:27:31.180 --> 02:27:34.550 is a reference to the superclass of this class. 02:27:34.550 --> 02:27:37.120 So if this class is Student, the superclass-- that is, 02:27:37.120 --> 02:27:38.980 the parent class-- is Wizard. 02:27:38.980 --> 02:27:45.070 So super() will have the effect of accessing the superclass. 02:27:45.070 --> 02:27:48.940 And then I'm going to go ahead and explicitly call its init method, 02:27:48.940 --> 02:27:52.630 and I'm going to pass to the Wizard's init method 02:27:52.630 --> 02:27:56.160 the name that the student's init method was passed. 02:27:56.160 --> 02:27:59.190 And I'm going to go ahead and do the same down here in Wizard. 02:27:59.190 --> 02:28:00.870 This is one line of copy, paste. 02:28:00.870 --> 02:28:03.600 But I think I'm OK with it here because it's still 02:28:03.600 --> 02:28:05.490 allowing me to do all of the name assignment 02:28:05.490 --> 02:28:08.190 and the error checking up in the Wizard class instead. 02:28:08.190 --> 02:28:13.470 I think we're OK now by just calling super.init for both student 02:28:13.470 --> 02:28:14.610 and Professor alike. 02:28:14.610 --> 02:28:18.420 Now, admittedly, this syntax is definitely out there-- the fact 02:28:18.420 --> 02:28:20.760 that we're calling super in parentheses and dots 02:28:20.760 --> 02:28:23.670 and underscore underscore on the left and the right of init here, 02:28:23.670 --> 02:28:26.100 but it's just a combination of these two ideas. 02:28:26.100 --> 02:28:32.130 super() is a way of programmatically accessing a current class's parent 02:28:32.130 --> 02:28:36.480 class, or superclass, and __init, of course, is just referring to, now, 02:28:36.480 --> 02:28:39.243 that class's own initialization method. 02:28:39.243 --> 02:28:41.160 Now, per the dot, dot, dot-- so there could be 02:28:41.160 --> 02:28:42.702 a lot more going on in these classes. 02:28:42.702 --> 02:28:46.410 But what's nice now is that Wizard as a class 02:28:46.410 --> 02:28:49.500 is taking care of all of the assignment of a wizard's name, 02:28:49.500 --> 02:28:51.652 whether that wizard is a student or a professor. 02:28:51.652 --> 02:28:53.610 And it's even doing some error checking to make 02:28:53.610 --> 02:28:56.320 sure the name was actually passed in. 02:28:56.320 --> 02:29:00.390 Meanwhile, student is inheriting all of that functionality 02:29:00.390 --> 02:29:03.540 and using it by calling the superclass's own init method. 02:29:03.540 --> 02:29:06.270 But it's additionally taking the house, that's presumably 02:29:06.270 --> 02:29:08.580 passed into the student constructor function, 02:29:08.580 --> 02:29:12.060 and assigning it to its own instance variable-- self.house, 02:29:12.060 --> 02:29:15.660 and similarly, professor, or restoring in self.subject 02:29:15.660 --> 02:29:19.090 the subject that was passed into that one as well. 02:29:19.090 --> 02:29:21.120 Now, how might we use these classes? 02:29:21.120 --> 02:29:24.690 Well, we'll continue to wave our hands with a little bit of detail here. 02:29:24.690 --> 02:29:27.960 But at the bottom of this file, or any other file that imports this one, 02:29:27.960 --> 02:29:30.060 I could now write code like this. 02:29:30.060 --> 02:29:32.400 I could create a student variable and assign 02:29:32.400 --> 02:29:34.680 it the return value of the student constructor call. 02:29:34.680 --> 02:29:38.820 and maybe that student is named Harry and that student's house, 02:29:38.820 --> 02:29:41.250 for instance, might be Gryffindor. 02:29:41.250 --> 02:29:45.970 And meanwhile, I might do something like this. professor = Professor over here. 02:29:45.970 --> 02:29:49.740 And notice, the lowercase S on the left, capital S on the right. 02:29:49.740 --> 02:29:53.070 Same for professor on the left-- lowercase and uppercase on the right 02:29:53.070 --> 02:29:53.940 respectively. 02:29:53.940 --> 02:29:56.820 Professor, quote, unquote, "Severus," and how 02:29:56.820 --> 02:30:01.710 about Defense Against the Dark Arts will be his subject? 02:30:01.710 --> 02:30:04.020 And meanwhile, if we want, more generically, 02:30:04.020 --> 02:30:08.490 just a wizard, who, at the moment is neither student nor professor 02:30:08.490 --> 02:30:10.590 teaching classes actively, we could even do that. 02:30:10.590 --> 02:30:14.670 We could do wizard = Wizard in capital W on the right-hand side 02:30:14.670 --> 02:30:16.920 of the equal sign, because it's the name of the class. 02:30:16.920 --> 02:30:18.390 And someone like Albus-- 02:30:18.390 --> 02:30:21.150 passing in only Albus's name-- 02:30:21.150 --> 02:30:26.910 not a house, not a subject, because, in this case, he's known only as a wizard. 02:30:26.910 --> 02:30:29.970 Meanwhile, with each of these calls, this line of code 02:30:29.970 --> 02:30:33.030 here will ensure that the init method for the wizard class is called. 02:30:33.030 --> 02:30:36.810 This line of code here will ensure that the init method of the student 02:30:36.810 --> 02:30:40.440 class and, in turn, the init method of the superclass wizard is called. 02:30:40.440 --> 02:30:42.750 And then lastly, on this final line of code, 02:30:42.750 --> 02:30:46.830 will this syntax ensure that the init method of the professor class 02:30:46.830 --> 02:30:51.580 is called, which, in turn, calls the init method of the superclass as well. 02:30:51.580 --> 02:30:55.300 Any questions now on this idea of inheritance, 02:30:55.300 --> 02:31:00.330 which is a key feature of a lot of object-oriented programming languages? 02:31:04.670 --> 02:31:07.920 MICHAEL: From what I've seen so far, a lot of times, there's a lot of nesting. 02:31:07.920 --> 02:31:09.600 If you do super, does it go one up? 02:31:09.600 --> 02:31:15.600 Is there any situation where it's nested in another class as well, above Wizard, 02:31:15.600 --> 02:31:16.100 let's say? 02:31:16.100 --> 02:31:17.725 DAVID J. MALAN: A really good question. 02:31:17.725 --> 02:31:19.590 If you were to have a super superclass-- so 02:31:19.590 --> 02:31:24.060 your hierarchy is even taller than the two levels of hierarchy 02:31:24.060 --> 02:31:27.840 that we currently have, absolutely. 02:31:27.840 --> 02:31:30.450 What's nice about inheritance, as the name implies, is, 02:31:30.450 --> 02:31:33.810 just as you might have inherited certain traits as a human 02:31:33.810 --> 02:31:37.200 from your grandfather and grandmother or your great-grandfather 02:31:37.200 --> 02:31:39.540 or great-grandmother, some of those properties 02:31:39.540 --> 02:31:44.290 can actually trickle down to you in the context of code as well. 02:31:44.290 --> 02:31:48.480 So when you descend from another class-- 02:31:48.480 --> 02:31:52.770 that is, when you subclass a superclass or a super superclass, 02:31:52.770 --> 02:31:55.170 you actually do inherit all of the functionality, 02:31:55.170 --> 02:31:57.840 not just from one level above you but from two or three, 02:31:57.840 --> 02:32:00.780 so you can indeed access some of that functionality as well. 02:32:00.780 --> 02:32:03.870 And you can even override it if you want some of these classes 02:32:03.870 --> 02:32:06.960 to behave a little bit differently than others. 02:32:06.960 --> 02:32:09.350 Other questions on inheritance. 02:32:09.350 --> 02:32:11.100 AUDIENCE: So it's similar to the last one, 02:32:11.100 --> 02:32:14.315 but can you have two parents on the same level? 02:32:14.315 --> 02:32:15.940 DAVID J. MALAN: A really good question. 02:32:15.940 --> 02:32:21.080 So there are ways to implement descendants from multiple parents. 02:32:21.080 --> 02:32:24.330 And there's different ways to do this, not just in Python but other languages. 02:32:24.330 --> 02:32:29.130 We've kept things simple here, though, by having a single inheritance path. 02:32:29.130 --> 02:32:29.880 A good question. 02:32:29.880 --> 02:32:32.490 How about one more question on inheritance? 02:32:32.490 --> 02:32:38.400 AUDIENCE: Can we have multiple arguments in super.__init? 02:32:38.400 --> 02:32:42.210 DAVID J. MALAN: Yes, but in this case, I'm only passing a name on line 18, 02:32:42.210 --> 02:32:44.860 and I'm only passing in name on line 10. 02:32:44.860 --> 02:32:45.360 Why? 02:32:45.360 --> 02:32:49.410 Because, on line 2, when I define the init method for the Wizard class, 02:32:49.410 --> 02:32:51.600 I only expect a single argument. 02:32:51.600 --> 02:32:54.780 But I could absolutely have other common functionality. 02:32:54.780 --> 02:32:55.950 I could add in a patronus. 02:32:55.950 --> 02:32:58.350 If both students and professors have patronuses 02:32:58.350 --> 02:33:02.160 that can come out of their wands, I could have two arguments instead. 02:33:02.160 --> 02:33:04.920 We've been using this feature of object-oriented programming 02:33:04.920 --> 02:33:08.220 now for quite some time in the form of exceptions. 02:33:08.220 --> 02:33:11.820 Indeed, if you look at the official documentation for exceptions in Python, 02:33:11.820 --> 02:33:14.430 you'll see that there's not even the ones we've seen in class, 02:33:14.430 --> 02:33:15.750 like value error and others. 02:33:15.750 --> 02:33:19.860 There's any number of others as well, but they are all, themselves, 02:33:19.860 --> 02:33:21.340 hierarchical in nature. 02:33:21.340 --> 02:33:25.620 This is just a subset of the available exceptions that come built into Python. 02:33:25.620 --> 02:33:29.350 And you can actually, as a programmer, create your own exceptions as well. 02:33:29.350 --> 02:33:33.120 But as this chart here captures hierarchically, 02:33:33.120 --> 02:33:37.350 all exceptions we've seen thus far actually descend from 02:33:37.350 --> 02:33:40.238 or inherit from superclasses already. 02:33:40.238 --> 02:33:42.030 So for instance, at the bottom of this list 02:33:42.030 --> 02:33:44.440 here is ValueError, which we've seen quite a bit. 02:33:44.440 --> 02:33:48.790 And if you follow the line straight up on this ascii rendition of this chart, 02:33:48.790 --> 02:33:53.310 you'll see that ValueError has a parent, class, or superclass, called exception. 02:33:53.310 --> 02:33:57.150 And the exception class, meanwhile, has a parent class called base exception. 02:33:57.150 --> 02:33:59.320 Why did the authors of Python do this? 02:33:59.320 --> 02:34:04.170 Well, it turns out that, whether you have a value error or a key error 02:34:04.170 --> 02:34:06.610 or an assertion error or any number of others, 02:34:06.610 --> 02:34:11.460 there's a lot of functionality common to all of those types of errors 02:34:11.460 --> 02:34:13.650 that you want-- 02:34:13.650 --> 02:34:16.180 that you want a programmer to be able to use. 02:34:16.180 --> 02:34:19.590 And so it turns out that the authors of Python decided, you know what? 02:34:19.590 --> 02:34:22.980 Let's not have a dozen or more different classes 02:34:22.980 --> 02:34:26.370 that all just have copy, pasted similar functionality. 02:34:26.370 --> 02:34:28.470 Let's create this hierarchy so that, even 02:34:28.470 --> 02:34:32.100 though the exceptions toward the bottom of this list are very precise, 02:34:32.100 --> 02:34:33.480 they at least inherit-- 02:34:33.480 --> 02:34:36.790 that is, borrow some very common functionality up above. 02:34:36.790 --> 02:34:40.770 So it turns out that, when you use the Try and the Accept keyword in Python, 02:34:40.770 --> 02:34:44.490 generally speaking, we've tried to catch very specific exceptions, 02:34:44.490 --> 02:34:45.510 like ValueError. 02:34:45.510 --> 02:34:48.660 But technically, you could capture the parents or even 02:34:48.660 --> 02:34:51.565 the grandparent exception for a given exception, 02:34:51.565 --> 02:34:54.690 especially if you're not necessarily sure which one is going to get raised. 02:34:54.690 --> 02:34:58.260 Or, better yet, there could be many exceptions that get raised, 02:34:58.260 --> 02:35:00.150 but you want to handle them all the same, 02:35:00.150 --> 02:35:02.190 and you don't want to necessarily enumerate them 02:35:02.190 --> 02:35:04.240 in parentheses, separated by commas. 02:35:04.240 --> 02:35:07.830 You want to say you want to handle all exceptions of a certain superclass 02:35:07.830 --> 02:35:09.640 in much the same way. 02:35:09.640 --> 02:35:12.060 So this has been latent this whole time, any time we've 02:35:12.060 --> 02:35:16.380 seen or used or caught or, now, raised exceptions, and built into Python 02:35:16.380 --> 02:35:17.230 is this hierarchy. 02:35:17.230 --> 02:35:19.140 And if you were to invent your own exception, 02:35:19.140 --> 02:35:21.460 generally, you wouldn't want to start from scratch. 02:35:21.460 --> 02:35:25.230 You would want to descend from-- that is, subclass, one of these existing 02:35:25.230 --> 02:35:30.150 exceptions and add your own twist on it, your own functionality as well. 02:35:30.150 --> 02:35:33.420 Well, there's one final feature of object oriented programming 02:35:33.420 --> 02:35:35.970 that we'd like to share with you today, and then it 02:35:35.970 --> 02:35:39.480 will perhaps be quite the eye opener as to what you can really do now 02:35:39.480 --> 02:35:41.400 that you have classes at your disposal. 02:35:41.400 --> 02:35:44.010 And this, too, surprise, has been a feature 02:35:44.010 --> 02:35:46.680 you and I have been taking for granted for weeks now. 02:35:46.680 --> 02:35:49.470 This has just worked, but it's been implemented in a way 02:35:49.470 --> 02:35:51.420 that you can now leverage yourself. 02:35:51.420 --> 02:35:54.360 It turns out that Python, and some other languages, too, 02:35:54.360 --> 02:35:58.260 support this notion of operator overloading, whereby 02:35:58.260 --> 02:36:03.450 you can take very common symbols, like plus or minus or other such syntax 02:36:03.450 --> 02:36:08.640 on the keyboard, and you can implement your own interpretation thereof. 02:36:08.640 --> 02:36:12.000 Plus does not have to equal addition. 02:36:12.000 --> 02:36:14.520 And minus does not have to equal subtraction. 02:36:14.520 --> 02:36:16.920 And in fact, you and I have already seen another context 02:36:16.920 --> 02:36:19.770 in which plus means something else. 02:36:19.770 --> 02:36:23.940 Plus has not always, in Python, meant addition, per se. 02:36:23.940 --> 02:36:27.665 What else has Python used plus for? 02:36:27.665 --> 02:36:28.665 AUDIENCE: Concatenation? 02:36:28.665 --> 02:36:30.082 DAVID J. MALAN: For concatenation. 02:36:30.082 --> 02:36:34.090 For joining two strings, for adding to a list can you use plus as well. 02:36:34.090 --> 02:36:38.880 So plus has actually been, funny enough, overloaded by the authors of Python 02:36:38.880 --> 02:36:39.730 for us. 02:36:39.730 --> 02:36:43.200 And so we can use the same symbol in much the same way as addition 02:36:43.200 --> 02:36:46.470 but with different data types to solve slightly different problems. 02:36:46.470 --> 02:36:49.440 Well, let me propose that we go back over to VS Code here, 02:36:49.440 --> 02:36:53.460 and let me go ahead and create a new final file called vault.py. 02:36:53.460 --> 02:36:55.200 So code of vault.py. 02:36:55.200 --> 02:36:58.620 And let me propose that we implement the idea of a vault 02:36:58.620 --> 02:37:01.740 at Gringotts, keeping on theme, wherein there's 02:37:01.740 --> 02:37:03.480 a bank in the world of Harry Potter. 02:37:03.480 --> 02:37:06.300 And within this bank, families and individuals 02:37:06.300 --> 02:37:10.170 have vaults containing all sorts of money in the wizarding world. 02:37:10.170 --> 02:37:12.780 And the type of money that exists in the world of Harry Potter 02:37:12.780 --> 02:37:15.990 are coins called galleons and sickles and Knuts, 02:37:15.990 --> 02:37:18.310 and those are in descending order of value. 02:37:18.310 --> 02:37:20.940 And so inside of a vault might be a whole bunch of coins-- 02:37:20.940 --> 02:37:24.210 gold, silver, and bronze, essentially, each in those denominations, 02:37:24.210 --> 02:37:25.050 tucked away. 02:37:25.050 --> 02:37:29.280 So how can I go about implementing, first of all, the idea of a vault 02:37:29.280 --> 02:37:33.570 so that I can store, for instance, for Harry Potter, how much coinage 02:37:33.570 --> 02:37:37.110 is in his family's vault, or for Ron Weasley the same? 02:37:37.110 --> 02:37:39.510 Well, let me go ahead and vault.py and first 02:37:39.510 --> 02:37:43.500 create a class called Vault, essentially meant to represent a bank vault. 02:37:43.500 --> 02:37:46.200 Perfect, another real world, or fantasy world, 02:37:46.200 --> 02:37:48.690 entity that I want to represent with code. 02:37:48.690 --> 02:37:51.180 I could use a tuple or a list or a dictionary. 02:37:51.180 --> 02:37:54.300 But again, I'm going to get a lot more functionality with classes, 02:37:54.300 --> 02:37:57.340 and we'll see one final flourish with operators. 02:37:57.340 --> 02:38:00.080 Inside of this vault class, let's go ahead and do this. 02:38:00.080 --> 02:38:03.850 Let me define my init method, taking its first argument of self. 02:38:03.850 --> 02:38:06.850 And let me define three arguments to this. 02:38:06.850 --> 02:38:09.760 When you create a vault, in my code here, 02:38:09.760 --> 02:38:12.970 I want to be able to initialize it with some number of galleons, some number 02:38:12.970 --> 02:38:14.590 of sickles and, some number of Knuts. 02:38:14.590 --> 02:38:17.980 I want the user, the programmer, to be able to pass in one or more 02:38:17.980 --> 02:38:19.060 of those values ideally. 02:38:19.060 --> 02:38:21.310 But they can be optional, so I'll give them defaults. 02:38:21.310 --> 02:38:24.190 So let's go ahead and define a parameter called galleons, 02:38:24.190 --> 02:38:28.270 whose default value will be 0; sickles, whose default value will also be 0; 02:38:28.270 --> 02:38:31.310 and knuts, whose default value will be 0 as well. 02:38:31.310 --> 02:38:35.770 So the programmer can pass in one or two or three or even none of those, 02:38:35.770 --> 02:38:38.330 and they'll all have some implied defaults. 02:38:38.330 --> 02:38:41.710 How do I want to remember those values that are passed in? 02:38:41.710 --> 02:38:42.760 Well, let me do this. 02:38:42.760 --> 02:38:45.790 self.galleons = galleons. 02:38:45.790 --> 02:38:48.610 And self.sickles = sickles. 02:38:48.610 --> 02:38:51.910 And self.knuts = knuts. 02:38:51.910 --> 02:38:55.750 And so I could add some error checking, especially if you don't pass in. 02:38:55.750 --> 02:38:58.930 A number I could turn these into properties to do even more validation. 02:38:58.930 --> 02:39:01.980 But let's keep it simple and, as always, focus only on the new ideas. 02:39:01.980 --> 02:39:04.480 So I'm just going to trust that these values were passed in, 02:39:04.480 --> 02:39:07.930 and I'm going to immediately assign them to these instance variables. 02:39:07.930 --> 02:39:09.620 What, now, do I want to do? 02:39:09.620 --> 02:39:12.400 Well, let's come up with a way of printing out 02:39:12.400 --> 02:39:15.220 what is in someone's vault, ultimately. 02:39:15.220 --> 02:39:16.520 But first let's do this. 02:39:16.520 --> 02:39:21.310 Let's create a vault for the Potters by creating, via assignment, a new vault. 02:39:21.310 --> 02:39:26.350 And let's say that the potters have 100 galleons, 50 sickles, and 24 knuts. 02:39:26.350 --> 02:39:29.770 And that's in that vault. And let's print out, for instance, potter. 02:39:29.770 --> 02:39:32.140 All right, let's run this code and see how it works now. 02:39:32.140 --> 02:39:36.310 Let me go ahead and run Python of vault.py, Enter. 02:39:36.310 --> 02:39:37.300 Seems to work. 02:39:37.300 --> 02:39:39.250 No syntax errors or anything else. 02:39:39.250 --> 02:39:42.010 But this is not very enlightening. 02:39:42.010 --> 02:39:46.480 How do I fix this, thinking back to what we've done before? 02:39:46.480 --> 02:39:49.170 AUDIENCE: You have to use the __str. 02:39:49.170 --> 02:39:50.170 DAVID J. MALAN: Exactly. 02:39:50.170 --> 02:39:53.500 I need to use one of those special methods that comes with classes 02:39:53.500 --> 02:39:57.828 and define for myself how I want a vault to be printed as a string. 02:39:57.828 --> 02:39:59.120 So let me go ahead and do that. 02:39:59.120 --> 02:40:03.760 Let me define the str method taking in self as its sole argument here. 02:40:03.760 --> 02:40:05.710 And let's just return a very simple string 02:40:05.710 --> 02:40:08.050 that just reveals what's in the vault. 02:40:08.050 --> 02:40:11.170 So I'm going to return a formatted f string, 02:40:11.170 --> 02:40:15.370 inside of which is self.galleons and then the word galleon, 02:40:15.370 --> 02:40:17.080 so I know which those are. 02:40:17.080 --> 02:40:20.650 Then let's do self.sickles, and let's output the word, sickles. 02:40:20.650 --> 02:40:24.310 And then lastly let's output self.knuts, and then knuts here. 02:40:24.310 --> 02:40:29.230 So I know, in this string, just how many of each of those coins 02:40:29.230 --> 02:40:32.860 I have in this particular family's vault. All right, let me go ahead 02:40:32.860 --> 02:40:37.370 and run Python of vault.py, changing nothing else except the str method. 02:40:37.370 --> 02:40:42.823 And now, , we, see indeed that Harry has 100 galleons, 50 sickles, and 25 knuts. 02:40:42.823 --> 02:40:44.740 All right, well, let's do one thing more here. 02:40:44.740 --> 02:40:48.100 Below that, let's go ahead and define a Weasley variable. 02:40:48.100 --> 02:40:51.790 And Ron never seemed to have quite as much money in the vault as did Harry. 02:40:51.790 --> 02:40:55.730 So let's say that the Weasley vault will have 25, 50, and 100. 02:40:55.730 --> 02:40:58.240 So I'll just reverse the order of those denominations, 02:40:58.240 --> 02:41:00.740 rather than Harry's 100, 50, 25. 02:41:00.740 --> 02:41:04.340 And now let me go ahead and print Weasley like this. 02:41:04.340 --> 02:41:08.500 And let's go ahead and clear my terminal window, run Python of vault.py. 02:41:08.500 --> 02:41:11.770 This time, that str method will be invoked twice, once 02:41:11.770 --> 02:41:13.610 for each of those vault objects. 02:41:13.610 --> 02:41:15.880 And we'll see, indeed, that the first one for Harry 02:41:15.880 --> 02:41:19.330 has got 100, 50, and 25, respectively, versus Ron's 25, 50, 02:41:19.330 --> 02:41:21.790 and 100, respectively. 02:41:21.790 --> 02:41:23.680 But now let's do something interesting. 02:41:23.680 --> 02:41:27.550 Suppose that you wanted to combine the contents of two vaults, 02:41:27.550 --> 02:41:30.100 be it Harry's and Ron's or any other two people. 02:41:30.100 --> 02:41:33.280 How would you go about doing this in code? 02:41:33.280 --> 02:41:37.190 Well, if I wanted to combine the vaults for someone, I could do this. 02:41:37.190 --> 02:41:39.550 Well, I could do galleons equals-- 02:41:39.550 --> 02:41:45.160 let's do potter.galleons + weasley.galleons. 02:41:45.160 --> 02:41:47.470 That gives me a variable called galleons that has 02:41:47.470 --> 02:41:50.230 the sum of Harry and Ron's galleons. 02:41:50.230 --> 02:41:56.560 Let's next do sickles = potter.sickles + weasley.sickles. 02:41:56.560 --> 02:42:03.130 And then lastly, let's do knuts = potter.knuts + weasley.knuts. 02:42:03.130 --> 02:42:04.180 I've got three variables. 02:42:04.180 --> 02:42:05.870 What can I now do with these values? 02:42:05.870 --> 02:42:09.040 Well, let's create a third-- a new vault. Total will 02:42:09.040 --> 02:42:12.640 be the name of this variable equals a new vault, Capital V, notice. 02:42:12.640 --> 02:42:15.130 And now, let's pass in those three new variables-- 02:42:15.130 --> 02:42:18.220 galleons, sickles, and knuts. 02:42:18.220 --> 02:42:20.830 And that's it, and let's print out this total vault. 02:42:20.830 --> 02:42:23.950 So we should now see three vaults-- one for Harry, for Ron, 02:42:23.950 --> 02:42:26.410 and the combination-- the addition of the two. 02:42:26.410 --> 02:42:30.100 Let me go ahead and rerun Python of vault.py, and there we have it. 02:42:30.100 --> 02:42:36.100 What was 100, 50, 25 and 25, 50, and 100, combined through addition now, 02:42:36.100 --> 02:42:39.460 is 125, 100, 125. 02:42:39.460 --> 02:42:41.740 So pretty straightforward, using techniques from weeks 02:42:41.740 --> 02:42:45.760 ago, where we're just declaring a few new variables and doing some addition. 02:42:45.760 --> 02:42:49.420 But wouldn't it be cool if I could do something like this? 02:42:49.420 --> 02:42:51.760 Wouldn't it be cool if I could just somehow, 02:42:51.760 --> 02:42:55.750 not manually create my own vault and do all of this annoying math up here-- 02:42:55.750 --> 02:43:01.750 what if I could just do potter + weasley and get rid of all of this logic here? 02:43:01.750 --> 02:43:05.950 Wouldn't it be nice if I overload the operator-- 02:43:05.950 --> 02:43:08.890 we know as plus, just like str does, just 02:43:08.890 --> 02:43:13.270 like list does-- to allow me to add two vaults together 02:43:13.270 --> 02:43:15.070 on the left and the right. 02:43:15.070 --> 02:43:19.450 Well, it turns out in Python and through operator overloading, 02:43:19.450 --> 02:43:21.460 there is a way to do just this. 02:43:21.460 --> 02:43:23.920 If you consult the documentation, there's 02:43:23.920 --> 02:43:27.940 this and so many other special methods that come with classes. 02:43:27.940 --> 02:43:31.120 The third one we'll see here is this one here-- 02:43:31.120 --> 02:43:34.503 __add__. 02:43:34.503 --> 02:43:36.670 And you'll see that it very generically is described 02:43:36.670 --> 02:43:40.540 in the documentation is working for any object, be it a vault or str 02:43:40.540 --> 02:43:41.980 or a list or something else. 02:43:41.980 --> 02:43:44.680 By convention, it's going to take a first argument called self, 02:43:44.680 --> 02:43:48.280 and then it's going to take some other argument, by convention, called other. 02:43:48.280 --> 02:43:51.310 self, in effect, is going to be referring to whatever 02:43:51.310 --> 02:43:53.440 object is on the left of a plus sign. 02:43:53.440 --> 02:43:55.360 other is going to be referring to whatever 02:43:55.360 --> 02:43:57.520 is on the right-hand side of a plus sign, 02:43:57.520 --> 02:44:01.540 thereby giving us a way of describing, in code, the operand 02:44:01.540 --> 02:44:06.640 on the left and the operand on the right of the operator, plus, in between. 02:44:06.640 --> 02:44:09.160 That is to say, if I go back to VS Code here, 02:44:09.160 --> 02:44:13.300 what I'm trying to do is implement support for this. 02:44:13.300 --> 02:44:16.270 Well, let me try, without writing any other code just yet-- 02:44:16.270 --> 02:44:18.760 Python of vault.py, Enter-- 02:44:18.760 --> 02:44:24.760 TypeError: unsupported operand type(s) for +: 'Vault' and 'vault.' 02:44:24.760 --> 02:44:27.940 That is to say Python, at this moment, does not know what 02:44:27.940 --> 02:44:30.310 it means to add two vaults together. 02:44:30.310 --> 02:44:31.690 You and I might have an instinct. 02:44:31.690 --> 02:44:35.170 Probably want to combine the galleons and the sickles and the knuts 02:44:35.170 --> 02:44:36.010 respectively. 02:44:36.010 --> 02:44:37.180 Python doesn't know that. 02:44:37.180 --> 02:44:39.490 It just knows that you have a new class called Vault. 02:44:39.490 --> 02:44:41.440 But let's teach Python to do this. 02:44:41.440 --> 02:44:43.360 Let me clear my terminal window. 02:44:43.360 --> 02:44:46.430 Let me scroll back up to the class itself, where, at the moment, 02:44:46.430 --> 02:44:49.450 I only have two special methods-- init and str. 02:44:49.450 --> 02:44:51.670 But let's add this third. 02:44:51.670 --> 02:44:57.820 Let me go into the class here and define __add__ and then specify its first 02:44:57.820 --> 02:45:02.020 parameter as self, as before, and then a second parameter for this particular 02:45:02.020 --> 02:45:03.790 method called, by convention, other. 02:45:03.790 --> 02:45:06.040 Now, as always, I could name those parameters anything 02:45:06.040 --> 02:45:08.207 I want, but I'm going to stick with convention here. 02:45:08.207 --> 02:45:10.240 And now, inside of this method, am I going 02:45:10.240 --> 02:45:13.570 to have to now add together the contents of two vaults? 02:45:13.570 --> 02:45:14.530 Well, what two vaults? 02:45:14.530 --> 02:45:17.510 Well, if we scroll down to our goal at hand, the goal, of course, 02:45:17.510 --> 02:45:19.810 is to add this vault plus this other vault-- potter 02:45:19.810 --> 02:45:21.430 plus weasley, respectively. 02:45:21.430 --> 02:45:25.150 Well, it turns out, in Python, that, when you do overload an operator like 02:45:25.150 --> 02:45:27.490 plus, what's going to happen automatically, 02:45:27.490 --> 02:45:32.860 as soon as Python sees that, is it's going to call that __add__ method, 02:45:32.860 --> 02:45:35.260 and it's going to pass into it to arguments-- 02:45:35.260 --> 02:45:38.530 whatever the operand is on the left-- potter, in this case-- 02:45:38.530 --> 02:45:41.890 and whatever the operand is on the right-- weasley, in this case. 02:45:41.890 --> 02:45:46.720 And those values are going to get passed in as self and other, respectively. 02:45:46.720 --> 02:45:49.570 What that means is that we can access their contents up here 02:45:49.570 --> 02:45:51.680 in our implementation of add as follows. 02:45:51.680 --> 02:45:55.330 Let me go ahead and define a local variable called galleons and set that 02:45:55.330 --> 02:45:58.720 equal to, for instance, the sum of self.galleons-- 02:45:58.720 --> 02:46:01.330 whatever's in Potter's vault in this case, 02:46:01.330 --> 02:46:04.270 plus whatever is in Wesley's vault in this case, which 02:46:04.270 --> 02:46:06.190 would be other.galleons. 02:46:06.190 --> 02:46:07.900 Let me do the same for sickles. 02:46:07.900 --> 02:46:11.320 self.sickles + other.sickles. 02:46:11.320 --> 02:46:13.180 And let me lastly do that for knuts. 02:46:13.180 --> 02:46:17.017 So self.knuts + other.knuts. 02:46:17.017 --> 02:46:18.850 But at the end of the day, I'm going to need 02:46:18.850 --> 02:46:22.720 to return a brand new bigger vault that contains all of those contents 02:46:22.720 --> 02:46:23.440 together. 02:46:23.440 --> 02:46:28.000 And if we ultimately want to assign that bigger vault to a variable like total 02:46:28.000 --> 02:46:32.300 here, on the left, we'd better return a value from this add method. 02:46:32.300 --> 02:46:36.010 So I'm going to go ahead and give myself a brand new vault, as by returning 02:46:36.010 --> 02:46:40.120 capital Vault, which of course, is going to call my vault function into which 02:46:40.120 --> 02:46:42.955 I can now pass some of those initialization arguments. 02:46:42.955 --> 02:46:44.830 Well, how many galleon, sickles, and knuts do 02:46:44.830 --> 02:46:46.750 I want this brand new vault to contain? 02:46:46.750 --> 02:46:50.890 Well, I want it to contain this many galleons this many sickles, 02:46:50.890 --> 02:46:52.510 and this many knuts. 02:46:52.510 --> 02:46:54.910 So ultimately, what we're doing in this implementation 02:46:54.910 --> 02:46:59.020 of add is adding together those galleons, sickles, and knuts, passing 02:46:59.020 --> 02:47:02.170 them to the vault function so that we get a brand new bigger vault, 02:47:02.170 --> 02:47:05.030 and return that altogether. 02:47:05.030 --> 02:47:07.270 So now I've defined this new special method 02:47:07.270 --> 02:47:13.450 called add that should now just make plus work for two vaults. 02:47:13.450 --> 02:47:14.050 Let's see. 02:47:14.050 --> 02:47:17.950 Let me run down to my terminal window, Python of vault.py and hit Enter. 02:47:17.950 --> 02:47:23.170 And voila, and now we've implemented an overloaded operator, plus, 02:47:23.170 --> 02:47:25.360 to do what you and I as humans would hope 02:47:25.360 --> 02:47:27.670 would be the case when you add two vaults together. 02:47:27.670 --> 02:47:30.130 But I've now written the code more specifically 02:47:30.130 --> 02:47:35.710 to teach Python what it means concretely to add two vaults together. 02:47:35.710 --> 02:47:37.900 And it's with very similar code in effect, 02:47:37.900 --> 02:47:40.750 underneath the hood, that Python is doing this for two strings, 02:47:40.750 --> 02:47:45.070 to concatenate them together, to joining two lists into a new list with list, 02:47:45.070 --> 02:47:48.080 and so many other classes as well. 02:47:48.080 --> 02:47:53.380 Any questions now on operator overloading or this example here. 02:47:53.380 --> 02:47:55.090 AUDIENCE: How would you go about creating 02:47:55.090 --> 02:48:03.040 a function for adding a student and a vault for two separate classes? 02:48:03.040 --> 02:48:04.445 Would that be possible? 02:48:04.445 --> 02:48:06.320 DAVID J. MALAN: Let me see what happens here. 02:48:06.320 --> 02:48:07.270 I don't know offhand. 02:48:07.270 --> 02:48:08.063 Let's do this. 02:48:08.063 --> 02:48:09.730 Let's create a str and see what happens. 02:48:09.730 --> 02:48:12.080 If I add Potter plus a str-- 02:48:12.080 --> 02:48:12.700 str object. 02:48:12.700 --> 02:48:13.870 Yeah, so it would work. 02:48:13.870 --> 02:48:15.910 I'm just figuring this out as I go here, Eric. 02:48:15.910 --> 02:48:18.190 So just to be clear, what I did was I just 02:48:18.190 --> 02:48:20.170 changed weasley to str just to see what would 02:48:20.170 --> 02:48:25.360 happen when I add a vault plus a str, and it will work, theoretically. 02:48:25.360 --> 02:48:25.990 Why? 02:48:25.990 --> 02:48:32.770 Because so long as the type of value on the left has an add method implemented, 02:48:32.770 --> 02:48:35.510 other can be any type that you want. 02:48:35.510 --> 02:48:37.810 You just have to decide and code what it's 02:48:37.810 --> 02:48:40.840 going to mean conceptually to add a vault plus a string, which, 02:48:40.840 --> 02:48:44.660 in this case, probably doesn't make any sense at all, but it's possible. 02:48:44.660 --> 02:48:46.420 It's going to be the operand on the left. 02:48:46.420 --> 02:48:47.440 And I'm inferring that. 02:48:47.440 --> 02:48:49.180 I did not know the answer a moment ago. 02:48:49.180 --> 02:48:54.130 I'm inferring that because what I got was an attribute error here on line 11 02:48:54.130 --> 02:48:58.330 because Python did not like this. other.galleons didn't work, 02:48:58.330 --> 02:49:00.890 but I could make it work by figuring something out. 02:49:00.890 --> 02:49:02.020 Really good question. 02:49:02.020 --> 02:49:03.520 Didn't know that one myself. 02:49:03.520 --> 02:49:07.120 Other questions on operator overloading? 02:49:07.120 --> 02:49:09.495 AUDIENCE: Can you define new operators in Python? 02:49:09.495 --> 02:49:10.870 DAVID J. MALAN: I don't think so. 02:49:10.870 --> 02:49:15.790 There is a very long but precise list of operators that you can overload. 02:49:15.790 --> 02:49:19.720 I do not believe you can assign arbitrary characters 02:49:19.720 --> 02:49:21.760 to be operators in Python. 02:49:21.760 --> 02:49:23.682 Let me defer to Carter in the chat to-- 02:49:23.682 --> 02:49:26.390 OK, I'm seeing two of my colleagues are saying, no, not possible. 02:49:26.390 --> 02:49:28.383 So I'm going to go with my first instinct, no. 02:49:28.383 --> 02:49:29.800 Otherwise, that'd be kind of cool. 02:49:29.800 --> 02:49:32.080 You could make emoji do whatever you want to. 02:49:32.080 --> 02:49:35.200 How about one final question on operator overloading? 02:49:35.200 --> 02:49:39.640 AUDIENCE: Is that the only operation you can do as far as-- 02:49:39.640 --> 02:49:41.250 can you do a subtraction as well? 02:49:41.250 --> 02:49:42.250 DAVID J. MALAN: You can. 02:49:42.250 --> 02:49:44.800 You can do so many others let me. 02:49:44.800 --> 02:49:47.810 If, Carter, you don't mind pulling up this URL here-- 02:49:47.810 --> 02:49:50.960 so this link here-- special method names and today's slides, 02:49:50.960 --> 02:49:54.430 you'll see a long list of all of the operators that you can overload. 02:49:54.430 --> 02:49:57.790 You can do less than, equals than, plus equals, minus equals. 02:49:57.790 --> 02:50:00.490 Pretty much any symbol you've seen me type on the screen 02:50:00.490 --> 02:50:04.380 can be overloaded in the context of classes. 02:50:04.380 --> 02:50:06.757 So even though, today, we focused entirely 02:50:06.757 --> 02:50:09.840 on object-oriented programming, this is a technique that we've been using, 02:50:09.840 --> 02:50:13.020 really, since the first week of the class because those ints, 02:50:13.020 --> 02:50:16.480 those strs, those floats, those lists, those dictionaries, 02:50:16.480 --> 02:50:20.280 and so much more were already underneath the hood this whole time-- classes 02:50:20.280 --> 02:50:21.333 and objects thereof. 02:50:21.333 --> 02:50:23.250 But you now, as a programmer, have the ability 02:50:23.250 --> 02:50:26.790 to create your own classes with your own instance or class variables, 02:50:26.790 --> 02:50:30.090 with your own instance or class methods, with your own properties, 02:50:30.090 --> 02:50:33.180 and even with your own custom behavior for operators. 02:50:33.180 --> 02:50:35.820 So ultimately, you can absolutely continue 02:50:35.820 --> 02:50:40.500 using those simple tuples or lists or those dictionaries or other structures 02:50:40.500 --> 02:50:41.010 as well. 02:50:41.010 --> 02:50:44.610 But object-oriented programming, and with it, classes and now these objects 02:50:44.610 --> 02:50:46.410 is just another tool in your toolkit. 02:50:46.410 --> 02:50:49.260 And daresay, as your code gets more sophisticated 02:50:49.260 --> 02:50:51.240 and your problems get bigger, you'll find 02:50:51.240 --> 02:50:54.900 that being able to model these real world or even fantasy world entities 02:50:54.900 --> 02:50:57.660 with classes and related data and functionality 02:50:57.660 --> 02:51:01.800 will ultimately just allow you to define code that's not just correct but ever 02:51:01.800 --> 02:51:04.060 well-designed as well. 02:51:04.060 --> 02:51:06.770 This was CS50.