 Hello, everyone. In today's video, I want to talk about Python under the hood, the memory, and the notional machine. So you all have in your head what we call a notional machine of how Python works. And the notional machine is very simply your interpretation, your mental model of what happens when you press the run button and the code gets stepped through that you typed in and computation and, you know, just what's going on when you press that button. Well, data structures are all about managing memory, right, and searching and sorting and storing of data. And all of that takes place in memory, computer memory. What I want to talk about today is make sure that we have a pretty good, or at least a high-level grasp of what computer memory is and how Python uses it. What is a good notional machine for Python? And let's get everybody together on the same playing field, okay? So stepping into this, whoop, whoop, if I can get my clicker working, there we go. So computer architecture in a nutshell. All computers today be that your laptop, your desktop, your smartphone, computers up in the cloud, follow the same basic architecture. And it's this architecture that you see in front of you. This is the so-called Von Neumann architecture named after John Von Neumann, who proposed it in the late 40s, early 50s. Brilliant man, but even today, all computers follow this general organization. You have the user input, be it keyboard, mouse, speech to text, what have you. And we have a central processing unit, and this is the brain of the computer, right? It's the little silicone chip. It's the thing that you pay a lot of money for when you buy your computer. And it's the thing that does the work. Computers are really, really fancy calculators at their core. And if you are at UNC Wilmington taking CSE 242 computer organization, you will learn all about how the CPU actually works. You know, it's silicon, it's electricity, there's fundamental physical operations and logical operations going on there. But it's the brain. It does the computation in your big fancy calculator that we call a computer. But in order to do these calculations, it needs a place to store data and to save things, right? And if you imagine just a regular old calculator, a scientific calculator, right? It's got memory in it. If you put 5 plus 4 equals 9 into a calculator, the results of that calculation are put somewhere before they can be displayed. Well, that place where data is put after it's read in, excuse me, after when it's ready to be printed out, when it's having computations done to it, that is the memory of the computer. Typically, when we're thinking of memory, we're thinking of one of these things. The stick of RAM, random access memory. It's more appropriate to call this thing main memory though. But colloquially, we all call this stuff RAM, right? And you can buy little sticks of RAM and if you rip open your computer, you're going to see, if you rip open your desktop, you're going to see two or four of these things. If you open up your laptop, you'll see them as well, but you're going to be a lot smaller. Then the final component of computer architecture is the output, the display, the screen, the audio, and what have you. So this is the architecture, right? Now, what happens when I run my code? Not only in Python, but in any language. Well, in Python, when you press the run button, the Python interpreter is started. Python is an interpreted language. Back in computer science 1, 31, you would often interact with the interpreter directly. When you run Python.exe or what have you, this is what's going on. The Python interpreter then parses. Parsing is the formal name in computing and in language theory in general. Of pulling apart the different individual statements, the different individual syntax elements of a line, of a statement, of an expression. That's called parsing, right? And there's a formal way to parse computer languages and you'll learn that later on in your computer science career. The interpreter parses your Python code line by line. And for each line, the interpreter tells the computer's CPU two things. This is important. It tells the CPU what to compute. So if you're adding two numbers, do the addition. If you're comparing two values, do the logical comparison. If you call a function, you tell the CPU, hey, go over to this other place and do this other thing. So Python is telling the computer what to compute and where in memory to read and write the data needed for that computation. So you've got an operation, the computation, and you've got memory, the data. The computation and the memory are kind of like physical instructions for the computer. So Python is a program, it's running, and it says, hey, CPU, go do this thing, compute c equals a plus b. And it also tells the CPU where in memory it can find the values of a, b, and c. Where to put the value c. This is very important. Data is a physical thing. It lives somewhere. The CPU is a calculator, but in order to calculate, it needs to know where to get the data. Kind of like if you were doing the work on pen and paper. If I tell you to multiply two gigantic numbers, even modest numbers, say 25 times 482. You get out your pen and paper, the paper is memory because you are keeping track of the intermediate steps of computing something. And your brain is the CPU. You know how to do the steps. Python is kind of giving the algorithm for how to do the multiplication. You know, first you do the, multiply the ones digit times the ones digit, the ones digit times the ten digit, carrying things over as needed, et cetera, et cetera. Right? So, thinking of Python now, okay? You've got a Python script, and you need to run it. The Python interpreter is going to execute your code line by line. Now in your head, I want you to form this concept made up of three things. The first is what we call instruction memory. This is the memory that holds the instructions for your code. I've got a little tiny simple Python program here that prints out if a number is even or odd. This tiny little program goes into a place called instruction memory, okay? And Python is going to read your code line by line from there. So the first thing that happens actually when you run a Python script is Python puts that code somewhere into memory to run it, okay? The code naturally exists in a file on your hard drive, right? You've got a file called like math.py or assignment 1.py. So that's one thing, but Python reads that file and then shoves it into instruction memory. And the reason it puts it into instruction memory is it can process instruction memory a whole lot quicker than it can your file. And Python does some other optimizations in there with how it represents your code that we don't really care about at this point. But just have in your head that the first thing that happens is that Python copies your code into instruction memory. And it does it in a sequential way, right? The first line of code is the first thing, then the second line of code, third line of code, fourth line of code, fifth line of code. And when you execute your code, Python goes from line to line, right? It goes from the spot of memory to the next instruction at the spot, next instruction at the spot, et cetera, et cetera, et cetera. Now the other thing that Python has at its disposal is what we call heap memory, okay? Heap memory is a gigantic pile of memory, okay? Now that Python grabs chunks from for storing data. Memory in a computer, I showed you the picture of the little stick of RAM, right? When we say memory, that's usually what we're thinking over, this RAM, this main memory. But technically to the computer, everything is memory that holds information for any length of time. So RAM sticks, your hard drive is considered memory, a USB stick that you might push in there, caches, if you've heard the term cache, right? That's all memory, to the CPU, to the brain, everything is memory. And it doesn't really distinguish. There are different types of memory in the computer because they're good for different things, but we don't care about that. The heap memory is where Python goes if it needs to grab some data or grab a space in order to store some data. So think of heap memory as a blank piece of paper, right? It's actually a bunch of different chunks that are all over main memory somewhere. But Python says, hey, if I need to create, say, a variable called x, well, I need to put this value 5 somewhere. Where am I going to put it? I've got to put a value somewhere in memory. That place that it goes to figure out where to put it is called the heap memory, okay? We'll talk more about heap memory here in another minute. The final component to our notional machine for Python is called the call stack. The call stack is what Python uses to keep track of where in your code it is, okay? We're not going to go deep into the call stack right now. We're going to come back to it later. It's going to be a really important concept. But for now, I just want you to have in your head this notion of instruction memory. This is where Python copies your code. This notion of heap memory where Python goes in the computer to store values or get some scratch space to store values. And then there is the call stack, and that's what Python uses to keep track of where it is in your code. So let's take a minute. As I mentioned at the beginning, data structures are really about managing memory. We're going to see in upcoming assignments that it's all about memory, okay? And managing the memory and using it in an efficient way. And when you use memory inefficiently, you will get things like, not necessarily running out of space, but your code will be slow. Your programs will be really slow, right? So there's a right way to store a billion names that you want to search really quickly, and there's a wrong way. And that right versus wrong way depends on how things are going to be organized in memory. So what is memory? What is it really? I've got pictures of sticks here. This is main memory. And again, we commonly call this random access memory, or RAM. This is the backbone of computer memory. In truth, all memory in computer is RAM. But we're talking about these sticks, right? They form the bulk of memory in your computer, or the bulk of memory that is used for computation in your computer. Memory is physical hardware, okay? It is capable of storing data over time. And that's the key to memory. Memory retains things over time. Your CPU, when it's running instructions, doesn't retain the value. It needs a place to keep it and to put it, right? Main memory, caches, hard disk drives, flash, that should say flash drives, not flask drives. These things are all memory, but they're physical things, okay? Inside these physical things, all of our data that we want to store, numbers, strings, files, audio, video, they are represented as binary digits, a.k.a. bits, right? Bits are ones and zeros, right? Everything inside a computer is comprised of ones and zeros that make up, that represent the bigger concept, the file, the string, the audio, the video. Bits are grouped together in groups of eight called bytes, right? Bits are eight bits in a byte. Now, Python, we're talking about Python here. Python uses groups of bytes, groups of eight bits, to represent data such as integers, floats, strings, etc. Every byte, really key point here, every byte of memory in your computer has a unique memory address. This memory address is how the CPU finds the specific area in memory you're looking for, right? So, when you write a Python program, you say something like, print the value of x, right? Well, where is x? It has a memory address, and so the CPU can go to that address and say, what's the value that is stored here in terms of ones and zeros? Is it the number five stored as ones and zeros? Is it the string Python stored as ones and zeros? What is it, right? Every byte of memory has a unique address. So, when you say something like x gets 42, and we don't say, quick aside here, when you're programming, you don't say x equals 42. That's not right. Equals is something else. Equals is equal equal, two equal signs, right? That's for comparison. This is x gets 42. You are assigning the value 42 to x. So, when you write this line of code, x gets 42. The bytes that represent the integer 42, all those ones and zeros that equal 42, are written to memory. They are created somewhere in memory. Little hardware things are twiddled and toggled so that a bunch of zeros, a bunch of ones and zeros that represent 42, are written to memory. And the variable x holds the memory address of that data. This is super, super important that you understand this. So, repeat it to yourself until you do, and I'm going to show you a picture on the next slide. When you say x gets 42, the bytes representing the integer 42 are written to memory. 42 is created and placed somewhere in memory, somewhere on these things. And the variable x holds the address of where those bytes representing 42 live. So, when you do an assignment, think of it this way. You assign some variables. What happens is the values of the variables are created somewhere in memory. You don't decide that as the programmer. Python goes to its heap space, this space. So, when you say x gets 42, Python pulls a square off of the heap and says, I'm going to shove 42 into the square. Let's go back here. I shove 42 into the square. The answer, the variable the answer, now points to that square in memory. This is where the value 42 lives. When I say pi gets 3.14159, the value 3.1415 is created in memory. 1s and 0s are switched around, so they equal 3.14159. And then pi now points to this value. Same with strings, same with anything else in Python. I'm going to say it again because it's so, so important that you understand this from now until the end of your programming career. When you assign a value to a variable in Python and really in any programming language, you are creating this thing in memory. And then you're saying, alright, the value 42 is created. And the variable, the answer, now points to the value you created. So let's look at it slightly different way. Here's my variables. These are variables. They are just names. That's all that variables are. They're names, they're bookmarks. They point to a place, but they are not the value themselves. The value lives in memory. So this variable is pointing to a place in memory. There's a memory address here. Memory addresses are usually written in hexadecimal. They're going to be big and ugly and look something like this. But this is a reference for the CPU to be able to go and figure out, hey, on which one of these little chips do you live? And inside each chip, how far down on you? Just think of the address as a zip code. It's a way to route your mail or to route the value that you want to write or read from memory. That's all it is. There's also a value there at this location. The ones and zeros of the bits representing 42. And then there's also information about the type. There's an integer at address, this horrible thing, that has the value 42. Ones and zeros. Types in Python. What they really do is they tell Python how to interpret the ones and zeros at a memory address. The ones and zeros, this may blow your mind, the ones and zeros for 42 may be the same ones and zeros for poodle. And 3.1415. But what distinguishes them is this type. This type tells Python how to interpret those ones and zeros that it finds at that memory address. Okay? So a new and important way of looking at Python programs, you really got to, this has to sink in deep because it's so, so important. Each line of Python code tells the CPU to compute something, possibly using some data, possibly using some variable data. Data, for example, 5 poodle 3.14, these are data, these are values, they're not variables, they're values, okay? Data are stored in bytes of memory. And Python variables are bookmarks for the addresses in memory of the data we care about, okay? Super important. Variables are not values in memory. They point to values in memory. This is why you can reassign values. Variables, okay? This is why you can reassign variables. You can't reassign values actually. All right? So study this, really let it sink in, and why? Why do we care about this so much? We care about this so much because if you start to think about Python in terms of its instruction memory, its heap memory, and the fact that variables are not values themselves, but they point to values, it's going to represent to you a growth in your understanding that's going to help you debug your programs. You have to have a really good mental model of how programs work, or at least how Python is interpreting your programs, so that you can debug your programs, and also this is going to help us derive better algorithms, right? Having this model of how things work, better algorithms, how to analyze your code so that you can like make it better, and then ultimately how we can design data structures to work with this notional machine in the most efficient way, okay? So next video, what we'll take a look at is we will go through tracing some code to make sure that we really understand what Python is doing in terms of memory assignment whenever we declare variables and reassign variables and use the values that are pointed at by variables. Thanks.