Hey, everybody! Today we're going to explore object-oriented programming (OOP) and what that looks like in Python. Before we get started let's warm up with some strings. I have a file opened here in Visual Studio Code where we can explore. Recall that we can set a variable equal to a string. For example, I could set greeting equal to the string "Hello world!" and then if I wanted to, I could print the value stored in greeting by writing print and in parentheses greeting. Let's run that and see what it does. Notice it prints "Hello world!" In the terminal. Have you ever tried printing the type of a string? Let's try that. We'll write print and then inside the parentheses we'll call type and inside its parentheses we'll put greeting. When we run this, notice gets printed. It turns out a string is a class, which is one of the concepts we're exploring today. Classes basically are blueprints (or plans) which indicate how something should be made. Objects are the elements which get made based on those plans. In OOP terminology, the process of creating that object is called instantiation. On the line where we set greeting equal to the string "Hello world!," we created an instance of the string class called greeting. In OOP terminology, greeting is an object. Classes define properties and methods which can be used by the objects created from them. Methods are really just another word for functions, and properties are really just variables. You may recall there are many string methods, and you even may have used the count method already. Let's set a variable cnt equal to what gets returned by the count method when it's called for the greeting string and sent the string "l" as a parameter. To do this you would set cnt equal to greeting.count followed by the string "l" in parentheses. Notice that to access an object method, we write the name of the object followed by a dot and then the name of the method and parentheses which contain any arguments the method needs. Let's print what gets stored in cnt by adding a print statement, and run this code. Notice it prints 3, because we sent "l" to count, which counts how many times its argument appears in the greeting string. There are three "l"s in "Hello world!", so it returns 3. Now we don't actually know how count does its job. We just know what it does. And really that's all we need to know, in addition to the arguments it requires. In OOP terminology, this is referred to as abstraction. In fact, we really could write our own counting function. We would begin our function definition with the keyword def followed by the name we wish to call our function, perhaps ourCount, the parameters it takes in parentheses and a colon. Because it's counting the characters in a string, it will need two parameters, which we'll call phrase and letter. In the body of the function, let's begin by just checking if we can identify when the character we're counting is in the string. We can write a for loop to traverse each letter in the phrase one by one by writing for c in phrase:. Then in the body of the for loop we could check if the current letter we're considering at this point in the loop is equal to the letter we're counting by writing if c == letter:. If it is, we could print "found one!" with a print statement. Let's run this to see if it works. Of course, it didn't. Can anyone tell me why? Of course! We need to call it. Ah! It printed "found one!" three times, which is just what we would hope to see. Now let's alter our definition just a bit, so we keep a running sum, which we return at the end of the function. Above the for loop we'll need to initialize our running sum, which we might store in a variable called ourCnt, to zero. Then we can replace the print statement with a statement that adds 1 to ourCnt, and add return ourCnt after the for loop is done. Running this, we see it also prints 3. My main purpose in showing that we could write this function ourselves is to further illustrate the notion of abstraction. Again we don't need to know how count does its job. We just need to know what to send it and what it will return. Thank goodness, we don't have to write all those string methods ourselves, right? The beauty of abstraction goes even further, though, because using classes and objects allows us to write code which is even more modular, making it easier to read, share, debug, and maintain. In the early days of programming, we didn't have if statements and loops. We only had goto statements. When you saw a goto statement, you knew were jumping somewhere else, but it wasn't inherently clear why. We also didn't have functions, so if we needed to do the same sequence of steps again, we had to write them again. Code was much less readability and was even referred to as spaghetti code. Thankfully, we have if statements, loops, and functions now. Object oriented programming advances the benefits of modularity to an even higher level. Let's consider one more aspect of objects before we take a break. As you may have guessed strings are not the only objects in Python. You can import the math object with the import math statement, which will allow you to call methods like square root. For example, you could set x equal to math.sqrt(64), and then print the value of x. This will print 8.0. In addition to methods, the math object has properties like pi, which you access in a similar fashion, by writing the name of the object followed by a dot and then the name of the property. So for pi you would write math.pi. If you use a print statement to print math.pi, 3.141592653589793 gets printed. So for the math object, you can call one of its methods, and you can reference one of its properties. Let's pause there, so you have an opportunity to try Knowledge Check #1, and identify key object oriented programming concepts.