3/6 OOP & Classes in Python: Special (dunder/magic) Methods

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Welcome to the third video of this series about  object-oriented programming and classes in Python!   In this video you're going to learn how to  use special methods, also called dunder or   magic methods, to implement different behaviors  when for example you add two numbers together,   you print an object, etc etc. So, here I've got  the class we used also in the previous videos   with just the "__init__" method. The "__init__"  method is actually part of the special methods and   they are special methods invoked using a specific  syntax. We've already discussed "__init__" in a   previous video so I'm not going to talk about  it specifically. So, a special method is used   to describe what to do when using a special  syntax, but what is the special syntax? Well,   let's see an example. So, when you add two numbers  together or two strings together Python recognises   the "+" sign and uses one of the special methods  under the hood. So let's actually write two   strings, so "string1" and "string2", something  like that and then we print "string1" + "string2"   and of course we get something like "ab", so here  Python sees that you're trying to add two strings   together using the "+" sign, here, and because  you have two string objects and the "+" sign, when   you do this Python actually uses the "__add__"  special method of the string class, of course.   So basically when you use the "+" sign to add two  strings together Python does something like this   ,so "string1", "__add__", like that and then  "string2" and if you actually print it and run it   you can see that "ab", "ab", they return the same  result. So this is just an example but Python does   this when you use the "-" sign, the "len" function  to get the length, when you print something,   when you compare objects, so using the "is greater  than", "is equal" etc etc. So having these methods   available means that we can actually create our  operation for our classes, which is incredibly   useful in my opinion. So, let's actually delete  the example here and see what we can do with our   class, by the way there are a lot of special  methods you can use so it would be impossible   to go through all of them here, so I'm going over  the ones that I think are more useful and you're   more likely to need but you know that in the  docs you can find all of them, I leave all the   important links in the description box down below.  So let's say we have two instances so, "person1" "John", 54, "London" and then we've got "person2",  let's call it "David", 24 and "Austin", something   like that. Sso if we print them we get something  like this, so if we print the instances directly   like this, "print" like that, we get something  like this, so we get "main person objects etc   etc", so as you can see we just get that this is  a person object and this is the address basically.   So, you need to know that when you print something  Python looks for the special method "__str__"   and in case it doesn't find it, it uses a method  called "__repr__", let's actually add them to out   class. So let's say that we want to add "__repr__"  like that, we need to pass "self "as usual,   like that. So this method as stated in the docs  should be a representation of the object that we   can actually use to create the same exact object  if you were to use it, so in this case something   like this, so we want to return an f-string,  I'm going to use double quotes, so "Person",   "self.name" and then "self.age" and then of course   "self.city" like this and let's close that.  So, the "__repr__" method is called when you   use the "repr" built-in function and when we  print an object and we haven't specified the   "__str__" method, so let's try to actually print  that, let's do something like this, so print   "repr(person1)" and this is actually the  same as doing something like "print",   "person1. __repr__()" , like that, so this is the  same thing and if we call that, as you can see   "Person('John')", so this is that and as you can  see this is the representation of what we actually   used to create an object so this is the same as  this, so if you just copy and paste this you can   create that object. And if we print without using  the "repr", so "person1", something like that   you still get the same thing because the special  method "__repr__" is sort of fallback if you don't   specify the "__str__" method so let's actually  write that here, so "def", "__str__", "self". Basically the "__str__" method is the one  used when you basically print an object   of this class or when you use the "str"  function to convert the object to a string,   so in this case I want to return an f-string  and I want just to add the "name" and then   the "age ", like this, "from", I don't know,   "self.city", I'm just making this up like that.  So now if we run this again, so this thing again,   we get, like a,s you can see "John(54) from  London", as you can see now using the, when we   print that, we get this, the "__str__" the result  of "__str__" and if we use that thing with "repr",   you get this, cool. So as I mentioned this is  also used to convert the object to a string,   so for example if I wanted to do something like  "person_string", "str(person2)", and then I can   print "type(person_string)", so there you see that  you get a string and then "person_string" like   that, as you can see you get "class", "string",  "David from Austin", so by using the "str" you can   convert the "person1" object to a string and this  uses the "__str__" method like that, so the result   is actually what you get, perfect! So now let's  say that, let's actually delete this, so now let's   say that we want to compare two Person objects  with "greater than", "less than", etc and we   want to compare them based on their "age", how can  we do that? So to do this there are other special   methods, so let me copy a sort of table here.  So "__lt__", "less than", "less than or equal", "equal", "not equal", "greater than",  "greater than or equal to something",   you can find those in the documentation as well so  I'm gonna delete them from here. So let's actually   try to implement them, so let's try to implement  the "less than", so you've got "self" and "other".   So what is "other"? So basically "self" is the  instance on the left of the sign and "other"   is the one on the right because when you do  something like, let's say 5 "less than" 10,   Python calls this method on this object, on the  5 integer object and the "other" here is 10,   perfect! So, we need to return of course  "self.age" "less than" "other.age",   so you get "True" if the "age" on the left  is actually less than the "age" on the right.   In this case "person1" is 54 and "person2"  is 24, so if you do something like "print" "person1", I'm just printing them but you can  use something like "if person1 < person2",   etc and also this is the same as doing something  like, "person1.__lt__(person2)" and if you were   to have "person2" here and "person1" here this  would be inverted as well. So, if we run that   "False" and "False". So basically Python sees  the "less than" sign here and uses the right   method and it does this for all the other symbols  like the "greater than", etc etc, and even though   we haven't written the "__gt__" method, which  stands for "greater than", if we do something like   "print", "person1", "greater than", "person2"  and we run that, we get "True" and it still works   because we have written the "__lt__" function  and Python automatically sort of creates the   "__gt__" function because it just has to invert  the result, of course if you want to have the   "__gt__" function that works differently, you can  specify another logic, by defining the "__gt__"   and write something else in the body of the  method. Of course the "less than or equal to"   and the "greater than or equal to" are different  signs so you need to specify the "__le__" and   the "__ge__" methods otherwise you would get an  error about the fact that it's not implemented.   I actually just want to mention one thing about  the equality, if you don't specify anything Python   is going to use the "is" keyword to compare  the two instances. I actually talked about   the IDs and the "is" keyword in another video  about mutable and immutable objects. So let's   say that we have these two instances, so let's  delete those, so let's actually delete this and   duplicate it, like that. So, they are equal but  they are not actually the same object in memory,   which means that if we do something  like "print(person1== person2)and   we print that you get "False" because they are  not the same object although they have the same   attributes etc etc and we can change this behavior  using the "__eq__" method. So "def __eq__",   "self" and "other" again, so we can do  something like "return self.name == other.name   and self.age == other.age and self.city ==  other.city". So an object doesn't need to be   the same object in memory to be equal to another  one, it just needs to have the same values,   so now if we save that and you run this, you get  "True" because all of these here are the same as   these even though these two objects are not the  same, they are equal because they have the same   attributes. So now listen up because I'm going to  explain you one important thing, so if you don't   set the "__eq__" method the object can be used  as, for example, dictionary key, because it's   hashable by default and uses the "ID" to calculate  the hash, but if you set the "__eq__" method as we   did here, then the object becomes unhashable. So  to check if an object is hashable we can use the   "hash" function like this, so we can do something  like "print(hash(..." and then "...person1))" if   we try to run that you get "TypeError: unhashable"  but if we didn't have the "__eq__" method here you   get this, as you can see you get a number, which  means that the actual object is hashable, let's   go back here perfect! So you've got the "__eq__"  method and you can also have the "__hash__" method   but the "__hash__" method shouldn't be defined if  you don't Define the "__eq__" method and actually   really really important most of the time you don't  want the object to be used as dictionary key,   especially if your class defines mutable objects,  like in this case. So if you define the "__eq__"   method and you don't define the "__hash__" method  automatically the object won't be usable in hashed   collections like dictionaries because this  is unhashable because we actually added the   "__eq__" method, but if you don't need to define  the "__eq__" method, so let's say that you don't   need this, so you didn't write it or maybe the  "__hash__" method is inherited from a parent class   for example, the object would be hashable and  that wouldn't be good. So to flag it as unhashable   you can set this in the class definition, so you  can set something like, up here, like "__hash__=   None", so now the object should be hashable  because we didn't define the "__eq__" method   but still you get unhashable because we  added this one. If we were to remove that,   as you can see you get something, so this is  actually working and if you want the object to   be hashable, maybe because you want to use it as  dictionary key, which in this case doesn't make   sense because you've got a mutable object so you  shouldn't do that but let's say that you want,   we can actually uncomment this, you can actually  use the "__hash__" method but you need to keep   in mind that objects that compare equal using this  the "__eq__" method need to have the same hash, so   let's actually define "__hash__". "self", so in this case we are creating the  "__hash__" method and usually it's good to put   all the attributes used in the "__eq__" method,  so here, self.name, self.age and self.city into   a tuple, so put them into a tuple and then hash  that tuple. So in this way you are sure that the   objects that result equal to the "__eq__" method  also have the same hash, so in this case you   want to do something like "return hash", then the  tuple in here, "(self.name, self.age, self.city)"   like that, so you hash this tuple and inside of  here you've got all the attributes here, as you   can see "self.name", "self.age", "self.city".  By doing this you're saying to Python that the   object "Person" is hashable and that it can be  used as dictionary key for example, but remember   that this is not what you should do in this case,  because you've got mutable objects. By the way,   just a side note, the hash returned by the "hash"  method is different between Python invocations,   so as you can see now you've got "hash(person1),  you run it you get this number, you run it again   you get this number, so they are not the same  and this is done for security purposes so if   you need a hash that is consistent between Python  invocations, maybe because you want to store it in   a database or something like that, you should use  a module like "hashlib" for example. Then, before   we look at how to implement arithmetic operations  like adding to instances together, subtracting,   etc etc, let's quickly have a look at the special  method "__bool__" which is basically a way to see   if an object is "True" or "False". So to do that  we need to add this method here, this special   method, which is "__bool__" of course and then  "self" and then here let's say that the "Person"   object is "True" if we have a name, an age and  a city and if we don't have a name or a city   or whatever the object is actually "False".  So "if self.name and self.age and self.city",   "return True", "else", "return  False", like that. So let's say that   we want to print, we want to see if the  "person1" is "True" or "False", we use the   "bool" built-in function, "person1", "person2",  and let's say that here we don't have the name,   if you run that you get "False" and "True",  of course if you had the name you would get   "True" and "True" and this could be used like,  for example, something like "if person1",   "print(True)", again you will get  "True", "True" and "True", perfect!   Let's now talk about arithmetic operations and  they are quite useful and worth learning. By   the way if you want to help me a little bit please  like the video and subscribe to the channel. It's   really really simple yet really important for me  :) :) And also if you want to leave a comment down   below I always appreciate them :) So there are  a lot of operations like addition, subtraction,   multiplication, division, etc we're just going  to have a look at a few because otherwise the   video would be too long, I'll leave all the  useful links in the description down below   if you need them. So let's see what happens  if we add two "Person" instances together,   so let's actually delete this, so "print(person1  + person2)" like that and let's see what we get.   We actually get "unsupported operand  type +" because we didn't specify that,   so, to implement the method we need to  add the "__add__" method here, so, "def   __add__", that that takes "self" and also "other",  where "self" is the object before the "+" sign   and the "other" is the one after the "+" sign,  so in this case "person1" is "self", "person2"   is "other". And let's say that we want to return  the the sum of their ages, it doesn't make sense   but you'll definitely make things that make more  sense, so "return self.age + other.age", so now   if you run that we get 48, perfect! In this case  you could even return a new "Person" object where   maybe the name is the two names together, the age  could be the sum etc etc, so let's actually try   that, so let's say "name" is equal to an f-string  with "self.name" and then "other.name" like that   and maybe the age is not actually, so you've got  "self.age + other.age" like that and then the   "city" will be "self.city" and "other.city", then  we can return a new instance so "Person(name,   age, city)", like that, so now if you run this  you'll see that you'll get a new instance, so   as you can see "David/David(48) from London",  let's actually change it and write something   different so, like that, something like that,  so you get something like "John/David(58) from   London/Austin" or something like that,  and we can assign it to a new variable   because we're actually returning a new  instance, so we can do something like   "new_person", "person1 + person2" and then we can  use that, and let's actually use the "IDs" to see,   "person2" and then "new_person" like  that and let's see what we actually get,   as you can see all of them are different  because we've actually created a completely   new "Person" object by combining the other  two together and it doesn't make a lot of   sense but it's just an example. And this is  actually used also when we do something like,   this, so let's actually delete those,  this "__add__" here is used also   when we do something like "person1 += person2",  which is equivalent to of course, you know that,   this is the same as doing something like "...+  person2", like this and in this case it doesn't   make sense because basically you would end up with  a completely new object in "person1", so you've   got "person1" and "person2", you add the two  things together and then "person1" becomes a new   object because you actually, you're still using  this with a new, and you return the new object,   so you get a new object in "person1", so if you  do something like "print(id(person1)) and then   you comment this out and you keep this, and then  you duplicate this, move that down here, if you   run that you can see that you get two different  objects, so basically you've got a "person1",   you've got a "person2", you add them together, you  create a new "Person" object and you assign it to   the "person1" but it doesn't make sense because in  this case you can just update "person1" right? So   to actually change the behavior of the "+=" sign  you can use a new special method and there is one   for all the operations, you just need to add an  "i" before the name, so, you can do something like   "def __iadd__(self, other)" and instead of  returning a completely new object we can   just update the "person1" because we're  basically adding "person2" to "person1",   I know that it doesn't make sense I keep saying  this because you might be thinking "what the   hell is he doing?!", but of course your object is  going to make more sense, this is just an example.   So "self.name" is equal to "f", let's actually  copy those from here, so "self.name" like that,   like this, of course you need to do something like  "self.name", "self.age", because we are updating   basically the "self", which is the instance on the  left of the sign and here we can just do something   like "..+= other.age", like that "self.city",  "other.city", "name" etc etc and then we return   "self", because we don't want to create a new  instance we just want to update the instance   on the left so in this case the "ID" should  be the same. As you can see this is exactly   the same because we didn't actually create a  new object, we added "person2" and we added   that to "person1" and we just updated the  "person1", we didn't create a new instance,   interesting! So if the video is already out  you should see it on the screen along with the   playlist containing all the videos of this series  and don't forget to like this one if you enjoyed   it :) And also subscribe to the channel for more  videos like this and I'll see you soon, bye! :) :)
Info
Channel: Fabio Musanni - Programming Channel
Views: 1,314
Rating: undefined out of 5
Keywords: python, python3, programming, coding, learn python, python tutorial, python for beginners, python special methods, python dunder methods, python magic methods, python class special methods, python class dunder methods, python class magic methods, how to use python special methods, how to use class special methods, how to use python class dunder method, how to use python class magic methods, python __add__, python __lt__, python __eq__, python __str__, python __repr__
Id: pSpEBvRFWfM
Channel Id: undefined
Length: 22min 23sec (1343 seconds)
Published: Wed Feb 22 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.