 Right, so our next speaker coming up is Nlidu Bonagar. Nlidu couldn't deliver the talk live due to some technical problems. So now we're very happy to be able to make the arrangements to record the talk and include it as part of the videos of your Python 2020s. So now Nlidu will be discussing object internals of Python, i.e. what is happening under the hood with different Python built-in data structures. Here you go, Nlidu. Yeah, thank you for the introduction, Rekul. Hello everyone. The topic for my presentation today is object internals in Python. And I'm Nlidu Bonagar. Like everyone of you here, I'm also a Python enthusiast. I'm a backend developer by profession. And when I'm not programming, I usually enjoy giving talks at various meet-up groups as well as conferences. And I'm available on Twitter and my Twitter handle is Nlidu followed by devil in the score. Moving ahead, the learning objectives for this talk include we'll be discussing what is meant by object followed by what is meant by memory address. Then we'll be going over types of objects that is mutable and immutable objects. Then we'll be covering is versus the comparison operator. Then followed by that, we'll be covering an interesting topic that is memory optimizations in Python. So an object is an entity that has attributes and methods associated with it. And based on different data types, the attributes and methods can be different. However, each of the data types have these three things in common. That is type, value and memory address. In our example here, our value is five. As the value is five, the type of our object is integer. And as this object is going to be stored somewhere in memory. So the unique number that you see here tells us the location. And we, and this is termed as memory address. Here is an example. Imagine this to be your Python interpreter. And here I'm doing a is equal to two. Basically, I'm initializing variable a with the value two. And in Python, the variable assignment takes place from left to right. This very this object integer object get is basically getting stored in memory. Now, all I want to do is check the type of the object. So just I'll use the built in method type and pass the parameter a inside of it. We are passing the variable name inside because this variable is in turn pointing to the object itself. And here you can see the output that it is integer. Similarly, we have done for Python. That is our string object. Then we are doing for list. Then similarly, we are trying to find out type for the dictionary object that you see here. And the location as we discussed before the location where the object is getting stored is our memory address. Here is a pictorial representation of same. This bigger rectangle can be imagined as a memory block. All our objects are getting stored in this memory block. While variables are labels in Python. So a, b, c, d, all of these are variables. And as these objects are stored in memory, this would have some memory address attached to it. So all the things in blue that you see are basically the memory addresses. This diagram is basically for understanding purpose and it is not exactly the same way that internally it happens. This is basically for easy explanation of things. Now how can we find that unique number that we were seeing earlier that unique memory address. So there is a built in method in Python that is ID and inside of the parenthesis we pass in object as a parameter. So again, imagine this to be our Python interpreter and I have some examples here. We are doing a is equal to two. Now I'm interested in knowing the memory address of the object. All I will be doing is using the built in method ID and then we are passing a as parameter. And then you can see that here is a unique memory address that is returned. Similarly, we can do for strings list dictionary or any Boolean value. And then we would be getting a unique memory address. Moving ahead, there are two types of objects in Python. One is mutable objects and others are immutable object. Objects of built in type list dictionary sets are mutable. So mutable objects are basically the objects which can be changed and the memory address of these objects remains the same. No new object gets created here. Here is an example. I'm initializing L list L and there are various items present in it one, two, three, four, five, and then I'm finding the memory address. So this is the memory address that is being written. Now I'm interested in changing my list. I'm interested in adding another item to the list. Now when I add another item to the list, I'm using append append would add the item to the list. The item has been added and now you can see that our list initially contained five elements and now it has six elements inside of it. But when you will check the memory address of this list, the memory address remains the same. That means we have modified the initial object, but no new object has got created while we were making the change. So that is the meaning of mutable. We were able to change it without creating any kind of new object. Now then there is another type of objects which are immutable objects. So objects of built-in type, end, float, bool, str are immutable in nature. Meaning when you make any kind of change to these objects, then a new object gets created. We'll see how here a is equal to we have initialized a tuple having four elements 10, 20, 30, 40. And then I find the memory address. The memory address returned is this. But I'm interested in adding a new element 50 to my to my tuple. So I'm trying to use append and then this would raise an error that tuple object has no attribute as an append. Which means it does not allow to add an element like this. Similarly, if I wanted to remove an element, this also raises again an error that is tuple object has no attribute as pop. This won't allow me to pop out an element. And then another way of adding the element is to reinitialize the list. And you can see here the memory address has got changed. It creates an new object here. We are unable to modify our initial object using this method. Next we are covering that what is meant by is versus double equal to operator. So this is basically a confusing concept and often can often is used interchangeably. So this is something that cannot be used interchangeably. There is a subtle difference between the two. That is, here is an example. I have initialized variable A and B with the value 10. And this is because I'm using something that is why I'm unable like you cannot see it as double equal to imagine here it is written as double equal to. So I'm all I'm doing is a double equal to B. So now when we do a double equal to B, the result is true. This is so because our comparison operator basically compares the values of the object as the value of A and B both are same. The result is true. While in our next example 10 and 20 as the values are different, the result is false. Now let us see what happens with is our example remains the same, but I'm doing a is B. So when you do a is B, the result is true. This is so because his operator compares the memory address. And earlier we had learned the memory address is determined by using the ID method. So we have now we have two same memory addresses. That is why the result of a is B is true. And now when you will see for the list here the result is false because our memory address for both the list is different. So this is the difference between the is operator as well as the comparison operator. Hence those can both of them cannot be used interchangeably. These are two different things and moving ahead now that we have covered the basic concepts here is an interesting thing. We'll be discussing memory optimization in Python. So there is sort and sorted built in methods in Python and we all know as the name goes that both of them are used for sorting. Then interesting thing to know is where lies the difference. So let us see where is the difference. I have initialized the list in this example 1050 4060 and I'm trying I'm finding the memory address. This is our same old way that we discussed earlier. Then I'm using the built in methods sort. Now what sort does is it sorts the list in place, which means no new object in memory is getting created. How do we verify this after the sort we can again find out the memory address and you can see that both the memory addresses remain the same. So when you are using dot sort method, no new object is getting created. So we are optimizing on memory. And when we are using sorted, you will notice that a new object gets created in memory sorted after sorting the list returns a new list. While dot sort does not return anything. So in so in case I would have done L is equal to a dot sort and then my value of L would have been none because a dot sort doesn't return anything. So basically this is the difference between sort and sorted. Also sort is used for list while sorted can be used with any object. So these are couple of differences between the two methods that we have and based on the use case use cases we can choose the best one possible for our particular use case. Next is integer caching. Now on your interpreter when you try doing is equal to 10 B is equal to 10 and find the memory address the result came out to be same. Now I tried this for a couple of numbers and then my results were mostly same as this first example that you can see here meaning with different numbers I was getting the same memory address. But I was too quick to conclude that the behavior for the numbers remains the same as in when I'm initial when I initialize a variable with the number support with any kind of values here I have taken as 10. And initializing another value another variable with the same value. I assumed that both the memory addresses would be same like we are seeing here. But then in case of 257 what happened was the result came out to be different both the memory addresses were different. And then this led me to another question that why is the difference. So this is based on the CPython implementation of integers and the behavior is different because Python assumes that integers between the range minus five to 256 are most frequently used integers. So what Python is doing is it is doing integer caching. So if your if the if your variable has a value within this range no new object is getting created. And if we are taking any value outside of this range it creates a new object. Now before creating the new object it first checks whether this object is initialized before or not. All those checks are internally done. And likewise there is another interesting concept in case of strings. So when you compile the Python code identifiers get interned and what are identifiers so our identifiers are variable names function names and class names. And then there is format for these identifiers your identifiers can have a letter number or an underscore inside of underscore in their names. So here is an example. I have initialized two variables A and B with a string hello. And then I'm trying to find the memory address you will notice that both the memory addresses are same. This is so because our first example is following the rules that we discussed earlier. Both the variables like this is a variable name and we are having only letters inside of it. That's why strings are already interned no new object in memory is getting created. While in this example again when you see memory addresses are same this is so because our rule is again followed. We have letters as well as a underscore in our variable name. That's why as the condition is being met the memory like string interning has already happened. So this saves the memory but then I tried out another example and my example was live is beautiful. Now in case of this you can see the memory addresses are different. Seeing a bit changing behavior I was curious to find out why is this happening. So this happens because you can as you notice our rule is being broken here. We are using letters but there is space as well. There is white space that you can see and because of this no string have interning happens internally in python. So both the memory addresses as you can see are different. However we can do forceful interning. This is not very much advisable but the way to do forceful interning is we import a built-in module named assist and use the method intern. And when you are forcefully interning it then again the memory address as you can see comes out to be same. But using forceful interning has its own trade-offs. So before going on and one of the use cases where forceful interning is helpful is if you have lot of data with you text data and you are comparing various long strings. So overall the process can be very slow. So at such a case if you want to compare if you are trying to compare whether your strings are equal or not that time that forceful interning can be used. Our next is a copying list using the assignment operator. So I have a list initialized with me and when I initialize the list you can see inside of the list again there is a list and tuple. Now I am trying to copy list L1 and list L2. Okay so first let us see the memory address. Memory address of L1 turned out to be this. Memory address of L2 turned out to be this. Now then I was curious to change and modify my list. So what I did was I did L11 dot append 6. Now here in my sub-list here I am trying to append an element 6. Now when I append an element 6 to it you can see when I tried doing again L1 6 has got appended. But the question is again if you see your L2 the change in L2 has also happened. However I did not explicitly append 6 in L2 but the change has occurred because for L1 and L2 both our memory addresses were same. That's why when you change anything in your L1 it will get changed in L2 as well. Next concept is shallow copy in list. So what happens in shallow copy? In shallow copy the outermost container is duplicated and whatever objects are inside of this container have this share the memory references. Now the definition might be confusing but here is a supporting example. I am initializing list L1. I have list L1 with me and you can see there are again embedded objects inside of our original list. Then I am trying to do list of L1 and then I am assigning it to variable L2. This default list when we do list of L1 by default what happens is shallow copying of list. So now again I am interested in finding memory address. I found out the memory address of L1 and L2 and here both the memory addresses turned out to be different. So both memory addresses are different but what needs to be remembered is in shallow copying the memory address of your embedded objects share the same references. Which means when I am doing L1 0, L1 0, so our L1 is this list and when I am doing L1 of 0, L1 0, L1 1, L1 2, L1 3. And I am similarly doing for L2 0, L2 1, L2 2 and L2 3 and you will see in both the cases our memory addresses turned out to be same. So internally if you make any kind of change to your list, so the same kind of change will occur in the list embedded objects of list L2 as well. Now deep copying is exactly opposite of shallow copying. Deep copy do not share the internal embedded objects do not share the references. So here is an example. Initially I did copy dot copy. This is shallow copying but then I am trying to do deep copy as well. Then I am appending 20. I am doing L1 2 dot append 20. So 0, 1, 2 I am appending an element to this list. So and then I am trying to find out what is L1. So as expected L1 got changed and 20 has got appended to it. Now as we learned in copy embedded objects share the references, so 20 got appended in L2 as well. But you will see in deep copy the embedded objects don't share the references. That is why that 20 has not got appended in list L3. So list L3 is basically has remained as our original list. No change that we made in L1 has got affected in has affected L3. So this is the primary difference between all of these copying. Deep copy and shallow copy. Now I had discussed quite a lot of concepts and a lot of confusing examples. And if you are thinking that why is it important to learn about object internals at all when we can already write these things and just get away like just try out the examples without even understanding the internals. So here are more examples. The use case that I had was to create a 3 by 3 matrix and I wanted to initialize each element with zeros in it. Now it strike me that this can be done using the star operator. And what I did was L is equal to and then I for creating sublist I used this expression that you can see here. It resolved my purpose. I was able to create a 3 by 3 matrix and there were three sub lists inside it with the items 0000000. So I thought that this is a better approach to do it. I don't have to create a loop and then create the matrix. Now an interesting thing happened. Firstly I just tried finding the memory address. It gave me a memory address. My next thing was I wanted to change the value of one of the element. So what I did was considering it to be a normal matrix that has got created. I did L sub 00 that means 0th element in 0, 0th sublist. And then I initialized it with value 1. Now my usual thinking was that only the element at this position is going to get changed. But then Python surprised me. What output came was this. It changed the element at 0000 position. Then it changed the value at this position and then the next. So this is when that knowledge of internals come into play and one realizes that why it is important to learn the concepts inside out. So now it is on you to figure out why such a bug or why such a error is coming. Next example is this one. I have defined a function append two and then I am calling this function with different elements. And now it is for you to basically try and find out that what is the expected output and then verify your expected output with the actual output that Python is giving. And then you can give a reason that why such a thing came what mistakes are we making and what should not be done. So that was all I had for the session. And overall what we covered was object internals then we deep dived into memory address for various use cases. Then we discussed an important concept of mutable and immutable objects then we discovered is versus the comparison operator, followed by a lot of memory optimization concepts, which covered that we covered integer caching string and turning. So that was all for the talk. So yeah, thank you very much need you for the fascinating talk and because this is a recording we will unfortunately not have the Q&A, but hopefully this video will be viewed by many personas does in the world. And also hopefully you can come to Dublin next year and meet and discuss Python with many more people in person. So thank you very much.