 Welcome to chapter 10 of the Introduction to Python and Programming Course. This chapter, finally, talks about object orientation. In particular, we will talk about classes and instances. However, these are really technical terms that belong to the bigger paradigm of object orientation. So, throughout the first nine chapters, we have seen a lot of things in Python that basically also belong to object orientation. And this has a reason because Python is, of course, an object-oriented language and anything follows this paradigm. And so this chapter has many overarching themes. One of them is it will put together everything that we have talked about so far. And then it will also give us another big tool. So chapter 10 is finally the tool where we get the possibility to express in a semantic way what our programs do in code. So in other words, in this chapter, we will learn how we will give names and concepts from the real world to code. Or in other words, how we model the real world in code in a way so that we can really say that's a good model of the world. And we can read it just as if you read some math that models some problem of the world. Okay. So because this chapter is about giving meaning to something, we have to specify a problem domain that we want to model. And I chose the problem domain of vectors and matrices. So this has several reasons. One of them is, of course, that this course is aiming at training you, preparing you for further course in data science. And besides good coding skills, what you also need to do to do data science is you also need to have good skills in math. And one of the disciplines within mathematics that is most important for data science is linear algebra. And because of that, I chose the domain of vectors and matrices from linear algebra as the example for chapter 10. And however, note that, of course, this is a programming course and not a math course. So I don't assume that you know too much about linear algebra. And I also don't assume that you will follow the linear algebra parts in this chapter. So I will focus on the bigger conceptual ideas of Python, of course, but the examples will be vectors and matrices. So if you want to fresh up your knowledge in math, there's a good opportunity on YouTube. It's this video course by Gilbert Strang from the MIT. He's a professor of math at MIT. And his course on linear algebra is one of the earliest courses that have been on the Internet. And he's a very lovely guy. I met him once on campus at MIT, and he always has a smile. He's always upbeat. And even with his age of, I think, late 80s or something, he still is very enthusiastic of teaching young people math, particularly linear algebra. And this online course that you just see here on YouTube is one of the most watched online courses ever. And so Gilbert also has a very special way to connect to young people and a very nice sense of humor. So his course consists, as you can see, of 35 lectures, each of them roughly an hour. I can really recommend that to fresh up your knowledge on linear algebra. And then once you have done that, also an important note. Anything that I talk about in this chapter, chapter 10, can also be done with a library called NumPy. And NumPy is the de facto standard. So if you want to implement any applications from the field of linear algebra in code, NumPy is probably one of the best choices to use. So in other words, what we are doing today here in chapter 10 is basically we start from scratch. We assume that NumPy has not been written. And we write our own little NumPy version. That's basically it. Okay, so let's look at an example. Assume you're given a vector y with the components 1, 2 and 3. How would you model that in Python? Let's say you don't know anything about NumPy, of course. If we already knew a little bit about NumPy and we have seen that in chapter 9, then you would know that we would model that in NumPy with a so-called ND array. And towards the end of chapter 10, I would actually compare our own library here with NumPy. So we will see how to do that in NumPy. But now let's assume for a moment that NumPy doesn't exist. How would you model a vector with the components 1, 2 and 3 in Python code? Well, maybe you would do it like this. You would use a tuple, which we talked about in the first part of chapter 7. So here a tuple 1, 2, 3 is assigned to the variable lowercase y. Maybe that is now our vector. Okay, we can look at it. It looks like a vector. It has nice round parentheses. So that's maybe why we choose to model that with a tuple. So here is another example, a matrix. A matrix consists of rows and columns. And this is a 3 by 3 matrix with the numbers from 1 through 9 arranged in a so-called row major way. That means when I say the numbers from 1 through 9, so the numbers, the first couple of numbers, the 1, 2, 3 make up the first row. So one convention in math is to look at matrices in a row first way. And we call that a row major way. The opposite, of course, would be column major way to look at the matrix. And we already, you know, if we just look at core Python without NumPy, there are no two-dimensional data structures. So maybe one way to model a matrix would be to use a dictionary. And as the keys, use a tuple of two coordinates, so to say, one coordinate in the horizontal, the other going to the vertical axis here. And then maybe we use some coordinate system as the keys in a dictionary. And then the numbers that are displayed here as the values within a dictionary. That would be certainly one way to model a matrix in code. However, there is another way that we will choose. We will choose, for example, a list of lists. And because of the fact that matrices are two-dimensional structures and lists only model one dimension, we have to make an assumption here. So if I model a matrix with a list of lists, I have to assume that the inner lists are either columns or rows. So what should they be? Obviously, I chose the way where every inner list in the outer list here is a row of the matrix. And this is commonly what is chosen. And that is what I meant by the row-major approach. But it would be certainly fine to do it the other way around. So we just have to remember that. Of course, if we accidentally switch the order, so we take this as the columns of the matrix in this, then all we would have to do in matrix terms is we would have to transpose the matrix. We will also see how to do that in this chapter. But, yeah, so if we don't know this convention, we will already have a problem, right? So sometimes that would be maybe something to document in the code. But let's now define X, uppercase X. So I don't adhere to the snake case here. And then uppercase X is, of course, the same thing as output. The literal notation remains the same. And now the thing is this. In math, in linear algebra, what we often do is we multiply a matrix by a vector to get a new vector. And again, the particular rules are not so important. Look at Gilbert-Streng's course. The one rule that I will tell you here is that a matrix is seen as a two-dimensional thing of rows and columns. And usually we refer to the rows as m and the columns as n. And then in order to multiply a matrix by a vector from the right-hand side, the number of components in the vector has to match the number of columns in the matrix. But in this case, of course, I chose a three-by-three-square matrix. So it doesn't matter if we multiply the matrix by the vector y from the right or the left. Both would work. However, also note that in linear algebra, if you multiply the matrix from the right-hand side, it is something different than multiplying it from the left-hand side. So we would get a different result if we multiplied the matrix from the left. But we will only do the standard way of multiplying from the right. And then one way to get to the solution here is to see the outcome vector on the right-hand side as a linear combination of the columns from the matrix, where the weights for the individual columns are chosen from the vector by which we multiply. But again, the details, I don't want you to know. If you know it, it's good. You should know them if you want to stay in data science or go into data science. But if you don't know it by now, it's okay. So what do we do in code? Well, I want to multiply x by y. Let's do that. And I get a type error. And that makes sense because how should Python know how to multiply a list of lists by a tuple? And that's exactly the point. I told you that we want to give semantic meaning to the code that we write. But a list of lists or a tuple has no semantic meaning. So Python doesn't know that we're talking about a matrix and a vector here. And it just looks at the operator, the multiplication here, and asks the left-hand side, the x. Hey, x, do you know how to multiply yourself with a tuple? And the x says, no, I don't know. I'm a list. I don't know how to multiply myself with a tuple. And then Python goes to the right-hand side and asks the tuple, hey, tuple, do you know how you can multiply the list? And then the tuple says, no, I don't know. And if both operands don't know how to multiply the other, then the operator will say, well, type error. I don't know what to do. So the storyline of this chapter is, at the end of chapter 10 here, we want to see x times y work. So that's the goal. Fix the type error. How do we do that? So we have to teach Python, core Python, what a matrix is and what a vector is. And we also need to teach Python the rules of how to do matrix algebra. So the way to do that is with the so-called class statement. So we have to define a class. And then we have to what is called instantiate the class. So we will see that in code. So here is our first version of what a vector is. And also one note in this chapter, all the code cells or many of the code cells contain lots of code. There is no way you can read all of it. There is also no way I can discuss all of it. So it is absolutely necessary that you also work through chapter 10 and read it. And I think chapter 10 is, if you read it the first time around and watch this recording, then I think you should get a big picture, but it's definitely a chapter that you would have to read two or three times to get to it. But yeah, so when you do the reading, you should certainly try to understand most of the code because nothing that you will see in the chapter 10 or here on the slide is something that you don't know. All the constructs, all the syntax that you see here, you know already. Or I just teach you the additional stuff you need to know to make this a class. I will just teach you the increment in this chapter to get from non-object oriented programming to object oriented programming. So let's look at this code cell and see and analyze what we see. The code cell starts with class and then a name, vector, and a colon. That's a typical header line. So all the rules that we know about header lines apply. In particular, they end with a colon and everything below here is indented by four. There's one level of intendation and if I scroll down, this is valid until the end. So this is one statement. The entire code cell is one statement. It's a class statement. So what is the class statement? Basically, the class statement is very similar to the def statement which defines functions. So we define something here. And then after we run the cell, Python has a name, vector with an uppercase v by the way, not a lowercase here, but we will get to the naming also. And then Python knows the name vector and then whenever we say something about a vector to Python, then Python will always look up this code that we'll see. So in other words, what the class statement is, this is so-called namespace. So here is code and this code is executed just like code within a function is executed when we call the function. However, there is one difference. The code within a function is only executed when we call the function. It is not really executed when we define the function. Of course, when we define the function, Python also reads the code and does something with it and puts it into the object in memory as so-called bytecode, but it doesn't execute the code. And here, that's one difference. So all the code that you see, when Python now reads this, when I now execute the code cell, then what Python will do is, Python will execute all the lines of code and will do exactly what is written here. And then the only thing you need to know about a class in this regard is it acts as a namespace. So remember when we ran functions, functions when we call them, they were given a local scope where we would keep all the names that are local to the function as it runs. And once the function call is over and the function returns to where it was called from, then the local scope goes away. We've seen that in chapter two extensively, and we've also seen that with many, many other examples. So in, for example, chapter four, when we looked at recursion and iteration, we also defined many, many functions, and whenever we executed them in, for example, Python tutor, then we saw that every time we execute the function, the function has its own scope as long as it's running. And we also saw that several functions can be run at the same time and wait for each other and so on. So what I'm saying is, Python will have many, many scopes at the same time and will, by this way, isolate many, many names from each other that could also be the same, but Python knows which name belongs to which namespace, and a class is nothing but such a namespace. We're in, we have all the names. So, and then how does this work? Well, when I execute this code cell, all this code here is run, and then whatever the code does, the result of that will be stored in this new namespace. So let's run this from top to bottom in our head. So the first line of code sets a variable called dummy class variable to imaVector. So after this statement is run, the class statement, inside the namespace that belongs to this vector class, we will have a variable called dummy class variable, which is set to this string here, okay? And then we see three more statements, and it happens to be depth statement. So it's ordinary functions. Even though these functions are the names, they start with a double underscore and they end with a double underscore. And we call this the Dunder convention, double underscore or short Dunder convention. And in chapter one, we already saw how Python uses those names that have a double underscore, a leading double underscore and a trading double underscore for variables that it uses in very special situations or variables that are somewhat different from the variables we use. And now in this chapter, whenever we define a function inside a class and the function's name starts and ends with a double underscore, we call this a so-called special method. And what a special method will do, we will see, but that's just a name. And no, you cannot just prepend any name, any function name with a double underscore and then it's special, no, it's not the case. The names init, wrapper and str stir that we see here on this slide, they are specialized. If you want to know where you will find them, so Python 3 data model, if that's what you should look for. And then in the Python documentation, in the docs you will find here it says slash reference slash data model. And this page will give you all the possible names of all the possible special methods. So all the names are standardized and you will find them here. We shouldn't care too much about this for now, but in the long run, if you study object orientation, this will be probably the number one page that you will look at to find out how something works. Okay, let's go back to the presentation. So what happens is, Python runs this code and it creates one variable and three functions. And now let's analyze quickly what the functions do and let's also see what is the rest in this class. So the class also starts with a doc string. That shouldn't surprise you by now. So this is the documentation of the class and I just say this is a standard one-dimensional vector and all entries are converted to floats. Okay, so that's the information we get. The dummy class variable I just put in there to show you in a moment. This is not so important for now, but the three methods, the three functions that we see, these are important. Whenever a function is defined within a class, we call it a method. And we saw already in chapter one that different data types in Python, so for example, the list type, the string type and so on, they have different methods. And what are methods? These are different functionalities. For example, the string data type has methods in the upper. We saw that the float data type has a method called is integer that looks at its contents, the contents of the object, of the float and tells us if all the decimals are zero or not, that's its job. So different data types in Python come with different methods, different functionalities of different data types and whenever we use the word data type or the type of an object, what we really look at is how does the object behave. That's the question we have in mind. And the special methods here, these are the things that will make a custom object, that's what the class at the end of the day is, a custom data type, a vector here, behave in a certain way. So this is how we teach Python how to look at an object of this type in a way that it knows how, in this case, for example, to do the linear algebra rules and so on. So, long story short, there is the first function called init and init is the initializer. This function will be called every time when we create a new vector. So this vector class is basically a general blueprint of vectors in general and then whenever we create a concrete vector out of the blueprint, then this function will be executed, the init function. That's why it's called initializer. And this function takes two arguments. The first one is self. The second one is data. And then we see that the two other special methods down here, they also take one argument called self. So what is self? So self is a convention. This is the name self is a by convention given to the first argument or the first parameter of a function that becomes a method in a class. And whenever this function is called, the first argument that is passed to it will be the object on which we call the method. So, in other words, to give you an example that you are familiar with, you have a string object. An object could be, you know, my first name, whatever, any name that is in a text string. And you call dot lower on it. And then somewhere in Python's source code, some method called lower will be called, which is a function, and the function will get one argument passed to it, which is the text string on which we called the function. That's why it's self. So whenever we have a concrete example of a vector and do something with it, the first argument to any of these functions will be the vector object itself. That's why by convention we call itself. And then we have a second parameter called data. And the data is actually documented in the doc string of this function, of the init function. And data is, as we see, any iterable. And it says the vector's entries must have at least one element. Yeah, it doesn't make sense to have a vector that has no component. I mean, a mathematician would probably disagree, but we as pragmatic programmers, we don't want vectors that have nothing in them. And then I should have written here, which I haven't, but I should have that the iterables should also be finite. So these are the two properties of sequences that we look at, the iterable property and the size property. And this is really all we need to create a vector. So we need a sequence that is finite so we can iterate over it. And actually, I mean, only the iterable is needed here, but maybe we should also put some documentation that the iterable be better ordered. Why? Well, if you create a vector from the numbers one to three, you want the resulting vector to be one to three and not in another order. So, but, you know, long story short here, data can be anything, it should be iterable, so it will most likely be a tuple or a list. And we saw that earlier when we modeled the vector, we used a tuple. So probably we'll just use the tuple to pass it in as data. And then inside, inside the init method, what happens is cells, again, is the object itself. It's the concrete vector that gets created. It already exists at this point. And then what we do is we loop over data one by one and data is the source of data, the source of numbers we want to put in the vector. And then for every number in this data argument, we call float. So we cast it as floats. And that's actually what the doc string up here says. This will be a vector that only holds floats. And then we put this in a list. Okay, so what this is, that's the list constructor and inside it, we have a so-called generator expression, which we saw extensively in the second part of chapter seven. And why do we use a generator expression here? Well, I use generator expressions in this chapter basically anywhere so that working with the vectors and matrices that we decide here is memory efficient. I only want to, I don't want to have temporary lists and tuples and so on floating around in memory that only get created once and then stored away somewhere and then get forgotten or something. I just want to do all the operations in this chapter on a one by one basis. That's why I use generator expressions everywhere here. But then once the vector gets created, I have to physically store all the numbers in the vector and I do so in a list. And then the list, where's the list stored? The list is stored on the vector. So on self under an attribute called entries with a leading underscore. And the leading underscore in variable names means that the user of this code, so whoever will use the vector class to create vectors later, should not use it. So in other words, whenever we have a leading underscore on an attribute name that is on some object within, so in other words within some class definition, then this is a so-called implementation detail. We don't care, you know, from later on when we use the vector, we don't really care how the numbers are stored. They can be stored in a tuple or in a list or somehow else. We don't care. All we care about is we want to use the vector as such and how the data is stored. This is up to the person that wrote the definition, which is in this case also us. But maybe we are writing this vector definition, this vector class for someone else and the other person will only use the other vector and not modify the source code here. So this is basically only meant for us to be used internally here. And then I do some other stuff. Again, I'm not going over all the code. I'm just giving you the big ideas and then you will understand it better when you read it later on. Okay, and then wrapper and stir. For now I will come back to that. Let's just see. Let's define the class. Now let's see what can we do with the class. So I have run the cell. Note I have executed this. And then as I said, vector now exists. So what happened is, in memory, we have... I draw this a bit bigger here. So let's do this. Maybe a little bit zoom out. So, and this of course will have a type, but we don't know it yet. And here I have the name uppercase vector and this will be a reference here. Okay, and this will be our blueprint. So we will fill this in slowly over time. So, how do I know that this must be an object? Well, it has an address. So it has an identity that means this box must exist. Now it has a type. What's the type of vector? It's a type type. Okay, so I can put in here type. So where have we seen this? Where have we seen that the type of something of some object is type? We have seen this in chapter two when we talked about callables and we talked about built-in functions. But then there were built-in functions like sum and len and so on. But then there were other built-ins that we used. And which one am I talking about? I'm talking about the built-in constructors. So the in constructor, float constructor, string constructor and so on. When we asked type of this, we would get also type. So actually we can try it out. So as I said, what is the type of int? The type of int would be type. This is actually what we saw in chapter two. And this is how we differentiated the built-in constructors from built-in functions. So whenever something is of type type, now I can solve the puzzle. Whenever something is of type type, what this means is it's a blueprint. So it's basically a blueprint that is used every time we create a new vector. So every vector we will create will follow the same blueprint. In other words, every concrete vector, no matter how many numbers are in there, will follow the same mathematical rules that we only define once. So in other words, this blueprint, the type here, gives us the opportunity to write code only once and reuse it. It's kind of like a function in that way. It's a generalization of functions. It's a way to organize code. And we do that. And it's not only a way to organize code, it's also a way to give semantic meaning to the code. So here the blueprint has the name vector, so we know exactly what the blueprint means. So let's go on. And now what happens if I evaluate vector? Well, it just says main. Main is the file we are in. So whenever you open a command line and you don't switch the directory or you don't import something or whatever, then you're always in the main directory, so to say. And this is what we have here. That's what main means, but we can actually ignore main. It's not so important. And then vector is just vector. And we know that this is the, usually this is a so-called literal notation. Oftentimes, so let's just try. And actually main is not defined, okay? But it's okay. So vector is at least defined. So vector in the sense is a literal. So now what can we do with vector? So first of all, remember the dummy class variable. So the dummy class variable can be accessed with the dot operator. So vector dot dummy class variable will give us, I'm a vector. So in other words, I have, for example, a dummy class variable here. And this is something that, in this case, points to some object of type str. And this has init, I am a vector. So in other words, I use the dot notation to follow the references. We have seen this before when we imported the modules, third-party stuff and so on in chapter two first, but then also several other times. Okay, what else can vector do? So can we actually access init? Yes, we can. And what is init? As I told you, init is just a common function. So in other words, the name vector is an uppercase v acts as if it is a namespace. We have stuff init, we have functions init and we have variables init. And I can access the functions and the variables by going via the vector variable. So I say vector dot and this is how I get here. So in Python tutor, whenever we called a function, the function's global scope was actually always some area that belonged to the function call that was kept here, right? And now the namespace, so to say, is this thing here. But it's still namespace. And the namespace is nothing but a list of names at point somewhere. And that's the global namespace and this is the class namespace. Okay. And then somehow magically some names appear. So for example, the vector class has a variable called underscore underscore name underscore underscore. So we call that thunder name. Double underscore is abbreviated as thunder and in this case, we say thunder name. Okay. And Python does that, you know, a lot of times. So it often creates something in the background that follows, you know, something we do. And whenever you create a vector or whenever you execute a class statement, then the class will have a name, at least most of the classes. And the name will also be stored on the class. Okay. And now I want to create a new vector, a concrete vector. So now note this is the blueprint. And the blueprint does not contain any numbers. Now I want to create the vector one, two, three. How do I do that? By passing an interval, this is data now, the interval containing one, two, three to the vector class. So in other words, what I do here is I call the vector class vector and then the call operator. So this looks like a function. The only thing that tells me by convention that this is not a function, is the fact that it's uppercase. And that's convention classes. They are usually written with uppercase, camel case, that's the convention. That means we start with a uppercase letter here. And functions would be snake case, so lower case. And that's, but at the end of the day, I can call a function with the call operator, but I can also call a class with the call operator. And I pass it one argument, which is a list. Now just to make this clear, we modeled a vector with a tuple. And that was just arbitrary. We could have modeled a vector also with a list. But now let's use a tuple. So I will now pass in a tuple, the tuple one, two, three, as the first and only argument to the vector class. And whatever I get back, I store in V. So let's do that. And now let's look at V. What is V? So what I just did, I called the vector class. And what then happened is the init method was called. And the init method, we remember, had two parameters, self and data. However, when I called vector, I only passed in one. That's important. I only passed in data, because I already told you that the self argument, or the self parameter, will always be filled in with a reference to the object. So where's the object? Okay, what happens is when I call vector, Python is smart enough, and creates a new box. And the box will have a type, of course. I don't have it yet. But what we do is the vector, when I call it, it creates a new object. And the object is stored as V. So whatever this object is, the lowercase V is referencing it. Okay? So this is what happened when we called vector. We call vector. And then the init method in the vector class will be executed. The first argument that is given to the init class, to the init method, will be the reference to this object, because that's the new object. This is self now. This is the concrete object that is about to be created from the vector class. And then the second argument passed to the init is data. So what happens? This init method initializes this new object that is created. And then once the initialization is done, the object exists in memory and has the name V referencing it. And how can we see that V must be an object? Well, of course here, V has an ID. So this is the address, the identity. And now V also has a type. What's the type of V? Well, the type is vector. So the type is now uppercase vector. And now uppercase vector has the same variable name as the name vector that references the class, the blueprint. But now this new object is of type vector. Okay? And also on objects we have a danda class attribute and the danda class attribute also has, you know, vector. So type of V and V dot danda class is actually the same. So this is a synonym. This is also a name that Python creates for us automatically. And then what can I do with lowercase V? Well, I can now put it in a cell on its own and evaluate the cell. So I will now evaluate V and I get back vector, parentheses, parentheses, one, two, three as floats, parentheses, parentheses. So it's like up here, just up here the data, the data argument only has integers. But if I go back to the init method how it was defined, ah, there was one too much. The init method how it was defined we had this generator expression where we looped over all the elements in data which is in this case a tuple we passed in and we called the float construct for every number in it. Every integer that was in data in the argument here will become a float. Now the question is how does or why does Python give us back such a nicely formatted representation of the vector? Well, first of all, whenever a vector or whenever a representation of an object of any object can be copy pasted back into the code cell and executed and it works, we call that a literal, right? However, this is not a literal so that's now what we will call the text representation of an object. So every time I copy paste now this back and every time I now execute this code cell what happens is a new object of type vector will be created with the same numbers in it the same components. So in other words just as the literal notation of any built-in allows us to create a new object with the same value as the object whose literal value we are copy pasting in the same way I can copy paste the text representation of any object that will be I can copy paste these text representation back into a cell and what will happen is I get a new object with the same value. So basically conceptually nothing new happens the same thing that happens for built-ins also happens here Now the question is why is it so nicely formatted? Does Python do that automatically? And the answer is of course no where did we teach Python to give us such a nice text representation? Well it's of course a special method and it's this one here the so-called wrapper method or thunder wrapper and wrapper is the abbreviation for representation and by definition if you let me see if you went back to the data model page here in documentation and if we read through it then there is a convention defined and this convention basically states that the text representation of any object should be a string so a built-in string type a string that when copy pasted back into a code cell gives me a new object with the same value that's the definition of the text representation and now by now we know what is a string well a string as we saw in chapter 6 is nothing but bytes and bytes are just sequences of 8 bits and the bytes are encoded into code numbers so every bit and every group of bits or every byte at the end of the day translates into an integer as we saw in chapter 5 and if we decode the bytes we get a unicode character which we saw in chapter 6 and the definition or the idea of the text representation is that we have unicode text that we can just copy paste and get the value back in a new object so that's the idea of text representation so in other words this is the output we will see in code cells that we see for built-ins as well but now because we don't have a built-in data type we have to create this on our own and also note there is a string special method these two are very similar however the DunderRapper method is there to create a text representation primarily for computers so another computer another machine can get exactly that represents the vector in this case decode the text into bytes and so on and will basically create an object similar to the one or exactly the same actually to the one that we are reading so the wrapper is basically a more technical thing of representing an object and then we have DunderString and DunderString has only one purpose and it is to create a representation text representation that we as humans want to see okay so whenever we use and I will just show you how this works whenever we use for example the print function and give it our object here the vector object then we want a nice representation of the vector so that we as the human know what the vector is and that's why we have two different representations here the other is called Stir or DunderRapper, DunderStir and both of them return as we can see a string so we have a format an F string here both of them return a string and the first string is to be consumed by a machine and the second string this one is for us it's the debug output so to say let's see what that means since we have a vector V again if I just evaluate the vector in a code cell I get back the text representation the DunderRapper result but now I can explicitly call Python's wrapper built in function so I can call wrapper and give it V and I also get it back but now note how now I have here the quotation marks so this is now a text string and if I go back here I don't have the quotation mark so the difference is because I don't have the quotation marks this I can copy paste back into the code cell and generate a new object with the same value and because here I have the quotation marks I could also copy paste this back but then I just get a text string with the value so this is a little bit different but don't get confused here it basically returns the same thing just the one thing returns it as a literal and the wrapper explicitly returns it as a string object and then there is Stir the string constructor that we saw in Chapter 6 and we can also call the string constructor with V with the V as the argument and then we get now the nice representation for us humans so the difference is the first text representation the wrapper we want to be able to copy paste it back in to get the same object back and the result of a string we don't want to copy paste it back what we want is we want to get a I would call it a summary so that we as the human know what the vector is and I built this Thundersturm method such that it gives us back only the first component and the last component because this vector could be very big it could contain thousands of numbers and I don't want and if a vector contains thousands of numbers this would also contain thousands of numbers and would be very long but we need all the numbers otherwise we couldn't copy paste it back in the command line and get it back so the second there that's for us as humans and we don't really care about the individual numbers but what we care about is how many numbers there are that's what the brackets 3 means that means the vector has just three components in it so we have seen now three special methods the most important one was the init the init is the one that does the initialization and also note how if I go back there was for example also a input validation here so I say if the passed in data does not contain any elements so if the length of the list is 0 then we raise a value error and say the vector must have at least one entry so the initialization the init method not only takes the arguments and stores them on the self object on the vector itself but it also does some input validation and let's see this at work so let's see where we put it maybe here so here I create a good vector but let's say I now create a vector and I pass in let's say an empty list so I pass in I try to create a vector that does not have any components inside and what happens if I execute this I get a value error and it says the vector must have at least one entry and it's exactly the value error I just showed you so this is why the init method is so important the init method gets the arguments that we want to collect in order to create a new object and then it verifies that the object is you know the argument that we pass in is good and our assumption was the vector should have at least one component and then it creates the vector and that's what init does so this is a really important method and then we have seen two other examples of special methods now the wrapper and the stir they are not so important actually but I just want to show you how things some built-in functions translate to what's going on with an object so the stir built in for example when I called it basically just forwarded the call on to the done the stir method on the object okay maybe let's get rid of this again here and let's go on so now we've understood the basics of the basics of glass definition and I will subsequently fill in the blueprint and also the example as well so that you see that what goes on in memory here so now we do the same thing for a matrix we have now taught Python a early implementation of what a vector is and now we do the same thing for a matrix so we use the class statement to create a new blueprint so at the end of the day when I execute this what this will do is this will let me zoom out a bit this will create another blueprint and we also know already that this must be of type type because it's a blueprint and then let's see what is in there definitely we will have an init special method and I don't what I don't do now is because this wouldn't the diagram the space wouldn't be enough of course there is a function object somewhere and we have a reference from the init to the init to the function object so this is just an ordinary function the init method and I don't do this here what I did for the string here I don't put references everywhere just assume that everything that I put in the blueprint also has a reference to some object because everything in Python is an object and so I just have a more simplistic way of showing this to you and now let's quickly analyze what is how does the matrix work so a matrix is initialized also from data and now the thing is this we have to now assume if we pass in the rows or the columns first and so let's read documentation so it says data is supposed to be an iterable of iterables the matrix entries and must be provided with rows first the number of columns must be consistent across rows okay that means every row has to have the same number of columns or every column has to have the same number of rows in other words I must not forget an entry to the matrix and then also all the entries are also converted to floats so how do we do that? Well I have here a nested list comprehension a nested generator expression in here so I have here I have an inner expression which is a list and I have here a generator expression and then the whole thing is another generator expression so I am now nesting the generator expressions and this is just to be memory efficient but you don't have to really understand everything here that's okay and then what we do is we do some input validation and I'm not going over this but what this ensures is that basically I cannot create a matrix with no entry and also all the rows have to be have to have the same number of columns and then we have a wrapper and a stir special method who do the nice text representations and that's it so let's see what is matrix matrix is an object it's of type type that's what I put here so it's also a blueprint and it goes by the name matrix so now the matrix name points here so what can we do now I can create a matrix so let's create a matrix M and the matrix consists of three rows with the numbers 1 through 9 so I have here the first parentheses is again the call operator I'm calling the matrix blueprint to create a new object a new matrix object and then I have one interval which is which I represent with the brackets here and then also the interval the list contains three tuples and every tuple contains three elements and let's see if that works it does and now what is M? M is an object of type matrix and it's a matrix and we see it has also a nice text representation and don't be confused that we only have parentheses here we didn't hear this text representation as you will see is intentionally built this way so also see that for example with trailing comma for technical reasons here it's not so easy to build such a thing so you will read it up in the source code when you read when you do the reading but at the end of the day if you copy paste this back this would create a new matrix with the same elements in it and it doesn't matter because when we designed the init method inside the matrix so we just said we want data to be an interval of intervals we don't care if it's a list of tuples or a list of lists or a tuple of lists we don't care it must be an interval of intervals and that's what we have here and that's also what the output down here shows so what do we have here we have again a new object and this object is now of type matrix and we will have something in it and this goes by the name M so that's the situation and maybe to make this a little bit nicer so what matrix means is the matrix type means whenever we work with V or M then Python will go ahead and look up the type and then go to the blueprint and the blueprint will tell Python how these objects should behave so that's the whole story and then we can of course have as many objects as many vector objects and as many matrix objects as we want they all work like a blueprint says so far so good and now let's start with putting many many more special methods inside our two blueprints and then to slowly over time make sure that the V and the M object behave just like vectors and matrices and then at the end of the lecture we will be able to multiply M by V that's the whole point so let's talk about another topic here so called computed properties so what are computed properties well let's look at the example of a matrix so when I created a matrix for example this matrix here M the matrix consists of nine elements arranged in three rows and three columns so if I asked you what's the dimension of the matrix you would tell me that's a 3x3 matrix and if I told you that no that's a I don't know a 5x7 matrix then you would say no there are only nine elements and they are arranged in three rows and three columns so what I'm saying is just by the fact that the nine numbers are arranged in the way they are we have derived properties the matrix has a derived property which is the dimension so the property is kind of like dependent on the state or on the data that is in the matrix so when I want to ask the matrix how many rows do we have this is not something we can make up this is something that we just look at the numbers that the matrix is storing and then we know the answer so how do we do that we take here again a new version of the matrix class so throughout this chapter whenever I show you a new definition a new class definition of a matrix or vector I will often leave something out that we've seen before just to keep it at least a little bit short and then finally at the end of the chapter I will show you the full picture of the matrix and the vector class so what's new here the two new things here are the properties what are properties so these are attributes that get calculated and how do they get calculated well the blueprint does the following the blueprint takes as the argument the concrete matrix in this case that stores the data and then looks at the data and then calculates the attribute in this case n rows and n calls and returns that for the object so maybe just to finish the diagram here maybe let's have here the list 1, 2, 3 and here let's have a list of lists just draw in the numbers second list 4, 5, 6 and another list 7, 8, 9 so just to show you that the actual data of the vector matrix they are in these objects of course they are not in the blueprint but now if I want to know how many rows and columns I have what we do is we will define a function here in the blueprint that when executed is given the concrete object as the self-argument and then the blueprint will look at the list of lists that is stored internally on the object in the hidden property entries and then just do some counting and give us back what we are looking for so in other words what does n rows do and rows just takes the hidden list and we know that the hidden list in entries is a list of lists and every inner list is a row so in other words if I want to know the number of rows I just have to take the length of the outer list that's it and if I want to calculate the number of columns I have to index into the list into this outer list here to get to any of the inner lists and each of the inner lists basically resembles a row and then if I take length function of the inner list of one of the three inner lists here I get three back because all the rows are guaranteed to have the same length as I ever did so in other words we are just looking at the first inner list and this is the number of columns right so we have two functions however we don't want to call a function here so when we use v and m or in this case m in particular I don't want to say m dot n rows call operator I just want to say m dot n underscore rows as Python built in property does it basically makes Python call this method when we don't call it so let's first right here we have an attribute n rows and we have a second attribute n calls let's define the function and just see an example so here's my new my new m and now here's m I created the matrix m and here it is and the matrix consists of two rows with three elements each so now if I call dot n rows on m then the function we just saw gets executed and if I call dot n calls on m then the n calls function in the blueprint gets executed but I don't want to write here a call operator and that's why we write property and the property then makes Python call the function so now in other words conceptually speaking I create a matrix with six numbers arranged in two rows and because of that the matrix has derived properties the number of rows is two and the number of columns is three and then of course because this is a derived property we cannot set it we cannot just claim that the number of rows is three so what do I mean by that I cannot try to set the number of rows to three let's do that I get a so-called attribute error I can't set the attribute in other words these properties they are read only and that makes sense in this context it makes sense there are ways to make properties writable however let's say if I have a matrix a three by three matrix and I tell the matrix hey your number of rows should be two what should the matrix do it actually would have to get rid of the last row right so in other words we could see a situation where we allow the matrix to set the number of rows and the number of columns but then if we allow the user of this class to do that then we would have to have some logic that would basically throw out some of the some of the data in the matrix and also note that if I have a three by three matrix and I try to set number of rows to ten what should we do should we make up data then so in other words what I'm saying is it's really not a good idea to make this property readable but there are ways in python to make properties readable writable I mean read only but in the given context it absolutely makes sense okay these are derived properties now let's continue a little bit so now the next topic is instance methods versus class methods so let's see an example then it becomes easy so let's first look at this new version of matrix and now I want to shift your focus on the transpose function so what is transpose every matrix once it exists can be transposed transposition of a matrix means we flip the rows and the columns so how do we do that or actually what does that imply transposing is an operation that is always attached to a concrete matrix we cannot transpose a matrix that doesn't exist yet I always have to look at a concrete matrix in order to transpose it which is why transpose takes an argument self so whenever I transpose a matrix whenever I ask the blueprint hey transpose something the blueprint will say give me the matrix I should transpose and then we get a concrete matrix here for example m and then when we call the function here the transpose function in the blueprint then self will be set to m automatically so whenever we call a method as we shall see we don't have to specify self on its own and then what the transpose method does it references the matrix class itself and calls it this matrix itself and passes some argument to it we don't want to understand what is really going on this is the magic that you should read at home or just try to figure out otherwise but what it conceptually does is and that makes sense when we transpose a matrix in this case we get back a new matrix so matrix gets called again with other data and what the the zip and the unpacking self it basically flips the rows in the columns but it's not so easy to see why it does that but in other words the transpose method will give us back a new matrix so it returns a new matrix object here okay so let's first see how this works let's create a new matrix I always have to create a new matrix the blueprint what really happens in Python is I get a new blueprint somewhere else and because of that because the objects are attached to the particular blueprint I also have to create a new object which is attached to the new blueprint that's why we always have to keep recreating the m's and the v's so what is m now m is just the matrix as we created it two rows, two by three matrix and now I want to transpose it well now that we defined the transpose method in the matrix blueprint we can now go ahead and call the transpose method on every matrix object which is here and that's it and then as I just said the transpose function inside the matrix class takes its first argument as self we read the first argument as self we can assume that Python automatically gives in the reference to this object here this is basically true for any function you create inside a class the first argument is always automatically filled in with a reference of the object on which we call the method this is also true for init so in it all these special methods are not different than any of the other methods the non-special methods but again the method always works with concrete data with a concrete matrix so let's do it it's called m.transpose and what do I get back I get back a new matrix which has now three rows consisting of two columns and the first here the first column one four and now is the first row one four so it seems to work and of course we know if I take the matrix the transpose twice I should end up with the same matrix so let's do that let's verify if that works so I call m.transpose and then call operator because methods have to be called it's not like properties and then I say and this is an example of method chaining that's what it's called so because and that's something that we have to implement because calling transpose on m returns a matrix we can on the new matrix call transpose again second time and the third time and the fourth time this is called method chaining and the nice thing about method chaining is that it reads really nice in English basically so I have a matrix and I transpose it twice what get I back I get back a new matrix n and then we know that n should be like the original matrix right and we see that the n now is a two by three matrix and looks just like m one problem let's first verify that m and n are different objects so we use the identity operator is and indeed m the matrix we created and it's transpose transpose n are two different objects which means when we call dot transpose here a new object gets created and we call dot transpose on it again and then another new object gets created and then this one would be stored as n this is basically what we did but we would actually end up with one intermediate matrix that gets lost and a second matrix that gets stored with the variable n and then we end up with two matrices that have the same data inside them however the n here if I use the comparison operator for m and n m and n unfortunately don't evaluate equal and that means the comparison operator does not work and there's a reason for that how in the world should python know how it should compare two objects of our new custom data type because we haven't taught python yet we will of course do so later on but for now comparison does not work okay this is a method a so-called instance method and then we contrast this with a so-called class method so at the end of the day everything we see in this memory diagram in the memory part is an object so the class is an object and this class is an object and then what are these objects well these objects are objects of type vector and objects of type matrix but they are also objects everything is an object in python however we see that by the green arrow which are through here that the blueprints the classes and these objects so these objects and the upper two objects they have a special relationship to each other and how we say that what we call this relationship is we say when the matrix so we call this here the blueprint and the concrete object we refer to as the instance right so that's the behavior so an instance is just a concrete example or a concrete version of the blueprint and the blueprint is called class but we use these terms class and instance to refer to the role that the objects have with each other the relationship they have with each other but at the end of the day they are all objects so object wise they are the same but they have a certain relationship and the relationship is that of this object is the class for this instance and of course it's the same up here so and whenever now we do something with the concrete instance we usually refer to that in this case as an instance method so transpose is an instance method why because the method the function transpose when executed it needs a concrete instance to work and the concrete instance is called self and I told you that we don't have to pass it inexplicitly because Python does it for us so Python by default assumes that whenever we call a function in the class blueprint that it is an instance method and that the first argument is the concrete instance on which we call the function however there is a second type of class method which is called a class method and a class method works like this we specify it just as we specify any other function within the class and then we use this add operator and just write class method that's built in by Python just like property and now the first parameter of this function by convention does not be called self it is called CLS which is the short form for class we cannot write class because class is a keyword that we cannot use so we abbreviate it with CLS for class so just as self gets filled in when called with a reference to a concrete object CLS when called gets filled in with a reference to the class itself that's why we call it a class method so what are class methods good for class methods one good use case for class method is to provide a so called alternative constructor so what is a constructor so look at init up here init is what gets executed whenever I call the matrix class so matrix and here I have a call operator whenever I call the matrix class whatever I pass in here is put through the init through the init special method the initializer now in this particular example we assumed that the data the data argument to the initializer is provided in a row major way so as a list of lists and the inner list arose as I just said we basically just made that up we could also do it the other way around and that's exactly what we do down here we define a function called fromColumns which is also given data but data is also an interval of intervals however here the inner lists are supposed not the rows but supposed to be the columns so in other words the role that this fromColumns function down here takes is the same as the initializer just that it basically has to transpose the data so in other words what we do here is we know that whenever we create an object of class matrix in this case and we give it data then we assume that it's a row first but then whenever we just you know whatever matrix we get back if we transpose it then we know it's column first so that's the trick here so basically this allows us to create a new matrix by passing it in the data in a column first way and then we also look at this the up here matrix is called and we use matrix as a variable so the class statement creates as we saw a variable called matrix this is the one that we have in the list of all names and we can always just call matrix the name but we don't we don't just have one reference to the class in particular as I told you that this class method property of this class method thing here ensures that the first argument to the from columns function is a reference to the class that is why the cls variable here is also a reference to this particular box here to this particular object in other words I can call cls as well cls and calling with data is exactly the same as calling matrix right however I don't what I do here is a hard code so to say the name in here it references itself we see that it's obviously possible but it's not so good for some other reasons that you will see when you read the chapter in particular when you get to the spot where we talk about inheritance at the end of the chapter and so we prefer to also parameterize the class here but it's not so important you could also also works so now what does from columns do as I said it works as an alternative constructor so let's construct a new matrix by giving it by giving it the data in a column first way so here I just have the transposed data and I don't call matrix but I call the from columns class method on the matrix class so I write matrix dot from columns and I pass in the data in a column first way and I store the result as M and now if I look at M the matrix is of course the same as before so the matrix the text representation of a matrix is always row first that's how we designed the matrix but now we have an alternative constructor that's the role that class methods often play but syntactically just reiterate the point of the section we have a function inside a class definition the first argument will automatically be filled in with a reference to the concrete object that we are creating or working with on which we call the method which means the lowercase M or the lowercase B this is the default behavior of python because it's a default behavior we don't have to specify anything above the function and by convention we call this argument self to communicate this is a reference to the concrete object and whenever we want to work with a reference to the class object itself then what we do is we just put this at class method above the function and then by convention we don't call the first parameter self but we call it CLS for class and then python automatically puts in a reference to the class so when we see when we call the from columns method we also only specify one argument there is no two arguments here and that is because the first argument is automatically filled in but now it's not a reference to the concrete object but it's a reference to the class blueprint so it's actually not so hard it just gets a little it needs a little getting used to and then you can do it okay so now another topic sequence emulation so we spend lots of time on analyzing sequences in particular in chapter 7 but we've seen sequences before strings are also sequences in chapter 6 so what is a sequence a sequence is any object in python that fulfills 4 criteria 4 properties the first one is it has to be a container so it's an object that contains references to other objects that is obviously true for vectors and matrices because vectors and matrices are just containers that contain numbers so first property is fulfilled the second property is these objects that are contained should be finite we call that a sized object and certainly a matrix and a vector is also finite at least for the vectors and matrices that we built here if you asked a mathematician on that topic they will probably tell you that there are vectors that contain an infinite number of components but we are not so crazy as the math people we are pragmatic programmers so we just don't work with infinite vectors so that is the second property the third property is the iterable property so we want to be able to loop over stuff we have kind of a hard time to see how we can loop over vectors and matrices but for vectors it's actually quite natural if the vector 1, 2, 3 consists of three components then why shouldn't we just loop over the components in order from top to bottom in the vector that makes sense for a matrix because it has two dimensions we would have to make up an assumption what is the order do we want to order in a row first way or in a column first way and of course as you can guess we will loop over matrices in a row first way but again we can loop over the elements or the entries in a matrix and then what's the fourth property of a sequence we call that formally reversibility and that just means we have to have a predictable order and do vectors and matrices have a predictable order yes they have vectors definitely and the matrices as well right because we have like a coordinate an x, y coordinate for every entry in the matrix so in a way the numbers in a vector and in a matrix are ordered so all the four properties with a better concept of a sequence they are fulfilled for vectors and matrices so now that means we can use vectors and matrices in place of any object that also has those four properties so let's see how we can implement these four properties for our vectors and matrices here is it so we do it for a vector first init and wrapper and then we have two new special methods the first one is called dunderlan we can already guess that this would probably correspond to the lan build in function and then we have dunder getItem dunderlan takes only one argument self that makes sense you know that's just a reference to the concrete vector and then getItem takes two arguments the first one again because it's an instance method self and then the second argument is index that makes sense because if we want to index into something let's say you want to index into a string how do we index well by specifying an index and an index is nothing but an integer starting at 0 and upwards until 1 minus the length however we also saw that we can have negative index indices for lists and tuples and also strings so that's basically the idea behind and now what happens is we have this hidden entriesList self.underscoreEntries is just a list object which stores the numbers inside the vector kept a secret from the user of the vector but still it's a list, it's on the vector so what do we do if we call the dandelion method all that we do is we call the lendBuild and function on the hiddenList object that's it and because vectors are one-dimensional then at the end of the day the length of the inner list that holds all the numbers is also the length of the vector so in other words what this method does it forwards the function call so whenever the dandelion is called it forwards the function call to the innerList it calls lend with a reference to the innerList and now let's look at the item, what does it do at first we do a type checking so we make sure that index is indeed an integer and then we also forward the indexing so what we do here is we just return the element of the the element from the innerList at the index position and that's a smart way to do because we know that self.underscoreEnter is a list so we can also index with negative indices and by just forwarding the index parameter here the thing is we don't even have to implement the logic of you know of indexing with negative numbers because the list object that is hidden inside the vector takes care of that so let's define the new vector create a new instance of a vector with the numbers 1, 2, 3, 4 in it and now I can call the built-in function lend with v as the argument and I get back 4 and now I can also index into the vector with index 0 and I get back 1 point 0 why do I get back 1.0 well obviously if we look at the lower case v it only contains floats of course and now let's check if we can how we can index let's let's try to get the last number this will be index 3 this works what happens if we provide an index of 4 we get an index error list out of range that's how we see that we use we implemented the vector with a list however we could also do like this v index minus 1 and we also get a 4 or index minus 3 to get this case the 2 ok so indexing works and the only two things that we needed to implement for the vector was dunderlen and dunder get item and we also implement this of course as we shall see for the matrix I just put it here so that I won't forget so these two special methods they implement the length and the indexing now you wonder there are two more properties missing how can we loop we didn't specify any looping well guess what python is smart for us it's very smart so python knows that if something can be indexed into indexing means 0 1 2 3 indexing means there has to be an order otherwise indexing doesn't make sense so whenever we implement get item the dunder get item method we communicate to python hey we are in order this data type must have an order because otherwise indexing wouldn't make sense so we have order we can index and we have a length we have a finite thing otherwise we wouldn't have dunder length so by just specifying dunder get item and dunder length we basically say python you can loop because python is smart enough to figure out that in order to loop over something that it can index into it just starts with index 0 index 1 index 2 and so on and goes to 1 minus the length and because both the indexing works and python can get the length because we implemented it that's because that's why we can figure out the looping on its own we don't have to teach python how to loop because python here just makes the next step automatically so let's see we can loop over v we use v as if it were an integral and all of a sudden we can loop over the vector that's nice we can also loop over the vector in reverse order because we do have an order to begin with we have the last we have a length so we know what is the last index python just goes from the highest index back to 0 and can figure out the indexing or the looping on its own and because looping works we can also use the in operator the in operator is the characteristic operator for the container property and what does python do now if I run this cell 3 in v well what python does behind the scenes it does a linear search it goes over the v object one at a time until it finds a component in the vector that evaluates equal to the number we are looking for and if I specify let's say number 5 then python will go from the first to the last element of the vector and no no number in the vector will evaluate equal to the 5 because of that we get a false so all the four characteristic operations of a sequence work by just specifying the dandelion and the dandaget item ok that's nice so however can we mutate can we mutate the vector so can I assign for example to the first index in the vector the number 99 so can I replace the one with a 99 I get a type error the vector does not support item assignment now guess what so I'm not going to show you this here in the presentation but there is a method that we used dandaget item I tell you and this also in the chapter that you can read there is a method called dandaset item and if you implement this then you can actually do the item assignment as well and this leaves us with the question of should you allow that should you allow here updating an individual element in the vector and that's the question as to whether we want to design the vector and the matrix classes as mutable objects or data types or immutable data types ok and I told you that if we want to reason about code and analyze code it is easier if we have immutable data types which is why in the presentation here I only show you the immutable version but if you go back or if you go into the chapter and read the chapter then you will see also an implementation of a mutable vector and a mutable matrix class that shows you how you can implement code so that you can actually change the matrix after it was created but not in the presentation and here is the same thing for the matrix class so we have a new version here and then in order to let's just look at Dunderland here so Dunderland does also something smart here it returns the product of self and rows times self and calls ok and now what is n rows and calls well it's these properties that we've seen before right so in other words in order to figure out the number of entries in the matrix I don't have to do a lot of math here I can just use the two properties and multiply them and return the result ok so in other words the Dunderland method reuses the code from the n calls and n rows attributes here or properties so that's also something that is important just as we write functions to reuse code we also write classes to reuse code in particular to reuse code across objects of a different type because they all share the same functions but also within the blueprint we also want to reuse code and we do that all over the place here actually so even in the initializer you also see that I'm comparing to self and calls and that's a cool thing so in the moment when the matrix is initialized we can already use the n calls property which does this calculation so we use n calls which is here already up in the initializer and also note this in the initializer when I check if the user provided at least one number in data we use the LEN built in and call it on self that looks weird right but what that does is what LEN with self does dispatches the flow of execution down here to the dandelion method and then dandelion method runs and it looks at those two numbers and dispatches the flow of execution here and here and once we get the result back then the dandelion returns to here and then the result is compared to zero and if it is zero we get the value error so you see how not only code that is at the bottom but also code in the top uses code in the bottom so we have a really sophisticated way of code reusing each other and then the dandelion method is a little bit more sophisticated why I not only want to implement indexing by an individual integer but I also want to implement indexing as a coordinate system so I want to be able to specify to say give me the entry in the first row in the first column and that's why the code here is a little bit longer but if you read it at home there's nothing in it that is hard and again here maybe we use this mod in an intelligent way but all of this is not so hard to do and we do also type this patching so when index is an int then we do the indexing by one integer but if the index is not an instance of int but if it is a tuple and a tuple consists of two numbers then we do this indexing by a coordinate system and if the index is something other then actually we raise a type error and tell the user that this is not a legal way to do the indexing so let's see the new matrix class a new matrix object what can we do now by two matrix has four entries so length of m should give us four it does and then I can index into the matrix with as I said with a index with an integer index and let's say if I get the second index what I get is the two so the indexing goes also one row at the time that's also important so the indexing is row first and then I can also as I said do the indexing with a coordinate so if I say index 0,1 0,1 is really a tuple so we are leaving out the parentheses but it's really a tuple and the first zero means get the entry in the first row and the one means gives me the entry in the second column so this would be the two okay and then of course because I have dunderlen and dunderget item I can do looping so let's loop over the matrix and again we get the row the row major version and of course because of the indexing I can also go in reverse order and because the looping works I can do a linear search of all the numbers that are in the matrix and can check is the one for example in the matrix it is and I can also check is the 10 in there and then it goes over all the entries and it won't find the 10 and after it has gone over all the entries in the matrix it says false so it's a linear search it's a slow version of a search just remember if we need to search in a fast way we should probably use a dictionary dictionaries we covered extensively in chapter 8 we also actually built a matrix around a dictionary this would also be a nice way but here we built the matrix around a inner list of lists so that is why searching in the matrix is rather slow so you already see with this short comment that the implementation details they actually matter so it matters of what the use of the matrix class is more likely to search elements inside the matrix then this would not be a good matrix because the inner list but if we use the dictionary inside then of course the matrix would have lots of more memory usage because we saw because of the hash table implementation in chapter 8 that dictionaries have a lot of empty memory by design so that the searching goes fast and we don't want to do that with a matrix why? well because a matrix if we have let's say a big matrix say a 10 million by a 10 million matrix and we built this around a dictionary then we will use very much very much memory actually so this is the question that you have to answer if you design a library what is the common use cases for the use of the library and then you have to make the assumption and here we made the assumption that we just use a list of lists okay this was the sequence emulation so Dunder, Lend, Dunder, GetItem and now we have some more special methods and you get the point now right we will introduce more and more special methods and the special methods usually in their name already tell you with which built-in function for example they work with the built-in Lend function and so on and this will not change too much so let's look at a different way of looping so if we went back the previous version of matrix allowed us only to loop over the entries in the matrix in a row major way and what this new version does I will only quickly go over it it provides instance methods called rows calls and entries here and what they do is they allow us to loop over the matrix row by row column by column or over the individual entries so when we loop over a matrix by rows we can what we can do is and this is what we do here is we can interpret we can interpret every row in the matrix as a vector which is why this generator expression that gets returned gives back individual vector so for every row in entries here in the inner entries list we create a new vector and this is just to show you how you can really tailor make some instance methods to provide more ways of in this case looping to a user so now the user with this new version can actually decide if they want to go over the matrix in a row by row column by column way and then loop over individual vectors or you still want to go over the entries one at a time and then the entries method here takes two more arguments called reverse and row major that have default values so by default we go in forward order and not in reverse order and by default we go in row major way and not in column major way and then what this method does it gives the user many many ways to customize the way in which they want to go over the matrix okay so these are three new instance methods just examples of instance methods so this could have been already in the section on instance methods versus class methods because there is nothing new here but then the one thing that is here that belongs to the iteration idea is the dunder iter and the dunder reversed special methods and so what does dunder iter do so whenever we use the pythons built in iter function with an object what pythons does is it goes into the objects blueprint so in this case the matrix class blueprint and tries to find dunder iter and then what does dunder iter do dunder iter by default makes the matrix loop over the entries in forward order and row major so this is why there are no arguments specified so in other words dunder iter also dispatches the flow of execution to dot entries and in the same way dunder reversed dispatches the flow of execution to entries again but now sets the reverse flag to true so that iteration is now in backwards order and this is what the pythons reversed built in calls so whenever you reverse which we have done a lot of times whenever we say for something in reversed something then every time we do this the dunder reversed on this particular object will be called okay so let's see what this means so I create a new matrix of course and now I want to loop over the matrix one row at a time and I won't get back the row as a tuple or a list but I get back every row as a vector so now also what does this tell us this means now so far before this example actually we've only seen that vector class and the matrix class independently so whatever the matrix did only affected a matrix and whatever a vector did only affected a vector but now we have our first example of a method the entries method or the rows method here where doing something with a matrix returns us a vector so whenever we loop over the rows in this case the time in every iteration of the loop python gives us back a vector object so a new instance of class vector so in other words the matrix and the vector classes the blueprints they are related and that makes sense right so we have here special methods instance methods I mean rows calls and entries and then we have seen thunder iter and what else thunder reverse of course the space in this blueprint won't be enough to fill in everything you will see but you get the point so let's loop over the rows we see that we can also loop over the calls and we get back vector objects vector instances but one column at a time and then finally we can go over all the entries and dot entries will just by default as we saw go in forward order in row major fashion so nothing new but now I can go ahead and pass in a keyword argument row major equals false and that allows us to loop over the individual entries in the matrix in a column major way okay and of course we could also just loop over the matrix still without a method so the loops all here call some method some instance method but here we just loop over m itself and what that means is python looks up the thunder iter method on the blueprint and then as we saw the thunder iter will just dispatch to the entries method which is why we just get back the entries in forward order and then we can use of course the reverse built in and go in backward order that's what the thunder reverse special method does so now you know you get the point so by defining more and more methods and special methods on the matrix and vector classes we can now work with m and v in a more intuitive way I mean now at this point in time the matrix object m lowercase m here can do anything that we could also do with a list so the looping the container and all these things they work just as if we had a list however we have some additional methods like rows and calls and entries that allow us to even customize that so this is like at the moment it's basically like a list on steroids here and this is really what it is because when we started this chapter we modeled a matrix list of lists and it couldn't do any math and now we taught the list of lists because a matrix is just a list of list we taught it how to do some special things and python learned so now we can reuse all of this but so far we haven't taught we haven't taught a python any linear algebra yet this is yet to come but the looping and you know all the stuff the sequence emulation the iteration part all of this we have basically taught python already okay let's go on and now another topic something that we have seen before with a different name that's just a an easy topic here we have talked about duct typing several times in this course duct typing means that if I can pass in different types of objects to a function for example and the function still works with all the different types of objects then we say that all the objects they walk and quack alike and because of that what we can also say is that in python the type of an object is not so important the thing that is important is how the object behaves and when we are in the context of classes of object oriented programming there is this technical term called polymorphism which means this we have different things that's what the poly means that behave in the same way so example I use the built-in sum function with a table one two three four and it works give me ten I use the built-in sum function with a list one two three four it works so the sum function in other words only relies on the fact that it can iterate over the object we pass in and the object we pass in must be finite I cannot take the sum of an infinite object but other than that these are the only two properties looping, iterable and finiteness that we need and we have seen this before in chapter 5 when we also looked into abstract base classes for the first time remember when we looked at the complex number the flow data type and so on and then we classified them in abstract terms and then we implemented something some logic called goose typing at the end of chapter 5 this is something similar back then in chapter 5 we classified individual numbers and we said that among others the integer walks and quarks like a float because the integer 3 behaves like the float 3.0 so they behave alike but now we extend this to the next level and we look at bigger types not individual types simple types like for example numbers scalar types but now we look at some composite types like tuples list and so on and they also behave alike so in this case a tuple behaves like a list but that's nothing new, we knew that in chapter 7 already but now I can also take the sum of the vector and this vector the current v I have in memory also has numbers 1 2 3 4 in it so now sum of v works so how does the sum built in function you know know how to deal with a vector well the thing is it doesn't the sum built in function only knows how to go over an interval until it's end until the finite elements are over and just add up all the numbers in a running total way and that's it, that's all the sum function knows and because we can loop over vectors and because vectors are finite that's what the dandelion is for that's why we can call the sum built in function with v but we can also call it with m, it's also 10 if I have a 2 by 2 matrix currently but lowercase m that has the numbers 1 2 3 4 in it it doesn't even matter in what order I loop over them, the only thing that matters is I can loop from beginning to end and they add up to 10 so that's an example of polymorphism objects of different types behave alike and in the book, in the chapter there is a function called norm that looks at vector and matrix norms and that's the same here in the context of linear algebra but here in the presentation I just keep the example simple so we just look at the idea of summing up the individual numbers inside some container but we can also extend the idea of polymorphism to functions so what is polymorphism basically it means we have in this case a function that is capable of working with objects of different types and we know by now that we have as the programmer who implement the classes we are the responsible person to make sure that the sum function can work with a vector or matrix by writing the special methods let's see another topic which I call it representations of data so in math I know that I told you that I don't expect you to know lots of linear algebra for this chapter but let's say you have a matrix x that gets multiplied by a vector y any vector that we multiply a matrix by from the right side maybe and usually is interpreted as a matrix that consists of only one column and then we say that the vector is a column vector similarly when I multiply a matrix by a vector from the left hand side I often times assume that the vector is a matrix with one row and the row is just the entries of the vector in other words I can view a vector as a matrix of either one column or either one row and because of that what we really say is that the data inside the vector is the same data it doesn't matter if the data is modeled as a vector or a matrix there are different ways of representing the same data and so what we do here is in the vector class we define a new method an instance method called as matrix that does one thing it returns a new matrix object a new instance of a matrix that is either a one by n or n by one matrix where all the entries in the elements are components of the vector so now I can convert so to say any vector I have into a matrix but I don't really convert a vector because once a vector object like v for example exists in memory I can never change a type the only thing I can do is I can create a new object with a different type creates a new box of type matrix and puts the data from the vector inside the matrix and arranges them in either a row as a row or a column ok similarly I can do the same thing for a matrix so the matrix now is given a new instance method called as vector that allows us to take the data inside a matrix and make a vector out of it now the problem is if we have a matrix a concrete example of a matrix that has more than one row or more than one column then how do we do that because vectors are one dimensional the answer is we can't and because we cannot do that in this situation we check for that more like here it says if the matrix does not have either one row or one column then we throw an error and the error is called runtime error so because at runtime something goes wrong and we say one dimension m or n in the matrix must be one just a side note of course we could assume if we had a matrix that does not fulfill these conditions that has more than one row and more than one column then we could still force it to become a vector by just following the two dimensional entries in the matrix either in a row major or column major way and by this way creating a vector but then the vector will be longer than the matrix is either white or high so I chose to raise an error here I think this is more consistent but you could think of a case but at the end of the day if we are given a matrix of either one row or one column we can create a vector out of it and how do we do that? well very easy we just return a new vector so we call the vector class and all we pass to the vector class is and it's also so nice syntax object orientation is really super beautiful in code so all we do is we pass in a generator expression and this does it creates a new vector and data is a generator expression that loops over self and how can you loop over a matrix? well looping over a matrix means we dispatch to the iter special method and the iter special method knows how to do that and that's just a shortened version here the original example of iter that's not the point here the point is that we look at this method the S vector method and the S vector method creates a new vector out of a matrix and we see again an example of how we reuse code from within one method from another I think once you get used to this level of coding if you look at a code base it's really like an art to read a lot of thinking that goes into designing nice libraries and you can always learn by studying someone else's libraries how do people think so let's create a new vector V and this is of course one dimensional so we can make it a matrix how do we do that? we just call the S matrix method on it and then store that as M in this case it's obviously a 3 by 1 matrix a column matrix or a column vector so the dimensions are 3 by 1 and then when I go ahead and call S matrix here on V I get back a matrix object and then a matrix object comes with a method called S vector so what I can do is I can call S matrix a vector object and immediately call by method chaining S vector again and then I get back a new instance of a vector which has the same numbers in it and then the vector started out with so here I get back to the same vector it's not the same vector it's a new object with the same numbers in it and we can do the same for a matrix however here the example is I have a 2 by 2 matrix and I try to make it a vector to illustrate a point S vector now throws the runtime error because we don't want that to happen and now comes the fun part now comes the part where we teach our vector and matrix classes how to do linear algebra and this is of course this has to do with operator overloading because our initial example in this chapter was I had x times y so I had the multiplication operator and it's through a type error so somehow I have to teach Python that the multiplication works different for you know in the context of matrices and vectors so in other words I have to overload the arithmetic operators and we've seen that many times before just think the plus operator is addition when we work with numbers the plus operator is also concatenation when we work with strings so that's the same thing that we saw many examples of now we do that ourselves so first we do that for the arithmetic operators and before we do that we have to think up how many different cases can there be when we do some arithmetic with either a vector and another vector or with a matrix and another matrix or with the mix, the mixture of them so for example we have to and you will see this in the chapter when you read it but not in the presentation so when I want to only look at addition and subtraction I can do that for vectors with vectors and this only works if both vectors have the same number of components and also if I add two vectors what I mean with the technical term if I have a vector a and a vector b I can calculate a plus b but I can also calculate b plus a so these are the kind of thoughts you should make when you design your library and then we can add or subtract two matrices and then the thing is that's something that I made up but numpy does that too in there can I add or subtract from a matrix or vector a scalar you would say for those of you who know a little bit about linear algebra can you add the number 1 to the vector 1, 2, 3 that doesn't make sense in linear algebra but in numpy the people decided that this actually does make sense so we do that too it's called broadcasting it's a very easy concept numpy and get used to broadcasting you don't want to miss it but here is now in the code I will show you how this works at least in this chapter if you read it you will understand how broadcasting can be done and then there are all the various ways of how to multiply vectors and matrices a vector is called the dot project for example you can multiply a matrix with a vector you can also multiply a vector with a matrix so that multiplying a matrix by a vector from the right and from left is two different things and so on so you really have to think how many ways are there to have two objects of either kind work with each other and these are all the operations that we have to implement and I will show you a subset of that but the final version of matrix and vector classes in our chapter here will actually cover all of that so let's look at an example so what happens is maybe maybe give you an easier example maybe let's do this so how does python work when the operators are executed so I already told you this I think I would have to check this in chapter 3 but it doesn't matter in which chapter it was let's say I write 1 plus 1 let's say 1 plus 2 and evaluate this cell I get back at 3 what happens in memory but what happens in memory is following python creates a new object with a 1 in it of type int and a second object with a 2 in it also type int and then what python does goes to the object of the left hand side and says hey 1 do you know how you can add yourself to the 2 and then the 1 says well I'm an integer the 2 is an integer I know how to add integers and then what happens is they add themselves up the 3 it will become an int and then of course they are forgotten they only exist in memory as an intermediate object so to say and then what happens is what we see in the Trubin notebook is the 3 is given back as the output but then because we don't store it in a variable this gets forgotten as well but the important thing is that's what happens in memory and now let's do a second example let's add the number 1 is the float 2.0 so integer plus float execute that I get back 3.0 no there is a difference but now what happens in memory so what happens is python will create a new 1 of type int and then it will create a somewhat bigger box with a 2.0 need some other structure of bits to model the decimals and then python goes ahead and asks the integer do you know how to add yourself to 2.0 the float and then the integer says well no I don't know the idea of decimals I don't understand what decimals are I'm an integer I don't know that and then it reports back to python and says I can't do the operation I don't know addition and then what python does it goes to the float object and says hey float do you know how you can add yourself to the int and then the float says yeah no problem any integer I know that any integer is just the same number that I have here before the decimal plus a decimal and all 0's the float knows that so what the float does the result should be 3.0 another float and python takes that result and of course the two operands the left and the right operand they go away and then in Jupyter Notebook we get the output back 3.0 and because we don't store it away the output gets lost that's the logic of how of how the BLAST operator works and this is true for another operator that takes two operands as well so I think we showed you the example of the comparison operator in chapter 3 yes comparison operator in chapter 3 you can watch the recording of that you will see the same thing just not a plus but a double equal sign so let's analyze this pattern let's make this maybe a little bit more generic so let's say let's say assume that I have a vector 1 and I add a vector 2 how does that work how does that work so let's assume I created vector 1 and vector 2 before I have not done that but let's assume they exist so what python does is somewhere in memory there is a vector let's say 1, 2, 3 and this is of type vector vector and I call this vector 1 and this is the reference that goes here and then somewhere else in memory I have another vector and just for illustration purposes there will be 4, 5, 6 it can be any numbers, it doesn't matter and it's of type vector and the name is vector 2 and the reference goes here then what python will do in this example it will of course also go to the left hand side first and say hey you, you object I don't know what a vector is I'm python, I'm stupid I don't know what a vector is but do you know how you can calculate and then the vector says well let me look at the other operand and the other operand happens to be also a vector and in this case what would we want to happen is that we actually intuitively think that one vector knows how to add itself to another vector and we will see that this works and so the left operand knows how to do that and so the left operand will go away and let's assume that just for illustration purposes and we will see an example of that later I want to add vector 1 plus 10 the number, the integer 10 the new is the vector 1 object already exists it will create a new object number 10 in it and then or maybe let's do this to get the example right to say 10 plus vector 1 so what python does is it will create a 10 and then the vector 1 is still there and so python will go to the left operand and calculate how to add yourself to a vector and then the incest will have no idea what the vector is and then python says okay that's okay and returns to the right operand the vector now and then says do you know how you add yourself how an inc could be added to you I mean we will just see in just a moment how this can make sense and then the vector in our example would say yes and no the thing to understand is whenever the left operand in this case the 10 knows how to do the addition or the operation then this is just the common addition, subtraction and so on whenever the left operand does not know that what python does is it will ask the right operand to do the so called reversed operation of the same kind so reversed is the important word here we shall see this will be abbreviated with an R and this is how the logic is so this is what I just showed you with all the examples that's the only thing that you really need to understand and now let's see how we can implement that with special methods so let's only look at addition there is of course a thunder add method special method and this method knows how to do addition or that's where we implement the addition logic so what we do is we first have here an if statement that checks so first of all maybe let's begin the beginning so what does add what does thunder add take? thunder add takes two arguments the first one is always itself and the other argument by convention we call it other and other is the thing that is on the right hand side so in other words whenever whenever I do some operation let's say the plus basically I write what you should think of self plus other and then on self the dot add method is called and other is passed in as the argument so self of course here is passed in because it's the instance on which we call it and then other is given to it and then self can do either it knows how to handle the situation or it doesn't and now let's look at the code here the thunder add function so where's the case where the add does not know what to do well it's obviously down here not implemented and what return not implemented means is that means the left hand object says I have no idea what to do that's the situation and before that we have two other situations the first one is that other that is given to the to the self is checked for its type and if both objects have the same type which we check here then in this case we do vector addition and how is a vector how is vector addition done well we create a new vector and then we have a generator expression where we loop over both vectors one at a time and just add the components x plus y for x and y in the zip of self and other so I hope that by now you know what the zip does but at the end of the day we create a new vector where every component is just the sum of the components of the two vectors that we start out with and then there is an elif and the elif says if other is any number abstractly speaking what we do is we create a new vector and we add to every component x in the vector other the constant and this is what broadcasting is and we will see that soon but at the end of the day that's addition so addition basically is asked on the left hand side if we are the left operant then the thunder add is executed and then in thunder add we check what is the type of the right object so is the right object another vector or is it a number and if it's neither of the two what happens is the add method says not implemented and that's the case where self says to python hey I have no idea how addition works and then what python does if let's say if self says I don't know how to do addition what python then does it goes to other and calls on the other object thunder and now here comes the trick r add it as argument self and what is r add reverse addition and where is r add here it's just the next special method so this is what happens when the object that we are looking at is on the right side is the right operant and the left one didn't know how to add it okay and then in this case because vector addition is commutative a vector a plus a vector b I can also say b plus a what I do here is I just return self plus other and this is also dispatching the flow of execution to thunder add and this only works because addition is commutative and let's look at subtraction and subtraction is not commutative because a minus b is something different than b minus a and again thunder sub is the method when I say a minus b then what happens is we call the method a thunder sub and pass it the argument b and in the case where a does not know what to do so whenever this method returns not implemented as we saw then what python does it asks the right operant and the right operant will be asked like this b dot under our sub reverse subtraction and then we pass in the a so either the left operant does the operation or the left or the right one does right so that's it that's the entire logic so it sounds complicated in the beginning and maybe back in chapter 3 you were already wondering why am I telling you stories of we ask the left one can you work with the right one and then we ask the right one this seems very tedious when back in chapter 3 I told you that but now you understand why this has to be the case and the reason why this has to be so is because when we create new types like here the vector and the matrix type we want them to be able to work with the built-in types and the built-in types like int for example or tuple or list or whatever they exist in core python and when core python was developed the python developers did not know about our vector and matrix classes so whenever we write our own classes they have to take over a built-in type will never know how to do some arithmetic with our custom classes it's always our custom classes that have to be taught how to work with the built-ins and this is what we do here this is why we do all the type dispatching here and then you see there is a dandamool that implements multiplication and for multiplication we also see that multiplication is communicative and then in the case of multiplication what we do is we check if other is also a vector and if self is a vector I mean self has to be a vector because we are in the vector class so self is always a vector but let's say other is also a vector in this case then what we do is we multiply the two vectors what we do we take the dot product and the dot product is just the product of each component of both vectors and then we add up and get the sum but again linear algebra I don't assume you know so this would be the dot product case if I multiply a vector by a vector this is the dot product and then of course if I multiply a vector by a scalar by a number this is this here then I do scalar multiplication how does scalar multiplication work well I just create a new vector where every component is just x times other and other is just the scalar the constant and here I use again a generator expression and I loop over self but you know I mean it's just all repeats itself nor a scalar number then I just return not implemented and because multiplication is for vectors at least a communicative we can in this case in the arm special method just return self times other and what does self times other do well self times other just dispatches to the more special method okay that's it so I know that this may be a lot to understand in the recording but at the end of the day before you know I want you to understand the big picture here and the big picture is if you if you have whenever you work with an operator that has a left and a right operand we always ask the left operand to do the operation if the left operand doesn't know how to do it the right one gets asked and asking method call just with the R prefix for reversed because then the two operands are just reversed and in many many cases the R sub or the R add or the R mold dispatches to the non-reversed version but in cases where the two operands where the operation is not communicative then we must not do that we have to do something else okay and then let's define this new vector class so what can we do I create a new vector and now what I can do is I can say 2 times V and remember V used to be a tuple what would happen maybe let's compare this so 2 times V and V is 1, 2, 3 so now I get back 2, 4, 6 this is just scalar multiplication and just to contrast this on the slide further so here I have a vector that is created from a list 1, 2, 3 so let's take the same list 1, 2, 3 and just multiply the list 1, 2, 3 with 2 what does that do well it gives us back a new list 1, 2, 3, comma 1, 2, 3 so this is a list concatenation so the multiplication operator for lists just does concatenation and now what we did effectively Python because our vector also you know internally it's a list so what we just did is we taught Python that the standard behavior for list concatenation is not what we want we want scalar multiplication and now here we taught Python how to do linear algebra we taught Python here how to do scalar multiplication and then of course we can also multiply the scalar from the right it works the same and of course at the end of the day we multiply 2 vectors so in this situation I multiply v with itself v times v and this will of course be the dot product and the dot product is just the sum of you know the 2 that's quickly put the v here so it's just 1, 2 and 3 so the dot product would be just 1 times 1 plus 2 times 2 plus 3 times 3 and this would be 14 then the algebra definitions you should know as a data scientist one day but for now I want you to understand how the object orientated features work in Python so I want you to understand how you can do operator overloading on your own for your custom data types and it's not about the particular mass rules here and then of course we can do vector plus vector it works we can say a vector minus itself a vector this is nice so we know this must be correct and now comes the thing that numpy basically does by default so I have a vector 1, 2, 3 so just to see this is the vector 1, 2, 3 and now I add a constant 100 to the vector and in linear algebra this would be illegal but in numpy it's not and because we want to re-implement numpy on our own what we do is we just add 100 the constant 100 to every component in the vector so now this would be 101, 1, 2, 103 and this in numpy is called broadcasting and yeah so here in the source code in the documentation in the doc string I also speak of broadcasting here so this is inspired by numpy and we can also do 100 minus v for example so this works okay pooh now we say it's getting even bigger but now let's do that real quick so we are in the new matrix class and we also have an add we also have a reversed add we have sub, reverse sub and of course we have to do some more type dispatching because now you know what did we implement here for addition and subtraction well what we did is whenever I add a matrix to a matrix I just do ordinary matrix multiplication however this only works if the dimensions of both matrices match so that's why we have a check here it checks if the n rows of both cells and other and also the n coils of cells and other match the matrix don't match and we cannot do that but if it works then the new matrix will be created where every entry is just the sum of the two entries from both of the matrices and then the second case does the broadcasting again so if we add a scalar to a matrix it also should work then for the reversed operator we dispatch again the same as for sub the most of the logic here has to go into the multiplication because if I have a matrix and I multiply it with another matrix that's one case but I can also multiply a matrix by a vector I can also multiply a vector by a matrix and of course I can multiply a matrix by a scalar or a scalar by a matrix so there are many many cases that we have to cover and that's what we do here in MUL that's the standard multiplication and as you will see inside it I do some tricks so whenever when you multiply a matrix by a vector what is commonly done as I already said is that we view the vector as a matrix too so when we multiply a matrix by a vector from the right we view the vector as a column vector so as a n by 1 matrix and why is that important as I already said well we know how we can change the representation of a vector into a matrix we just call the S-matrix method on the vector and then all we need to do is we have to write one logic that does a matrix-matrix multiplication this is this hidden function so also see the leading single underscore means that this function is our matrix object this will only be used internally so the matrix-multiply method is for example used here matrix-multiply but it's only used internally from within another method and then what we do here is we dispatch all the different cases and then for example if I have another vector what I do is I change the vector I do the matrix-matrix multiplication and then the result of that will be returned as a vector again so in this case if you think all the examples through then what you will see is that you only really need to implement matrix-matrix multiplication once and all the other cases can be reduced to a matrix-matrix multiplication and that also allows us to understand, to reuse code so in this example it pays off to study linear algebra and to be good at linear algebra because the more we understand about this problem domain the nicer we can write the code and that means the more maintainable code is the more reuse within the code will be and so on this will just make the code better so this is now getting very crowded here but at the end of the day all of the code that we put in just implements now all the logic that we will now see with the next example so let's create two matrices M and N and for example M-matrix we can now do scalar multiplication we can here say 2 times M plus M times 3 divided by 5 this of course 5 times M divided by 5 so we should get back M we do we can subtract the matrix from each other so we get a zero matrix we can multiply now two matrices and there is actually a video in the chapter that you can watch to refute a matrix multiplication and then of course if we multiply two matrices then the number of columns of the left matrix has to match the number of rows of the right matrix otherwise we cannot do the matrix matrix multiplication which is why here I get a value the matrices don't have compatible dimensions so even though we are in pragmatic coding we have to respect the mathematical rules here and now we can do matrix times vector and this is basically what we started out in this chapter in the beginning this is what we wanted to achieve so now we have a matrix times a vector and it works and if we have a vector times another matrix it also works but let me see I think the example was supposed to be V times M and then we get a value error because also when we multiply a matrix by a vector or a vector by a matrix then also certain requirements about the dimensions have to be fulfilled and yeah so this is how we implement arithmetic on our own data types so maybe let's write some of the stuff here let's say we have thunder add of course we have thunder sub what else thunder mul I think thunder true div I abbreviated it as just div and we have the same here as well so we see that already the blueprints of the matrix and the vector get very crowded but you know the thing is as before there is always a certain special method that corresponds to you know one syntactical rule in python and for example just as thunder len relates to the len build and function the thunder add relates to the plus operator so for most of the things for most of the syntax in python there is always a special method that we can define to change or adapt the behavior of instances of the certain type within a certain situation in python and again we have seen in the documentation the one the python data model page where we see all the special methods that exist and now that was arithmetic and now we still want to implement some other operators for example the relational operators this is still an example of operator overloading so let's say I have two vectors here v and w both are the vectors 1, 2 and 3 and now I want to check is v equal to w and the answer is false and that doesn't make sense because both vectors have the same components they should be they should evaluate equal but they don't so we have to teach python what it means for two vectors to have the same components or we also have to teach python what it means for two matrices to have the same entries a side note I compare here now v to itself so I ask the question is v equal to v and the answer is true so the comparison operator is double equal operator it seems to work whenever I compare an object to itself and this is because python does the following python first looks at the identity of the left operand and then it looks at the identity of the right operand and if it sees in memory that we are really comparing one object to itself it's the same object the same box then python will just take a shortcut and say that's definitely true it does not ask the object to compare itself to itself ok so let's continue and define a a new version of the vector class and now it has this eq special method here and what does the eq special method do it does equality comparison and all it does here it takes two arguments self and other and we already know that is a vector and other is any other object that is on the right hand side of the equal operator and then what happens is python first checks if the two have the same class or if other is of the same class as me or in other words if other is a vector and then what python does here or what our implementation does we compare other to do the two vectors have the same number of components and if not then we cannot compare them that makes sense that is important here an important detail because I mean we could also say that a vector with five components if I compare it to a vector with six components should return false here because they don't evaluate equal but here we rather raise an arrow because we don't want the user to compare vectors of different lengths with each other and then if we get through this test then we loop over both self the self vector and the other vector in a pairwise fashion and then what we do that goes back to what we learned in chapter five we look at the absolute difference between the two components and check if the difference the absolute value of the difference is greater than the threshold a zero threshold and to do that I have I defined a class variable zero threshold and then I call self dot zero threshold so also there is something other going on but let's finish reading this so what I do here is because we model our vectors and matrices to consist of to consist of only floats because of that we don't compare the components in the vector for exact equality because we know that floating point numbers are not precise which is why we take the absolute value of the difference and compare that against some threshold and then if we find a single pair of components that is over whose difference is over the threshold we have to return fourth why because then once I find one pair it's not within the threshold I know the two vectors are different and so in other words what does this return do the return is within a for loop what that means is the for loop is exceeded early so this is also an example of the early exit pattern here which if we write a return statement within a for loop and this is hit then the for loop also executes also finishes immediately okay and what else is there so as we just realized I want to I have to compare the absolute difference to a certain threshold and what do I do how do I do that well I define a threshold I don't hard code the threshold here this is why I define the threshold to be 12 decimals to the right of the zero and now the thing is this self dot zero threshold does not exist so what I instruct python here to do is to look up the attribute zero threshold on the self object so on the concrete instance here and it doesn't exist why because the threshold is only defined here in the class blueprint right this is what this means to have a class variable up here so zero threshold exists only once for the vector class and it exists a second time for the matrix class as we will see but it does not exist so the number one to the negative 10 to the negative 12 is only stored twice once for the matrix once for the vector it is not stored for every instance but whenever I try to look up a variable on an object that doesn't exist that doesn't exist on the instance but what one automatically does it goes to the class and checks if the attribute exists here and if so it reads it in other words by using a class variable here we have a common default value that is used for all the instances it's just a minor detail so but again here the DunderEQ method either returns through a false or as we see here and now what we can do after implementing this I can do the example again create two vectors v and w and now they compare equal so now I have taught Python what it means for two vectors to have the same components okay now we are slowly getting to the end of the chapter luckily but if you go ahead and read the chapter you can see that we actually skipped some more stuff and now I will show you the whole picture and just to show you what the entire classes the entire vector and matrix classes look like and this will be super long so first we have the vector class and I added some more documentation because now that everything is you know afford to document everything so we have some extensive documentation here the doc string this belongs to the class as a whole and as we see that the methods are also documented separately so that's good style then we have some class variables in the beginning we saw the dummy class variable here on the vector class and just now just before we saw the zero threshold and there are some more class variables and what you use class variables for usually is a default value as a default value that holds true for every instance of a class unless the instance gets its own value for in this case storage typing or threshold okay and then we have the initializer we have the wrapper again for the output that we can copy paste for the text representation that we can copy paste back into a quota cell so that we can create a new vector in this case that has the same components the same value the same semantic value then I have the string the string a dunder string method which gives out the nice representation for me as the human because let's say in the in the chapter when you read it you will see we also there's a section where we create a very big matrix and a very big vector so that you cannot really see all the numbers and then this nice human friendly text representation paste off we have Len get item for indexing we have done the iter for iteration but here yeah and we've done the reversed for reverse looping we have the all the special methods that do the arithmetic add sub subtract more we have some orbit I skipped like that's a negative for positive for the unary plus and the unary negative minus operator we can take the absolute value of a of a vector this is also a function I skipped the norm function and so on and then of course we document everything that's just the vector and also the matrix the final matrix class is getting very long so this is really I mean it's just to illustrate a point this is to illustrate that you can when you write a bigger program for let's say a bigger study or maybe an application as a for business then you will put most of your business know how basically into classes and the classes are the way as to give semantic meaning to individual parts of your bigger program and also to make individual components reusable this is what instances basically are and now that we have seen the big picture let's quickly compare what we have achieved in this chapter with what NumPy actually gives us and note again in practice always use NumPy don't reimplement your own vectors and matrices but you know studying purposes I think vectors and matrices are nice as we saw in this chapter so let's start again with the same example as in the beginning we create a vector Y as a tuple and we create a matrix X as a list of lists and then we faced in the beginning the problem that I cannot multiply the matrix by the vector I get a type error the goal of this chapter was to fix each Python some math and of course if we use NumPy it already knows some math so how can we what can we create so called NumPy arrays which we cover in chapter 9 how can we create NumPy arrays out of the tuple and the list of lists well we just use the array constructor np.array to create a new array and how do we create vectors and matrices in our version from the tuple and the list of lists well in the very same way so I just call vector Y and matrix X so I pass in what the Y the tuple and the list of lists to the vector and the matrix class and then the initializer runs and verifies that the data is good and then we create a new vector and a new matrix object so now I have a Y array and an X array this is the NumPy what NumPy gives us and I have YVAC and XMAT which are the vectors and matrices that we build our own in this chapter and now let's look at the Y array this is just an array a one-dimensional thing an array is just an n-dimensional array as we saw in chapter 9 it's just a more generic version of a matrix so to say a n-dimensional matrix in a way if we look at our own Y vector the text representation which one is nicer I actually think that our version is nicer than NumPy's but the NumPy version of course is the defective standard so yeah and then of course the same for the matrix this is the way how NumPy shows us that there are two dimensions so NumPy when used in triple a notebook have this nice formatted output here and then I can of course look at our own matrix how does our own matrix look like well it does not look so nice maybe we only have this one-dimensional thing and we only see that our matrix is two-dimensional because we have several parentheses inside it but at the end of the day the text representations are not the decisive thing if you want to evaluate what to use so NumPy arrays come with a dot shape attribute which is the dimensions of the matrix or the vector so if I call Y array dot shape then I get back a tuple with only one element in it and that means that Y in this case consists of three components how do we do that for the matrix in NumPy we also call dot shape so in other words in NumPy both a vector a one-dimensional thing a two-dimensional thing are modeled with the same data type namely the array but then if I call dot shape on the matrix I get a tuple of two elements because the matrix has two dimensions that's why and the first dimension is the rows so we have three rows in the matrix and the second dimension is the columns and here we have three columns as well our own version of the matrix so here the usage is different but we can also create a tuple if we want note how we don't have an attribute on our vector because we don't really need an attribute on our own vector because we can always use the built-in length function on our vector and also get the number saying that let's use the built-in length function now call it with NumPy array the NumPy array Y gives us back three because there are three components in the vector if you call it for our own implementation of a vector we also get back a three so in this case our own implementation works just as NumPy and now we just do that for the matrix now if I call the length built-in on the X array what should I get back? what should I get back? I have a three by three matrix so nine entries in the matrix but I get back three, why? because for a two-dimensional array, NumPy the length of an array defaults to the number of rows so three here means three rows but if I call len for our own matrix we get back nine so it really depends on what we want I think our implementation is a bit more intuitive because the length of a matrix should be the number of entries in it can we call transport? yes NumPy also has a dot transport method so this is the transposition of the matrix we can of course also use our own transport method we get the same result, first row is always 147 so in that way we are similar and now let's do matrix vector multiplication, how do we do that in NumPy? I don't like that here we have to use the dot method on the x-array and then give as the first argument the y-array this is now the matrix vector multiplication what happens if I write vector what if I write x-array times y-array I funny enough get back a three by three thing that's weird multiplying in three by three matrix by a vector of size 3 I should get back a vector of three components so what happens here is really broadcasting so broadcasting in NumPy also works not only for scalars but also if I so the vector gets broadcasted up to the matrix and then the two matrices get multiplied on a per element basis so from a linear algebra point of view this operation does not make any sense however if I multiply our own version of matrix with vector we get back a vector so in this regard our own version of matrix vector may be a little bit nicer to look at however in newer versions of python there is a new operator namely the add and the add does also the dot product so the upper row is NumPy's way of saying matrix times vector and the star operator, the multiplication operator does something else in NumPy and we defined our own operator to do matrix vector multiplication according to the rules of linear algebra and if we wanted to load the add operator here we would just overload not the mul special method but the mat mul special method because the add operator here is called the matrix multiplication operator is quite new in python but not so many people use it it's kind of weird to look at but I think it's a good operator still and we can of course do scalar multiplication so 10 times the vector gives me a new vector and 1230 so that works for NumPy we have seen that I can do scalar multiplication for our own version of vectors as well and then now comes the weird part I can add NumPy's vector with our vector and why does that work why can I add two data types that definitely don't know about each other so NumPy knows about many data types but it definitely does not know about our vector data type that we just wrote here in this chapter and in our own vector we did not look at NumPy so how come how come that python magically knows how to add two things that don't know about each other's type how this works is because as we learned in python it's only the behavior of an object that matters and NumPy the NumPy developers, they were so smart that they in their own implementation of their ThunderAt special method at a very last resort what they do is they check if they can loop over the other object and of course if the other object is finite and of the same length we saw that the length built in gives back three for NumPy's array here and also three for our own vector and then what NumPy smartly does in the background is it simply loops over our vector and only because we implemented our own vectors to be a sequence to behave like a sequence by being iterable this is why this works and this is a very beautiful example of why A, we have to only think of objects in python in terms of their behavior, it doesn't matter what type something is, we only care about the behavior and it also pays off to always implement the standard behaviors that python offers most notably the sequence behavior whenever we build a new or whenever we create a new data type in our own custom application or whenever we teach python some new semantics about the world we should always think of making this class a sequence how do we do that but as we saw all we need to do is to include a DunderLen and a DunderGetItem method that's it and then python can figure out the looping alternatively we can implement the DunderItem method but it doesn't matter if we if we make you know create design our own classes to be a sequence then they interact seamlessly with loads and lots and lots of other data types that they've never heard about just because the behavior matters okay so let's see that what I'm telling you actually works I can add the two vectors even though they are totally different data types can I multiply them this check also works interesting so who is responsible for the multiplication here is of course also the NumPyArray and the NumPyArray by default does multiplication on an per element wise in a per element wise fashion this is why the vector 1, 2, 3 and both y array and both vectors 1, 2, 3 this is why they are they are multiplied component wise this is why we see 1, 4, 9 so mathematically this cell doesn't make any sense but it works and it does make sense in the context of NumPy because NumPy really doesn't model linear algebra first it models something else first namely the array idea can I add the two matrices well let's think before I execute this why could I add the two vectors in the first cell well because NumPy tried to loop over our vector and it could now guess what the x array also tries to loop over our x mat however we have seen by using the built-in len function that when I call len with x array I get back 3 because the only thing len gives back is the number of rows in something when I call len on our x mat however I get 4.9 for the number of entries in the matrix and because of that, because the sizes don't match NumPy does not know how to deal with that that's why I get a value over here okay so this includes this was the last slide in this presentation so I know that there is some more stuff to be discovered when you read this chapter and let me repeat a little bit what we learned today so this chapter put everything together and by everything I mean everything all the behaviors all the memory diagrams that I through and where I explain to you how something works in Python all of this behavior can be customized by knowing which of the special methods we have to implement and then we can make Python work with our objects in a very seamless way in other words we can always build custom data types that can fit seamlessly into the standard Python syntax but what this chapter is actually mainly about is is about giving semantics to code so we started with the example of multiplying a tuple times a list of lists and Python returned or raised a type error and now at the end of the day our own vector and our own matrix class they are nothing but a tuple and a list of lists that learned how to multiply each other so we taught Python linear algebra here and whatever the domain is that you model you can teach Python about whatever your problem is that you are modeling in the same way so here is just linear algebra that we used as an example but any application that gets a little bit bigger you would use the same ideas to organize your code so this chapter is really about giving semantics to code but this chapter is also about more it's also about reusing code so why do we reuse code well you know we see that classes are nothing but blueprints that contain all the behaviors all the functionalities that data types or the objects that are of this data type can have so we can have as many vectors as we want and they all behave in the same way this could be also a concrete instance of an int and this could be the int blueprint that's Python built-in built-ins but now we have figured out a way of how we can do the same thing for custom classes for custom blueprints there's a lot to do with reuse of code because every time we create a new vector object all we store in the concrete instances is as we can see we only store data in it and usually the same goes that data and behavior usually goes to an object basically organizing the data and the behavior that go with the data and what we mean by organizing the object-oriented paradigm means we put all the behavior in one box or in one group of boxes and all the data in other group of boxes so here we have data on the left and we have the behavior on the right and it's all perfectly separated and this is what you should aim at in coding and yeah, that's what we learned and I hope that you also learned a little bit of math and if you get bored after this course continue with Gilbert Strang's linear algebra course and he also I showed you in the beginning his original course the basic linear algebra course but he also has a new course that was published last year and in this new course he teaches the fundamentals the fundamentals of linear algebra when in the context of data science so this is also an interesting course you can really learn a lot from Gilbert and here again this chapter I think puts together everything from this course it's kind of like the final chapter that after which if you get this you get everything but I think also the important idea is now you should go back and study this course from the beginning on because now you know where everything you read about in the first nine chapters will lead to understanding chapter 10 and then starting to go over the book again from chapter 1 to and so on basically helps you to see the details then when you read the book the second time and you know what is the big idea where we are aiming at that will enable you to really understand the details and get good at it and you should repeat stuff anyways I'm pretty sure you know one can be good at this coding because the density of the materials in this course is quite high but again keep the big idea in mind go back do it again and I think the second time around many many things will make a lot more sense many of the things that you didn't get the first time around and then after you go back and have redone chapters 1 through 10 and refued you know really you're really good at the foundations of python then you will go on in the chapters after the 10 where we look at individual details and applications of python okay so this is the basic course in python I hope to see you soon again then for an addition for an add-on of this course which will probably be chapter 11 and also for other courses maybe see you soon