 Hello, everyone. So in our last video here on Python classes, we are going to talk about equality when you are comparing two custom classes and also a concept in Python called Dunder methods and what they are and how you can use them to your advantage. So let's get going. We often, when we're writing our code, we need to check for equivalence between objects. Is x equal equal to 5? Are two things equal to one another? And of course, we have a variety of other Boolean comparison operators that we need to use less than, greater than, things like that. Well, you may find yourself in a situation where you need to compare two custom data types that you have defined using Python class construct for equality as well. So let's try the following. We're going to create two of our student classes. And I've augmented my student class to include a student ID number. And let's compare them. So I'm going to switch over to my code and we'll do this exercise together. So I've got my code here. Make it a little bit bigger for you all. So before I get into equality, I want to show you one thing. So I'm going to add a third class variable here, a third piece of information that I want to represent the student ID number. And the ID number is just going to be a nine digit number. Here at UNCW, our ID numbers begin with 850. So I'll do 850, 1, 2, 3, 4, 5, 6. Now I need to, so I've added the student ID number. What I need to do, of course, is to add this ID number as a parameter up here. Now you may be tempted to do, well, ID. And you'll notice, if you're using PyCharm anyway, that PyCharm highlights this. And it says something called shadows built in name ID. Now what does that mean? You will see this occasionally. You'll also see it if, for example, you use the variable min tells you the same thing. Shadows built in name min. What that's telling us is it's giving us a warning, because Python also has a function named min. And it also has a function named ID, which we talked about in an earlier lecture. So Python is warning us, hey, you're redefining that name. That thing already has a name. And you are allowed to do this in Python. But you're liable to get some unexpected behavior if you do do that. So it's important that if you're making variables or defining your own functions or methods, don't use the names of Pythons built in functions. Make your own. So instead of ID here, I'm not going to use ID, because that's the name of a built in Python function. I'll call it student ID. You could call it something else. But just a good practice. Don't name your variables or functions or methods the names of Pythons built in functions. You could wind up in a situation where you think you're calling a Python function, but actually what you're doing is referring to a variable. Python will let you do it, and you'll get some weird behavior probably. So I'll call. All right, so now I've got a student here named Alice, Jones. So I'm going to make two of these students. I'm going to create two of them. One is student, Alice Jones assigned to x, and the other is assigned to y. Now, keep in mind, though, what are we doing when we call the constructor here? We create a new instance of class student. And in fact, we're going to create two instances. It's not going to try and strangely reuse this one just because it looks the same. Python doesn't do that. But they've got the same data, first name, last name, ID, exactly the same. So if we check, are these guys the same? I'll do it this way. If x equal equal y, print, they are the same. Else, print, they are different. So if these two things are equal, which it kind of looks like they are, we should print they're the same. Otherwise, we print they're different. So let's try it. Let's run this. They are different. Well, why are they different? They've got the same data. Well, the answer is this equality right here is doing what we call shallow equality. So what is shallow equality? Let me add a couple of print statements in here. Let me print x. And let me print y. So let's run this just so we can see what it does. When I printed these things, it said main.student object. So that's saying you've made one of these student things at here. Well, what is this? It looks like a memory address. And in fact, if we were to print the hex, how do we get the address? We call ID, ID on an instance. And this looks like hexadecimal. So we need to convert it to hex. Let's rerun this. Student object at this memory address. Here's the memory address. So what's happening here is what we call shallow equality. Shallow equality. When you compare two objects with equal, equal in Python, what it does by default is it goes and it says, are these the same two objects in memory? In other words, do they have the same memory address? If so, if x and y are both pointing to the same object in memory, yeah, it's got to be the same thing. It must be the same thing. And that's what Python is doing here. So what if we do this, x, y gets x. Go punch this into Python tutor and watch and see what happens as you step through. X and y start out different, pointing at different things. And then when I do y gets x, they're going to point to the same thing. So now when I print this, let's run it. When we get to this point, x and y are variables. They're pointing to a place in memory. And it's the same thing, same object in memory. So yeah, x equal equal y. This is what we call, again, shallow equality. Now note, I'm going to change this back, so I don't x and y are two different instances. They have different memory addresses, but they have the same data. They have the same data. Python isn't that smart. It doesn't know that they have the same data. It's just looking at their memory address. Is it the same thing? If so, if the memory address of x and y are the same thing, they must be the same thing. They must have the same data. Go back here. This is what we call shallow equality when the variables refer to the same object in memory. Very basic, very simple view of equality. But maybe not what we want, because it's probably the case that we want to consider these two different student objects to be the same thing, because their data is the same, or because maybe their ID number is the same. Maybe in our program, in our system, we want to consider two students to be the same if their ID is the same, but maybe their names change. Maybe someone gets married, or maybe someone doesn't want to be Alice anymore. Maybe they want to be Alex. So we can define how we want equality to be for our custom objects. So we would like two objects with the same data, but perhaps living at different memory addresses, to be considered equivalent. The way that we do that is by having deep equality. Deep equality is when two objects contain the same data values. OK, well, there's a way to do that in Python. Let's go over to our code. All right. I'm going to go back over here and get rid of my print statements. What we will do is create a new method that looks kind of like a knit. It's another so-called Dunder method called EQ. What do you think EQ is short for? Equals. Again, we've got two underscores. Underscore, underscore, EQ, underscore, underscore. One parameter called self. And then PyCharm is calling this other parameter other. You might, I'm going to rename this. It's OK to rename it to write. So what happens is when this method gets called and we've got to return something here, what we have to return is a Boolean. You know what, let's finish out the method first. So if, let's say that we want to consider two students to be equal if all their data is the same. So how can we check if their data is the same? Well, the way that Python interprets this call, I'll change this back to other. If you have x equal equal y, like we do down here, this is the call, what happens is Python says, self is x in this expression. Self is going to be x, other is going to be y. Left equal equal right. That's why I almost called this thing right. Other here takes on the value of y, x here, or self takes on the value of x. So the self is the left-hand side, the left-hand side of the operator. And y is the right-hand side. Other is the right-hand side of the operator. I think I'm reversed on my screen. So self is the left-hand side of the equal operator. Other is the right-hand side of the equal operator. So if I want to check that they're equal, what I do is I'll say, all right, if the self, if the left-hand side's first name, is equal to the right-hand side's first name, I can return true, else return false. The equal method, the equal operator, must return a Boolean value, either true or false. That's what it has to do. EQ must return a Boolean value, either true or false. That's it. And that's all it should do. You shouldn't be printing in here. You shouldn't be doing any computations. You should just be figuring out is the left-hand side of the equal operator equal to the right-hand side? That's it. So if self.firstname and other.firstname are equal, it'll return true. But that's not good enough. We want to make sure that first name and last name and student ID are equal. So we can do and self.lastname equals other.lastname. And finally, so I'm getting kind of long on the screen here. What I'm going to do is I'm going to add a backslash. And Python, that backslash lets me break my expression between lines. And that's all it does. You don't have to have this. Let me do it without it at first. And self.studentId equal equal to other.studentId. So there's my big, long expression for this. And actually, PyCharm's kind of yelling at me, saying this line is too long. Well, if you're in PyCharm, it's smart enough that it can break up this line. So if I put some line breaks in here, this little thing just means it tells Python, continue forming the expression out of the next line. If you take this out, you're going to get a syntax error. But if you have everything on one line, that's fine. This is just for readability. And in fact, I'm going to put everything back on one line, because I don't really want to confuse you. So we've got this big, long expression. Now, we've got x and y. And if we have done this correctly, if we've overridden this correctly, what's going to happen is when Python sees this, x equal equal y, it's going to say x is a student. Go to the definition of student and see if there's an EQ method. There is. OK, let me use it. So when we run this, and then it'll run this code, when we run this, we get that they are the same. Well, now we can change these values. Let's change Alice in the second one to Alex. This should fail. They're no longer equivalent. First name in the self is not going to equal the first name in the other. They're different. So now this EQ method gives us a more refined way or a more specific way of defining equality between objects. You're going to find this very, very, very useful, because you wind up using this sort of thing a lot when you write your code. Now, there's a more OK. So that's kind of the big thing with EQ. Must return a Boolean value, either true or false. We can actually make this a little more succinct. Look at this big, long expression. What does this evaluate to? Well, an if statement, its expression, this conditional, it's an and statement with a bunch of equalities. This thing will evaluate, imagine this in parentheses, it's going to evaluate down to true or false. So we can shrink this. This expression is true or false. It is what we want. So we don't actually need an if statement here. We can just return the result of evaluating this. It's mad at me because I've got to get rid of this colon. There's a colon that you can't see down here at the end. Let's run it. Still working. Still working. Change it. They're different. So this is the equal method. Very cool, very useful. Let me go back to the slides here. So now we have deep equality. Deep equality, the data items in the objects have the same values. We have overridden the equal method. But what happens here? If we're comparing two students, we're in good shape. But what happens if we compare a student with, say, an integer? Let's try it. Let's go back to our code and try it. Let's compare x. x is a student to 5. Let's run this and see what happens. Attribute error. Int object has no attribute first name. And if you're in PyCharm, you can follow the stack trace. So here's the offending line. Line 13 in EQ. What it's saying is, what happened here? The left side becomes self in this method call. The right side becomes other in this method call. And it's saying, int object has no attribute first name. Well, other here has been substituted with 5. 5.firstname? That doesn't exist. That's the attribute error. What does the dot operator do? The dot operator accesses attributes on a class instance. Ints, integers, ints types, doesn't have a first name attribute. They don't have a method called first name. They don't have a data variable called first name. So you get this exception. That's what it's screaming about you for, OK? So you're liable to encounter this particular exception, an attribute error. Something has no attribute first name. You're liable to encounter it a lot when you're defining custom classes. You've got to learn how to debug these error statements so you can effectively use your custom classes, OK? I'll put this back the right way. Go back to my slides here. All right, so just to summarize, shallow equality. Need to know these concepts. Shallow equality is when two variables refer to the same object in memory. So in our example back here, these things have deep equality because they have the same values, but they are not shallowly equal. They point to two different places in memory. This is one instance. This is a different instance, OK? Deep equality is when the data items in the objects have the same values, OK? So the implication here is that if you have shallow equality, you do also have deep equality. If you have shallow equality, you must have deep equality because they point to the same object in memory. However, conversely, if you have deep equality, you do not necessarily have shallow equality, OK? All right, so the EQ method, one final word on it. Now that you have defined the EQ method, you also get some other things for free. You get not equals, kind of cool, OK? And you get the in operator, right? So let me show you what I mean. x and y, they have deep equality, right? You get the not equal operator for free. So print is x not equal to y. And no, this should be false. This should be false now, OK? Because in our world, they are equal. So you get the equal operator for free. You get the not equal operator with EQ. And the other thing you get is the in operator. In tests for elements membership in a list or a sequence, right? So if we create a list, say students, with x in it and y in it, I've created a list, I can check to see is x in my list. The answer is yes, right? Because I have defined this EQ method, OK? Pretty cool, right? Pretty cool. Let's go back here. And one more thing I want to show you, OK? So when we print it, we're going to talk about the string method and the wrapper methods here, OK? So EQ, init, and now string and wrapper, they are what are called in Python dunder methods. Dunder because of the double underscore, right? Underscore, underscore, blah, underscore, underscore. They're also called magic methods. These methods are built, they're magic methods. They're in Python. And when Python is working with custom classes and you do certain operations, like equal, equal, or call the constructor, Python goes and looks magically, automagically, for definitions of these methods on your custom classes and calls them if they are there, OK? Dunder methods are a very useful one that you will find. Let's get rid of some of this stuff. I've got my two students here. Let's look at printing, all right? So let's print x. Ugly, ugly, ugly, OK? Let's use something a little bit better, OK? So first, we're going to define a method called wrapper. Underscore, underscore, R-E-P-R, OK? This method returns a string. Must return a string. And the idea is that it should be the string representation of your object. Now, what Python recommends is that you make this string look like the constructor call that created the object, OK? So we'll do this with an F string, OK? So I'm going to create an F string. And what is my constructor call? It looks like student with some parentheses. And then we put in the first name, self.firstName. And then we call it with the last name. I'm just trying to mimic this. But what I'm going to do is I'm going to have the data that's particular to the instance that wrapper is used on, OK? And self.studentId. Let me make these students, I'm also going to make these students have some different data. So now let me print x and print y, all right? So now when I print them, I don't get that funky student object address, whatever. I get something a little more useful, right? Really good for debugging and stuff, I would think, right? And you'll notice when I called print, right, I don't call wrapper. I don't call wrapper. I don't do this. This is wrong, right? I don't do that. I don't have to. This is a magic method. Rapper is a magic method. Python sees I'm printing, and actually what it's trying to do is it's trying to convert this to a string. Since I don't tell it how to convert it to a string, instead it calls wrapper. And it says, ah, OK, well, let me print this. Here's something that'll convert it to a string. And it does it, OK? The other time you will see this, OK, let me make my students list again. Students is a list containing x and y. Now when I print out my students list, here's my brackets indicating a list, and here's the individual students inside, OK? Kind of neat. All right, there's another method, the last method I want to show you, which is the string method. underscore underscore string. This method must return a string. And it is called when you call stir on your object or print. Hmm. Well, maybe that's a little confusing because print was calling wrapper a second ago. Well, there's an order of evaluation here. First, if you call print, print will first try and look for this method. And if it doesn't find it, then it will use this method, OK? On the other hand, well, let's just fill this out. The difference between string is that string is meant to be user-friendly, right? Like, again, from the user's perspective, from somebody who's using the system. Or maybe this constructor-oriented shape isn't good. Maybe what we want to print is something that looks a little nicer, like the person's first name, followed by her last name. And maybe we want to say, and their ID is self.studentID. You can put whatever you want in here. These are strings. You can structure them however you want. This one, wrapper, is very conventional. It should mirror the constructor call. String, what's user-friendly to you? That's what this is going to be. Let's run this now. So you'll see these first two print statements are now calling myString method. You can also, this string method also gets called with, if you do this, OK? This is also going to call this guy. Well, now what I'm printing, I get the user-friendly version. I invoke string, not wrapper. Cool. However, wrapper will still get called in certain scenarios. For example, again, you are not calling these directly. Python calls them for you. One of the scenarios is here. When you print a list, when I'm printing my list of objects, I get their internal representation. Cool. We will also, I think we might be able to get it this way. Let's try it. No, it's not showing there. In some instances, when you get an exception printed to the screen, and that exception involves a particular instance of your custom class, wrapper will get called. So again, you should always define at least wrapper. And wrapper should always be what the constructor looks like. It's going to come up. It's going to help you debug your programs and debug error messages. And then also, if you want to have a user-friendly representation of your custom class, override the string method. So everything we just said about string and wrappers captured on the slide and kind of come back and review it. All right, so these guys, just to remind you, init, string, wrapper, EQ, they are dunder methods for double underscore. And Python looks for these dunder methods in your class definition to determine how to apply certain common operators. There are many, many, many dunder methods. You can tell Python how to use less than operator, greater than operator, and operator, or operator, all sorts of stuff for your class. You don't have to. But they're there, right? They're there. And they'll be useful in other contexts, OK? So you can go there and look at what all these different things are that you can define. Basically, any operator you can think of, plus sign, minus sign, less than, and, or, they're all there. But you should only define the ones that make sense for your class. Usually, that's at a minimum going to be init, wrapper, and EQ, OK? All right, so know what dunder methods are. Know how to define them and use these three that are on the screen. And we have gone through some huge important concepts in this module about Python classes and how they work. You have to understand conceptually what's going on with defining a data type as a Python class and how that impacts memory. And then you need to know how to implement it syntactically, OK? So there's going to be a homework assignment built on this stuff where you get to practice all of these things. It's challenging. It's one of those things you've got to wrap your head but once you do, once you really understand classes and data types, you will have evolved a substantial step upward in your understanding of how to write programs to solve interesting computing problems, OK? So let me know if you have questions, and I'll see you next time.