[MUSIC PLAYING]
DAVID J. MALAN: All right.
This is CS50'S Introduction to Programming with Python.
My name is David Malan.
And over these past many weeks have we focused
on functions and variables early on, then conditionals, and loops,
and exceptions, a bit of libraries, unit test file
layout, regular expressions, object-oriented programming,
and really, et cetera.
And indeed, that's where we focus today, is
on all the more that you can do with Python and programming
more generally beyond some of those fundamental concepts as well.
In fact, if you start to flip through the documentation for Python
and all of its form, all of which is as always accessible at docs.python.org,
you'll see additional documentation on Python's own tutorial and library,
its reference, its how-to.
And among all of those various documents as well as others more online,
you'll see that there's some tidbits that we didn't quite touch on.
And indeed, even though we themed these past several weeks
of around fairly broad topics that are rather essential for doing
typical types of problems in Python, it turns out
there's quite a number of other features as well, that we didn't necessarily
touch on, that didn't necessarily fit within any
of those overarching concepts, or might have
been a little too much too soon if we did them too early on in the course.
And so, in today, our final lecture, well, we
focus really on all the more that you can do with Python
and hopefully whet your appetite for teaching yourself all the more to.
For instance, among Python's various data types,
there's this other one that we haven't had occasion to yet use, namely, a set.
In mathematics, a set is typically a collection of values
wherein there are no duplicates.
So it's not quite a list.
It's a bit more special than that in that somehow any duplicates are
eliminated for you.
Well, it turns out within Python, this is an actual data type
that you yourself can use in your code.
And via the documentation here, might you
be able to glean that it's a useful problem
if you want to somehow automatically filter out duplicates.
So let me go ahead and go over to VS Code here.
And let me go ahead and show you a file that I created a bit of in advance,
whereby we have a file here called houses.py.
And in houses.py, I already went ahead and whipped up
a big list of students inside of which is
a number of dictionaries, each of which represents a student's name
and house respectively.
Now, this is a pretty sizable dictionary.
And so, it lends itself to iteration over the same.
And suppose that the goal here was quite simply to figure out,
well, what are the unique houses at Hogwarts in the world of Harry Potter?
It would be nice, perhaps, to not have to know these kinds of details
or look them up online.
Here we have a set of students, albeit not exhaustive, with all of the houses.
But among these students here, what are the unique houses in which they live?
Well, I could certainly, as a human, just eyeball this
and tell you that it's, well, Gryffindor, Slytherin, and Ravenclaw.
But how can we go about doing it programmatically for these students
as well?
Well, let's take one approach first here.
Let me go into houses.py.
And let me propose that we first how about create an empty list
called houses in which I'm going to accumulate each of the houses uniquely.
So every time I iterate through this list of dictionaries,
I'm only going to add a house to this list if I haven't seen it before.
So how do I express that?
Well, let me iterate over all of the students with for student in students,
as we've done in the past.
And let me ask you a question now.
So if the current student's house--
and notice that I'm indexing into the current student
because I know they are a dictionary or dict object,
and if that student's house is not in my house's list,
then, indented, am I going to say houses.append,
because again, houses is a list.
And I'm going to append that particular house to the list.
Then at the very bottom here, let me go ahead
and do something somewhat interesting here and say, for each of the houses
that I've accumulated in, I could just say houses.
But if I just say houses, what was the point of accumulating them all at once?
I could just do this whole thing in a loop.
Let's at least go about and sort those houses
with sorted, which is going to the strings alphabetically.
And let's go ahead therein and print each of the houses.
Let me go ahead now in my terminal window
and run Python of houses.py and hit Enter.
And there we have it.
Gryffindor, Ravenclaw, Slytherin in alphabetical order,
even though in the list of dictionaries up here,
technically the order in which we saw these was Gryffindor, Gryffindor,
Gryffindor, Slytherin, Ravenclaw.
So indeed, my code seems to have sorted them properly.
So this is perfectly fine.
And it's one way of solving this problem.
But it turns out we could use more that's built into the language Python
to solve this problem ourself.
Here I'm rather reinventing a wheel, really the notion of a set
wherein duplicates are eliminated for me.
So let me go ahead and clear my terminal window
and perhaps change the type of object I'm using here.
Instead of a list, which could also be written
like this to create an empty list, let me go ahead
and create an empty set, whereby I call a function called
set that's going to return to me some object in Python
that represents this notion of a set wherein duplicates are automatically
eliminated.
And now, I can tighten up my code.
Because I don't have to use this if condition myself.
I think I can just do something like this.
Inside of my loop, let me do houses.add.
So it's not append for a set, it's append for a list.
But it's add to a set per the documentation.
Then let me go ahead and add this current student's house.
And now, I think the rest of my code can be the same.
I'm just now trusting per the documentation for set in Python
that it's going to filter out duplicates for me.
And I can just blindly add, add, add, add all of these houses to the set
and any duplicates already there will be gone.
Python of houses.py and Enter.
And voila, we're back in business with just those three there as well.
Let me pause here to see if there's any questions now on this use of set, which
is just another data type that's available to you,
another class in the world of Python that you can reach for when
solving some problem like this.
STUDENT: How can we locate an item in a set,
for example, find Gryffindor in that set?
DAVID J. MALAN: How do you find an item in a set?
You can use very similar syntax as we've done for a list before.
You can use syntax like if Gryffindor in houses then,
and you can answer a question along those lines.
So you can use in and not in and similar functions as well.
Other questions on set?
STUDENT: Look what happens if you have a similar house name?
Let's say instead of Slytherin, it is maybe
an O instead of an I. Will the for loop loop
throughout each of those letters in the house name?
DAVID J. MALAN: It would compare the strings.
So if Slytherin appears more than once but is
slightly misspelled or capitalized, if I heard you right,
those would appear to be distinct strings.
So you would get both versions of Slytherin in the result.
However, we've seen in the past how we can clean up users' data
if indeed it might be messy.
We could force everything to uppercase, or everything to lowercase,
or we could use capitalize the function built into strs,
or title case that would handle some of the cleanup for us.
In this case, because the data is not coming from humans using the input
function, I wrote the code in advance, it's safer
to assume that I got the houses right.
But that's absolutely a risk if it's coming from users.
Allow me to turn our attention back to some of the other features
here that we can leverage in Python if we dig further into the documentation
and read up more on its features.
Well, in some language, there's this notion
of global variables, whereby you can define a variable that's either
local to a function, as we've seen many times,
or if you put a variable outside of all of your functions,
perhaps near the top of your file, that would generally
be considered a global variable.
Or in the world of Python, it might be specific to the module.
But for all intents and purposes, it's going to behave for a given program
as though it is global.
However, it turns out that if you do this
when solving some problem down the line, whereby you have multiple functions
and you do have one or more variables that are outside of those functions,
you might not be able to change those variables as easily as you might think.
So indeed, let me go back to VS Code here.
And in just a moment, I'm going to go ahead and create a new file, how about
called bank.py.
Let's go ahead and implement the notion of a bank
wherein we can store things like money in various forms.
And let me go ahead and do this.
Let me go ahead and implement a very simple bank
that simply keeps track of my total balance, the number of dollars or cents
or whatever I might be storing in this bank.
And I'm going to give myself a variable called balance at the top, which
is an integer, a set to zero.
Now let me go ahead and define a main function as we often do.
And inside of my main function, let me go ahead and print out, quote unquote,
balance, and then print out the value of balance itself.
Passing to print, as we've often done, more than one argument so that they get
separated by a single white space.
And now, since I have a main function, really setting the stage
for doing more interesting things soon, let
me go ahead and do our usual if the name of this file
equals equals underscore underscore main, then go ahead and call main.
So this is a terribly short program, but it's perhaps
representative of how you might solve some future problem in Python.
Whereby you have a main function that's going to eventually do
some interesting stuff.
And at the top of your file, you have one or more variables
that are just useful to keep there because then you know where they are.
And perhaps not just main but other functions can access them as well.
So let's see.
When I run this program, Python of bank.py,
I would hope based on my own intuition thus far that I'm going
to see that my current balance is zero.
That is to say, even though the balance variable is defined on line one,
hopefully I can still print it online five inside of main,
even though balance was not defined in my main function.
Here we go.
Hitting Enter.
And voila, balance zero.
So it does seem to work.
Even if you declare a variable in Python outside of your functions,
it appears that you can access it.
You can read the value of that variable even inside of a function like main.
Well, let's get a little more adventurous now.
Because this program really isn't solving anyone's problems.
Let's go ahead and implement more of a bank,
like the ability to deposit money into the bank
and to withdraw money from the bank.
Thereby giving me some more functions that might very well
need to access that same variable.
Let me clear my terminal window here.
And let me go ahead and pretend for the moment
that I have the ability to deposit, say, $100 or 100 coins,
whatever the unit of currency is here.
And then, maybe I want to withdraw straight
away 50 of those same dollars or coins.
And now, let me go ahead and just print out at the bottom of main
what my new balance should be so that in an ideal world,
once I have deposited 100 then withdrawn 50, after starting at 0,
I'd like to think that my new balance on line eight should indeed be 50.
All right.
But I haven't implemented these functions yet.
So let's do that as we've done in the past.
Down here, I'm going to go ahead and define another function deposit.
I'm going to say that it takes an argument called n for a number of coins
or dollars or the like.
And I'm just going to do this.
I'm going to go ahead and say, balance plus equals n,
thereby changing the value of n.
I could do it more verbosely, balance equals balance plus n.
But I'm going to use the shorter hand notation here instead.
And now, let's implement withdraw.
So define a function called withdraw.
It too is going to take a variable-- an argument
n for number of dollars or coins.
And now, I'm going to go ahead and subtract from balance
using minus equals n as well.
And I'm still going to call main if the name of this file is main.
So what have I done?
I've just added not just one but three functions total, all of which
apparently need to access balance by printing it, incrementing it,
or decrementing it, as we've seen here.
All right.
Let me go ahead and focus on these three functions here.
Let me go back to my terminal window and run Python of bank.py and hit Enter.
And wow.
Seems like we've introduced some number of problems here.
And what are these problems?
Well, unbound local error is perhaps the first time we've seen this one here.
Local variable balance referenced before assignment.
And that's a bit misleading, definitely confusing.
Because I absolutely assigned balance of value on the top of my code.
And indeed, if I scroll back up, nothing has changed or been lost up there.
It's definitely been assigned to value.
And now on line 12, it would seem, that when deposit is called
I'm just trying to access that variable again.
So intuitively, what might explain this error message, unbound local error?
What is Python telling us there that Python can or can't
do when it comes to these so-called global variables that
are at the top of my file?
STUDENT: So if you want to change this variable,
you should write an inside left function main.
And the global variable unchangeable.
DAVID J. MALAN: Yeah.
STUDENT: [INAUDIBLE]
DAVID J. MALAN: So if you want to change the value,
it might need to be local to the function.
If you are trying to change a global variable though in a function,
it clearly does not work.
So it's OK to read a global variable.
Read meaning access it and print it and so forth.
But apparently, you can't write to a global variable in the same way
from within one of these functions.
All right.
Well, maybe the fix is to do this.
Let me clear my terminal window and that error.
And maybe I could just do this.
Let's get rid of the global variable.
And let's go ahead and put it, for instance, inside of main.
Might this now work?
Well, let me try this now.
Python of Bank.py Enter.
That alone did not solve it.
I still have an unbound local error.
This time though, it's for a different reason.
It turns out now that balance on line two is by definition a local variable.
A local variable is one that exists in the context of a function, at least
in this case.
A global variable is the opposite, one that does not,
for instance at the top of my file.
So here is another distinction in Python.
If you declare a variable in a function, like main,
just as I've done on line two with balance,
it is indeed local to that function.
Deposit and withdraw do not have access to that same variable.
Why?
Because it's local to main.
And so, you would think now we're kind of stuck in this vicious cycle.
Well, maybe the solution then is to move balance globally
so all three functions can access it.
But clearly, where we began, as Elena noted, we can't therefore change it.
So it turns out the solution to this problem in Python
is ironically exactly this keyword here.
It's a little different as you might have seen if you've
programmed before in other languages.
But there's indeed a keyword in Python called
global that allows you to tell a function that, hey, this
is not a variable that's local to you.
I mean it to be a global variable that I want you to edit.
So if I go back to VS Code here, clearing my terminal
window to get rid of that error.
Let me go ahead and undo the change I just made
and put balance back at the top of my file.
But this time, what I'm going to do is I'm
going to inform my two functions that need to change the value of balance,
that it is indeed global, by typing global balance again here as well as
here.
Global balance.
I still leave the same lines of code now on lines 13 and 18, that increment
and decrement balance.
But this now use of keyword global is a little bit of a clue to Python that,
oh, OK.
It's not a local variable.
This is not a bug that you've introduced.
You mean for me to edit this variable up above.
So now, let me go ahead in my terminal window and run Python of bank.py.
I'm hoping to see that my balance is zero plus 100 minus 50 is 50.
And indeed, it now is.
It starts off at zero per my first print statement on line five.
But it ends up at 50 total at below that on line eight.
Let me pause here to see if now there's any questions
on these global or local variables.
STUDENT: What happens when you declare a variable globally, and as
in the same variable globally and in a function?
DAVID J. MALAN: A good question.
You're always thinking about the so-called corner cases.
So if you declare a variable both globally, like at the top of your file,
and then an identically named variable inside of a function, same name,
the latter will shadow, so to speak, the former.
That is, you'll be able to use the latter, that is the local variable.
But it will have no effect on the global variable.
Temporarily, Python will only know that the local variable exists.
So in general, the rule of thumb is, just don't do that.
Not only might it create bugs in your code
because you don't quite change what you intend to change.
It's also perhaps non-obvious to other readers as well.
Other questions on globals or locals?
STUDENT: OK, what if we decide to add balance
as an argument inside the main function?
DAVID J. MALAN: Yeah, another good instinct.
But in this case, that also is not going to solve the problem.
Because if you pass in a variable like balance to each of the functions
and then change it within that function, it's
only going to be changing in effect a local copy thereof.
It's not going to be changing what's outside of those functions.
So I think we actually need a better way altogether.
And in fact, allow me to transition to perhaps a modification
of this same program.
Recall that we looked most recently at this notion
of object-oriented programming.
Whereby you can model real world entities, for instance a bank,
and you can model and encapsulate information
about that real world entity, for instance,
like someone's account balance.
So let me propose that we actually do this.
Let me start from scratch with bank.py.
Get rid of the global variable altogether.
And actually use some object-oriented code.
Let me define a class called account to represent someone's bank account.
And then, let me go ahead and initialize with my init method, which
again, takes by convention at least one argument called self.
Let me go ahead and initialize every person's bank account
to some value like zero.
Now, how can I do that?
Well, I'm going to go ahead and do self.balance equals zero.
Thereby giving me an instance variable called
balance initialized for this account to zero.
But I'm going to proactively remember how we also
introduced this notion of properties which might otherwise
collide with the names of my instance variables.
So just by convention I'm going to do this.
I'm going to rename this instance variable proactively
to underscore balance to effectively indicate that it's private, even
though that's not enforced by Python.
It's just a visual clue to myself that this
is something that really I should not-- or other code should not touch,
just functions in this class.
Now, let me go ahead and do this.
Let me go ahead and define an actual function
called balance that really is going to be a property whose purpose in life
is just to return self.balance.
And I'm going to go explicitly and say this is indeed
a property of this class.
Now, let me go ahead and re-implement those other two functions,
deposit and withdraw, but in the confines of this class.
So I'm going to say, define deposit.
It's going to take in an argument self as always,
but an additional one n, a number of dollars or coins to deposit.
And how do I now manipulate this?
Well, I'm going to do self._balance plus equals n.
And now down here, I'm going to do def withdraw self n, just like for deposit.
But here, I'm going to do self.balance minus equals n.
And now, if I go down below this class, I'm
going to go ahead and define myself a main function
just so I can try this now out.
I'm going to go ahead and create an account object by calling the account
constructor, that is the name of the class with two parentheses
if I'm not passing in any arguments to init.
I'm going to go ahead now and print out as before the balance of my account.
But to do that, I'm going to access the property of that account like this.
And I'm going to go ahead now and say, deposit another $100 or coins
with deposit 100.
And I'm going to go ahead, like before, and also now immediately withdraw
for whatever reason 50 of the same.
And now, I'm going to print one last time
balance followed by account.balance, again, accessing that property.
And for this whole thing to work, of course, I need one of these
if name equals equals underscore main, then go ahead and call main.
Now, before I run this, you'll see that it rather escalated quickly.
I had a very simple goal at hand to implement the notion of a bank.
And I was able to implement that perfectly fine ultimately
by declaring balance to be global but then to tell each of my functions
that it is indeed global.
But that's not really the best form of encapsulation we have at our disposal
now.
Per our focus on object-oriented programming,
if we're trying to implement some real world entity like an account at a bank,
that's what classes allow us to do.
And it allows us to solve that same problem perhaps a little
more cleanly, certainly if we're going to accumulate more and more functions
or methods over time.
So if I didn't make any mistakes here, if I run Python of bank.py and hit
Enter now, you'll see that it just works just fine.
Because in the world of classes in Python,
these so-called instance variables are by definition
accessible to all of the methods in that class
because we're accessing them all by way of that special parameter self.
So which way to do it?
For a reasonably small script wherein you are simply
trying to implement a script that has some global information,
like an account balance that you then need to manipulate elsewhere,
the global keyword is a solution to that problem.
But generally speaking, in many languages,
Python to some extent among them, using global variables
tends to be frowned upon only because things can get messy quickly.
And it can become less obvious quickly exactly where your information is
stored, if some of it's up here, some of it's in your function.
So generally, the rule of thumb is to use global variables sparingly.
Though technically speaking, in Python these global variables
are technically local to our module if we were indeed implementing
a library and not just a program.
So in short, try to use global variables sparingly.
But when you do, there is a solution to these same problems.
Questions now on globals or our reimplementation of the same idea
but using full-fledged object-oriented programming?
STUDENT: I just would like to ask, what this property does?
DAVID J. MALAN: What this property does.
So if I go back to VS Code here, you'll see
that this was a technique we looked at in our lecture
on object-oriented programming.
Whereby a property is a instance variable that somehow protected.
It allows me to control it can be read and written.
So in this case, I only have what's called generally a setter.
And or sorry.
In this case, I only have what's generally called a getter.
And there's no mention of the word getter here.
This is just @property means.
That function balance will allow me, recall,
to use syntax like this, where I can pretend
as though balance is indeed with no underscore an instance variable.
But I can now prevent code like mine in main from trying to change balance.
Because I do not have a setter, I would not
be able to do something like account balance equals 1,000
to just give myself 1,000 or coins because I have not defined a setter.
So again, per our focus on object-oriented programming,
these properties just allow me some finer-grained control.
Some languages allow you to define variables that are, so to speak,
constant.
That is, once you have set a value to them,
you cannot change the value of that variable.
And that tends to be a good thing because it
allows you to program defensively.
Just in case you accidentally, or someone else,
accidentally tries to modify the value of that variable,
if you have declared it in some language as a constant, it cannot be changed,
or usually cannot be changed without great effort.
Unfortunately, in Python, we're again on the sort of honor system here.
Where we have conventions to indicate that something
should be treated as though it's constant.
But that's not actually enforced by the language.
So for instance, let me go back here to VS Code.
And let me create a new file, for instance, called meows.py.
And let's see if we can't implement the notion of a cat meowing on the screen.
So I'll do code of meows.py.
And in meows.py, let me go ahead for instance
and implement a very simple program that just has a cat meowing three times.
So how about this.
For i in the range of three, go ahead and print out, quote unquote, meow.
All right.
Well, we've seen in the past how we can clean this up a little bit.
For instance, if I'm not actually using i,
I might as well Pythonically just change the name
of that variable to underscore even though that has no functional
effect here.
But here we have this three randomly hardcoded, that is,
typed explicitly into my code.
And it's totally not a big deal when your code is only two lines.
But imagine that this is a much bigger program with dozens
or even hundreds of lines.
And imagine that one of those lines just has a three in there somewhere.
You're never going to find that three very easily.
And it's going to be very easily overlooked
by you or colleagues or others that you've
hardcoded some magic value like a three right there in your code.
So it tends to be best practice, not just in Python but other languages
as well, any time you have what is essentially a constant,
like a number three that shouldn't ever change,
is to at least let it bubble up, surface it to the top of your code,
so that it's just obvious what your code's constant values are.
And so, by that I mean this.
At the top of this file, it would probably
be a little clearer to colleagues, and frankly, me tomorrow
after I've forgotten what I did today, to define a variable like meows
and set it equal to three.
And then, instead of hardcoding three here or even lower
in a much bigger program, let me just go ahead and pass in that variable's value
to my loop.
So that now it's just kind of obvious to me
that meows is apparently the number of times to meow.
And if I ever want to change it, the only code I have to change
is at the very top of my file.
I don't need to go fishing around or figure out what's going to break,
what do I need to change.
I just know that I can change these constants up at the top.
The problem though with Python is that Python doesn't actually
make variables constant.
It's indeed a convention in Python and some other languages
to at least capitalize your variables when you want to indicate to the world
that you should not touch this.
It is constant.
But there is literally nothing in my code preventing me from saying,
you know what?
Today I feel like four meows instead.
That would work.
In other languages though there's typically
a keyword or some other mechanism syntactically that
would allow you to prevent line three currently from executing.
So that when you try to run your code, you would actually get an error message
explicitly saying, you cannot do that.
So Python, again, is a bit more on the honor system
when it comes to these conventions instead.
Now, it turns out there's other types of constants, quote unquote,
that Python typically manifests.
And in fact, let me go ahead and change this around a little bit.
Let me delete this version of meows.
And let me introduce, again, a class from our discussion
of object-oriented programming, like a class representing
a cat, another real-world entity.
Recall that within classes, you can have not just
instance variables but class variables.
That is variables inside of the class that aren't inside of self, per se,
but they're accessible to all of the methods inside of that class.
Here too, there's a convention but not enforced
by Python of having class constants, whereby inside of the class,
you might want to have a variable that should, should, should not be changed.
But you just want to indicate that visually by capitalizing its name.
So for instance, if the default number of meows for a cat
is meant to be three, I can literally inside of my class
but outside of any of my defined methods just
create a class variable all capitalized with that same value.
And then, if I want to create a method, like meow,
for instance, which as an instance method might take in self as we know.
And then, I might have my loop here for underscore in the range of--
and now I need to access this.
The convention would be to say cat.meows to make clear
that I want the meows variable that's associated with the class called cat.
Then I'm going to go ahead and print out one of these meows.
And now, at the bottom of my code, outside of the class, let me go ahead
and do something like this.
Let me instantiate a cat using the cat constructor.
Notice this is important.
Per our discussion of OOP, the class is capitalized by convention.
But the variable over here is lowercase.
And I could call it just C or anything else.
But I kind of like the symmetry of calling it little cat here and big cat,
so to speak, over here.
And now, if I want this particular cat to meow
that default number of three times, I can just do cat.meow like this.
And that method meow is going to, per line five, access that class constant.
But again, it's constant only in the fact--
only in the sense that you should not touch that, not that it's actually
going to be enforced by the language.
Let me go ahead then and run this with Python of meows.py.
And there it is, three of our meows, meow, meow.
It turns out that Python is a dynamically typed language.
That is to say, it's not strongly typed.
Whereby when you want an int, you have to tell the program
that you are using an int.
You don't have to tell the program that you are using a str, or a float,
or a set, or anything else.
Generally speaking, to date, you and I, when we're creating variables,
we just give a variable a name.
We frequently assign it using in the equal sign some other value.
And honestly, Python just kind of dynamically
figures out what type of variable it is.
If it's, quote unquote Hello, world, the variable is going to be a str.
If it's 50, the integer the variable is going to be an int.
Now, in other languages, including C, and C++, and Java, and others,
it's sometimes necessary for the programmer to specify what types
of variables you want something to be.
The upside of that is that it helps you detect bugs more readily.
Because if you intend for a variable to store a string or an integer,
but you accidentally store an integer or a string, the opposite, or something
else altogether, your language can detect that kind of mistake for you.
When you go, for instance, to run the program,
it can say, no, you've made a mistake.
And you can fix that before your actual users detect as much.
In Python too here, it's again, more of a friendly environment
where you can provide hints to Python itself
as to what type a variable should be.
But the language itself does not strongly enforce these.
Rather, you can use a tool that will tell you whether or not
you're using a variable correctly.
But it's typically a tool you would run as the programmer
before you actually release your code to the world.
Or if you have some kind of automated process, you can run this kind of tool
just like you could reformat or link to your code with some other program
before you actually release it to the world.
So how might we go about using these so-called type hints?
Well, they're documented in the usual place in Python's own documentation.
And it turns out there's a program that's
pretty popular for checking whether or not your code is
adhering to your own type hints.
And that program here is called mypy.
And it's just one of several.
But this one is particularly popular and can be easily installed
in the usual way with pip install mypy.
And its own documentation is at this URL here.
But we'll use it quite simply to check whether or not our variables are indeed
using the right types.
So how can we go about doing this?
All right, let me go back here to VS Code, clear my terminal window,
and in fact erase meows.py as it currently was.
And let's implement a different version of meows that quite simply
has a function called meow that does the actual meowing on the screen.
And then, I'm just going to go ahead and call that function down
toward the bottom.
I'm not going to bother with a main function just for simplicity
so that we can focus as always only on what's new.
So here we are defining a function called meow.
It's going to take a number of times to meow, for instance n for number.
And inside of this function, I'm going to do
my usual for underscore in the range of n go ahead and print, quote unquote,
meow.
So based on our earlier code, I think this is correct.
I've not bothered defining the variable as i.
I'm instead using the underscore because I'm not using it anywhere.
But I think I now have a working function whose purpose in life
is to meow zero, or one, or two, or three, or more times.
Well, let's use this function, again, not bothering with main.
I'm just going to keep my function at the very top because there's only one.
And I'm going to write my code here on line six.
So I'm going to give myself--
I'm going to ask the user for a number.
And I'm going to go ahead and prompt them
in the usual way for that number of times to meow.
And now, I'm going to go ahead and call meow on that number.
Now, some of you might see what I've already done wrong.
But perhaps I myself don't.
So let me go into my terminal window and run Python of meows.py,
the goal being to prompt me.
This seems to be working.
I'm going to type in three.
And I would expect now the meow function to print out meow three times.
Enter.
But no.
There's some kind of type error here. str object cannot be interpreted
as an integer.
Why might that be?
STUDENT: Because the input function returns a string instead of an integer.
DAVID J. MALAN: Exactly.
The input function returns a string or a str, not an int.
So in the past, of course, our solution to this problem
has just been to convert the string to an int by using the int function.
But now, let me start programming more defensively
so that honestly, I don't even find myself in this situation at all.
Let me go ahead and do this.
Let me add what's called a type hint to my function that
explicitly specifies for meow what type of variable should be passed in.
I'm going to go ahead now and change the very first line of my code
and my function to specify that n colon should be an int
and this is a type hint, the fact that I've
added a colon, a space, and the word int is not creating
another int or anything like that.
It's just a hint, an annotation, so to speak to Python,
that this variable on the left called n should be an int.
Now, unfortunately, Python itself doesn't care.
Because again, these type hints are not enforced by the language.
And that's by design.
The language itself and the community prefers
that Python be dynamically typed, not so strongly typed
as to require these things to be true.
But if I run meows.py, type in three again, the same error is there.
But let me go about trying this mypy program, an example of a program that
understands type hints.
And if I run it proactively myself can find bugs like this in my code
before I, or worse, a user, actually runs and encounters something cryptic
like this type error here.
Let me clear my terminal window and this time run mypy space meows.py.
So I'm going to run mypy on my program, but I'm not running Python itself.
When I hit Enter, we'll see this.
All right.
We see now that mypy found apparently an error on line seven.
Error, argument one to meow has incompatible type str expected int.
So it's still an error message.
But mypy is not a program that my users would use.
This is a program that you and I as programmers would use.
And because we have run this code now before we, for instance, released
this program to the world, I can now see even before the code is called or run,
oh, I seem to be using my argument to meow wrong.
I had better fix this somehow.
Well, I can actually go about in hints adding
type hints even to my own variables here so as to catch this another way too.
If I know on line six that I'm creating already a variable called number,
and I know already that I'm assigning equal to the return value of input,
I could give mypy and tools like it another hint and say, you know what?
This variable called number should also be an int.
That is to say, if I now start getting into the habit of annotating
all of my variables and arguments to functions,
maybe mypy can actually help me find things
quite quickly as well before I get to the point of running Python itself.
Let's go ahead and try this again.
Mypy of meows.py and hit Enter.
And this time, notice that mypy actually found
the mistake a little more quickly.
Notice this time it found on line six that, error, incompatible types
and assignment expression has type str, variable has type int.
So before I even got to the point of calling meow,
line six, via this type hint, when used and analyzed by mypy
has helped me find, oh, wait a minute.
I shouldn't be assigning the return value of input
to my variable called number in the first place.
Why?
Mypy has just pointed out to me that one returns a str.
I'm expecting an int.
Let me fix this now instead.
So let me clear my terminal window.
And now, let me do what most of you were probably
thinking I should have done in the first place after all of these weeks.
But now, let me go ahead and convert the return value of input to an integer.
For today's purposes, I'm not going to try
to catch any exceptions or the like.
We're just going to assume that the user types this in properly.
And now, let me go ahead and run mypy of meows.py,
having not only added to type hints to my argument, to my function,
to my variable down here on line six.
And I've also now fixed the problem itself.
Let me go ahead and run mypy.
And success, no issues found in one source file.
Now, it's more reasonable for me to go and run something
like Python of meows and just trust that when I type in three, at least
I'm not going to get a type error.
That is, I didn't mess up as a programmer with respect
to the types of my variables.
Why?
Because when I wrote the code in the first place,
I provided these annotations, these hints
that inform tools like mypy that my intention had better
line up with what the actual code does.
Let me pause here and see if there's now any questions on type hints or mypy.
STUDENT: Is it common or how common is it for those to be used?
Or is it just that it's more used in more complex code
where it's more difficult to ensure that you're actually
using the correct type in the way that you're using the variables?
DAVID J. MALAN: It's a good question.
And it's rather a matter of opinion.
Python was designed to be a little more versatile and flexible when
it comes to some of these details, partly for writeability, to make
it easier and faster to write code, partly for performance
so that the program like Python doesn't have
to bother checking these kinds of details,
we can just get right into the code.
The reality, though, is that strong type checks
do tend to be a good thing for the correctness of your code.
Why?
Because programs like mypy can find, before your code is even run,
if there's already known to be an error.
And it tends to be good for defensive programming.
So the situation essentially is that within the Python ecosystem,
you can annotate your types in this way.
You can use tools to use those type hints.
But to date, Python itself does not enforce
or expect to enforce these conventions.
In larger code bases, in professional code bases,
commercial code bases, probably depending on the project manager
or depending on the engineering team they may very well
want themselves to be using type hints.
Why?
If it just decreases the probability of bugs.
In fact, let me propose now that I-- imagine a situation where
instead of expecting that meow prints meow, meow, meow some number of times,
suppose that I accidentally assume that the meow function
just returns meow some number of times.
We saw, for instance when focusing on unit tests
that it tends to be a good thing to have functions that return values,
be it an int or a string, rather than just having some side effect
like printing things out themselves.
So perhaps I'm still in that mindset.
And I've just assumed mistakenly for the moment
that meow returns a value, like meow, or meow meow, or meow meow meow,
a big string of some number of meows, rather than just printing it itself,
as it clearly does at the moment on line three.
And therefore, suppose that I accidentally did something like this.
Rather than just getting the number and passing it to meow, suppose I did this.
Suppose I declared a number of-- a new variable called meows,
the type of which I think should be str.
And suppose, again, I assume accidentally
that meow returns to me a string of those meows so that I myself
can then print them later.
This would be a little more conducive, arguably, to testing my meow function.
Why?
Because I could expect that it's returning meow, or meow meow,
or meow meow meow, separated by new lines,
returning a str that I could then assert equals
what I expect it to be in something like a unit test.
I'm not going to bother writing any unit tests now.
But let's just suppose that's the mindset I'm now in.
And so, on line seven I'm assuming that I
want to assign the return value of meow to a new variable called meows which
I've annotated with this type hint as being a str,
just so we can see another variable.
This one is not an int but a str instead.
Well, let me go ahead and run this code now, Python of meows.py,
Enter, typing in three.
And you'll see a curious bug.
Meow meow meow none.
Well, why is that?
Well, it turns out at the moment, my meow function only has a side effect.
It just prints out meow some number of times.
It doesn't explicitly return a value as it
would if there were literally the return keyword there.
By default then, when a function in Python
does not explicitly return a value, its implicit return value
is in effect none.
And so, what we're seeing here is this-- on line eight,
because I'm assigning the return value of meow, which is none,
to my meows variable, line three is what's still printing meow meow meow.
And line eight is what's now incorrectly printing none.
Because I accidentally thought that meow returns a value, but it doesn't.
So its return value is effectively none.
So I'm printing very weirdly the word none at the bottom.
So how could I go about catching this kind of mistake too?
I might make this mistake.
But maybe with less frequency if I'm in the habit
of annotating my code with this new feature called type hints.
What you can do here is this.
Let me clear my terminal window to get rid of that artifact.
And up here, let me additionally specify with some funny looking syntax
that my meow function actually by design returns none.
So you literally use this arrow notation.
In Python when hinting what the return value of a function is,
you would do this.
After the parentheses, a space, a hyphen, a greater than symbol,
like an arrow, and then another space, and then the type of the return value.
For now, it's indeed going to--
[SWALLOWS] excuse me, return none.
But now, at least I can catch it like this.
If I now run not Python but mypy on my code, which would be a habit
I'm now getting into if using type hints.
Check that I'm using all of my types correctly before I even run my program.
We'll see that now mypy has found online seven that meow, quote unquote,
does not return a value.
And mypy knows that because I have proactively annotated my meow function
as having none as its return value.
So now, mypy can detect that.
I should now realize, oh, wait a minute.
I'm being foolish here.
Meow clearly does not return a value.
I should not be treating it like it does on line seven.
Let me go about actually fixing this now.
So how do I go about fixing this?
Well, let's practice what we preached in our focus on unit tests,
having a function like meow not have side effects like printing itself.
But let's have it return the actual string.
And I can actually do this kind of cleanly.
Let me clear my error message in my terminal window here.
Let me get rid of the loop here.
Let me say this time that OK, fine, meow is going to return
a value, an actual str or string.
So I've changed none to str.
And now, I can implement this in any number of ways,
maybe even using a loop.
But recall that we have this syntax in Python, which will, I think,
solve this problem for us.
If I want to return a string of n meows, what I can actually do,
recall, is this.
Return quote unquote meow, backslash n, times that number n.
So it's kind of a clever one-liner, avoids
the need for a for loop or something more involved than that,
to just say, multiply meow backslash n against itself three times, or n times,
in this case, in general, so that I get back a big string of zero meows, one,
two, three, or many more meows instead.
I think now my code on line six is actually correct.
Now I've changed meow to behave the way I was
pretending to assume it always worked.
So I'm storing in meows plural a variable that's of type str.
Because now, meow does have a return value of type str itself
per this type hint as well.
All right.
Let me go ahead now and print meows.
But because each of my meows comes with a trailing new line, the backslash n,
I'm going to proactively fix what would be a minor aesthetic bug.
And I'm just going to avoid outputting an extra new line
at the end of those three.
So if I run Python of meows.py now, type in three, there's my meow meow meow.
And now, no mention of none.
Questions now on type hints, and these annotations in mypy,
and using them to defensively write code that just decreases hopefully
the probability of your own bugs?
STUDENT: Is the return three set have double quotes that
have meow slash n, why the program don't take it as a [? string? ?]
DAVID J. MALAN: Why does the program not take it as a-- as strange?
STUDENT: [INAUDIBLE], yeah.
DAVID J. MALAN: So recall that early on in the class
we looked at plus as a concatenation operator that
allows you to join a string on the left and the right.
Multiplication is also an overloaded operator for strings.
Whereby if you have a string on the left and an int on the right,
it will multiply the string, so to speak, by concatenating or joining
that many meows all together.
So this is a feature of object-oriented programming
and an operator overloading as we saw it in the past.
Other questions on type hints or mypy.
STUDENT: Can we not typecast this data type of this variable number?
DAVID J. MALAN: No.
You still-- and let me correct the terminology.
It wouldn't be called typecasting in this context because it's not like C
or C++ where there's an equivalence between these types.
You're technically converting on line five a str to an int.
You do still have to do this.
Because mypy, for instance, would yell at you
if you were trying to assign a str on the right to an int on the left.
You must still use the int function.
Int itself is still a function.
It's not a type hint.
But the word int is being used in another way now in these type hints.
So this int is still a function call as it always has been.
This syntax on the left is another use of the keyword int
but in the form of these type hints.
So you still have to do the conversion yourself.
All right.
Let me propose that we transition to another feature of Python that's worth
knowing, especially since it's one that you'll see in the wild
when you see code or libraries that other folks have written,
namely something known as a doc string or document strings.
It turns out in the world of Python there
is a standardized way per another pep, Python Enhancement Proposal, this one
257, that essentially standardizes how you should document your functions
among other aspects of your code.
And so, for instance, let me go back to my meows.py file here.
And let me propose that we now start documenting this code
too so that I know what the meow function does.
And in fact, the standard way of doing this using doc string notation
would be as follows.
To comment this function, not above it, as you
might be in the habit of doing with code in general, but actually inside of it.
But instead of commenting it like this with the usual hash comment sign,
like meow n times, it turns out that when you're formally docking--
when you're formally documenting a function like meow in this case,
you don't use regular inline comments, so to speak.
You use this syntax instead.
You use triple quotation marks, either double or single.
Then you write out your comment, meow n times.
And then you write the same again at the end.
So either three double quotes at the start and the end
or three single quotes at the start and the end.
And Python has built into it certain tools
and certain assumptions that if it detects that there is a comment using
this doc string format triple quotes on the left and the right,
it will assume that that's indeed the documentation for that function.
And it turns out in the Python ecosystem, there's a lot of tools
that you can then use to analyze your code automatically, extract
all of these document strings for you, and even generate
web pages or PDFs of documentation for your own function.
So there's these conventions via which if you adhere to them
you can start documenting your code as for other people
by generating automatically the documentation from your own code
without writing something up from scratch manually.
Now, it turns out if your function does take arguments
and perhaps does a bit more, there are multiple conventions for how
you can document for the human programmers that
might be using your function, whether it's you, or a colleague,
or someone else on the internet, to actually use these docs
strings to standardize the information therein.
So you might see this instead.
Using these same triple quotes above and below now,
you might see your one-sentence explanation of the function, meows--
meow n times.
Sometimes depending on the style and use,
it might actually still be on the first line but with a blank line below it.
But I'll keep everything uniformly indented.
And this is a convention used by some popular Python documentation tools
as well.
You would say syntax like this-- param n colon, and then a description of what n
is, number of times to meow.
Then colon type n colon int, which just indicates that the type of n
is an integer.
Then, if this function could actually raise an exception,
you can document that too.
And actually, it's not really--
well, it's arguably my mistake here.
If n comes in as an argument and is not, in fact, an int,
maybe it's a float, or a string, or something else,
the multiplication sign here is not going to work.
It's not going to multiply the string.
It's going to trigger what I know from experience to be a type error.
So I'm going to go ahead and proactively say in my own documentation
that this function, technically, if you use it wrong could raise a type error,
even though I'm hinting up here with this annotation
that you should pass in an int.
Again, Python doesn't enforce that.
So if you pass in a float, this might, in fact,
raise this function a type error.
And so, that might happen if n is not an int.
And then, lastly, I might say for clarity's sake for other programmers,
this function returns a string of n meows, one per line.
And the return type of that value, r type, is going to be str.
Now, all of this syntax here as I've used it is not Python per se.
This is a convention known as restructured text, which
is a form of markdown-like language that's
used for documentation, for websites, for blogs, and even more.
But it's one of the popular conventions within the world of Python
to document your own functions.
So this does not have anything to do fundamentally with type hints.
Type hints are a feature of Python.
What I'm doing here is just adhering to a third party convention
of putting in between a Python doc string from the start
to the end a certain standard format so that these third party tools can
analyze my code for me top to bottom, left to right,
and ideally generate documentation for me.
It can generate a PDF, a web page, or something else,
so that I or my colleagues don't need to not just only write
code but also manually create documentation for our code.
We can keep everything together and use tools to generate the same for us.
Any questions now on these doc strings, which
again are a convention of documenting your own code often
following some standard syntax?
STUDENT: Yeah.
So when you say you would document it and put it in a PDF,
is the purpose of doing this to publish it and share your function
so other users can use it?
DAVID J. MALAN: Absolutely.
In the past, when we have installed some third party libraries, for instance,
cowsay a few weeks back.
Recall that I showed you what functions it had.
But if you read the documentation, you might actually
see that it was documented for us by the author of that program.
Now, I don't believe they were using this particular syntax.
But it was definitely useful for you and me
to be able to read some web page or PDF telling us how to use the library
rather than wasting time reading through someone else's code
and trying to infer what functions exist and how to use them.
It just tends to be much more developer-friendly to have
proper documentation for our own code or libraries as well.
Other questions?
STUDENT: Yeah.
When with doc strings, when it's used to generate a PDF or whatever,
does it include any of the code?
So if you're referencing in your comment,
if you're referencing the code in the comment itself and
might not make sense without seeing the code.
Does it-- do these include it?
DAVID J. MALAN: Short answer, you can do that.
Not in the convention I'm using here.
But there's actually a clever way to write in your doc strings
sample inputs to your functions and sample outputs for your functions.
And if you use a different tool that we've not discussed,
that tool will run your code using those sample inputs.
It will check that your outputs match your sample outputs.
And if not, the program will yell at you saying, you've got a bug somewhere.
So this is just another way where you can
use doc strings to not only document but even catch errors in your code.
This has been a lot.
And there's a bit more to go.
Why don't we go ahead here and take a five-minute break.
And when we resume, we'll take a look at yet another feature of Python, yet
another library to write code faster.
All right.
Suppose we want to modify this meows program to actually take its input not
from the input function in the blinking prompt but from the command line.
Recall in our discussion of libraries, that you could use something
like sys.argv to get at command line arguments
that a human has provided when you're running your program.
So why don't we whip up a version of meow
that uses command line arguments instead of, again, input.
So I'm going to go ahead and delete what we've done here thus far.
And let me propose that we import sys as we've done in the past.
And let's do this.
How about if the user does not type any command line arguments.
Then my program will just meow once, just so that it
does something visually interesting.
Otherwise, let's also give the user an option
to specify how many times I want the cat to meow.
So let's start simple.
Let's first of all go ahead and do this.
If the length of sys.argv equals equals one,
that is the user only typed the name of the program and nothing else after
the--
in their command, then let's go ahead and just print out one meow like this.
Else for now, let's go ahead and print out something like this.
Else go ahead and print out, let's say, usage
for the program, which will be usage of meows.py,
just so that the user knows that the program itself is called meows.py.
Now let me go down to my terminal window and start to type Python of meows.py.
And at this point, notice that the length of sys.argv
should indeed be one.
Why?
Well, Python the name doesn't end up in sys.argv at all ever.
But meows.py, the name of the file does.
And it's going to go in sys.argv zero.
But that's only one element.
So the length of this thing is one.
There's nothing more to the right.
So when I hit Enter now we should see, indeed, one meow.
If I don't cooperate, suppose I do something like meows three Enter.
Then I'm going to see a reminder that this is how you use the program.
And this is a common convention to literally print out
the word usage, a colon, then the name of the program,
and maybe some explanation of how to use it.
So I'm keeping it very simple.
But let's be a little fancier.
What if I really wanted the user to type in maybe not three, but something
more sophisticated.
And in fact, when controlling programs from the command line,
it's very common to provide what are often called switches or flags,
whereby you pass in something like dash n, which semantically means
this number of times, then often a space,
and then something like the number three.
This still allows me to do other things at the command line if I want.
But the fact that I've standardized on how I'm providing command line
arguments to this program with dash n three
is just a more reliable way now of my program
knowing what does the three mean.
It's a little less obvious if I just do meows.py space three.
Well, what does the three mean?
At least with syntax like dash n three, especially
if you've read the documentation for this program,
ultimately, oh, dash n means number of times.
Got it.
It's a way of passing in two additional arguments
but that have some relationship between them.
So how do I modify my program to understand dash n three?
Well, if I'm using sys like this, I could do this.
elif the length of sys.argv equals this time three.
Because notice, there's one, two, three things at my prompt.
So sys.argv is zero, one, and two, three things total separated by spaces.
If it equals three--
and let's be safe, and sys.argv bracket one equals equals dash n,
then let's go ahead and do this.
Let's go ahead and convert sys.argv of two to an integer
and assign it to a variable, for instance, called n.
And then, let's go ahead and do this.
For underscore in the range of n, let's go ahead
and print out some of these meows.
Now, there's still an opportunity maybe to consolidate my print lines
with meow.
But for now, I'm going to keep these ideas separate.
So I'm going to handle the default case with no arguments up here as before.
And now, more interestingly, I'm going to do this to be clear.
I'm going to check if the user gave me three command line arguments, the name
of the program, dash n, and a number.
If indeed the second thing they gave me in sys.argv of 1 equals equals dash n,
then I'm going to assume that the next thing, sys.argv of two
is going to be an integer.
And I'll convert it to such and store it in this variable n.
And now, just using a loop, I'm going to print out meow that many times.
All right, so it's kind of a combination of our earlier focus on loops,
our earlier focus on command line arguments,
just creating a program that allow me to claim
is representative of how a lot of command line programs work,
even though we've typically not used many like this.
But it's very common to configure a program, one,
you're about to run it at the command line with something
like these command line arguments like dash n or dash something else.
Now, I'm going to go ahead and hit Enter.
And I think I should see, indeed, three meows.
By contrast, if I do two at the end, I should see two meows.
If I do one, I should see one meow.
And frankly, if I just omit this altogether,
I should see one meow as well, because that was my default case earlier.
And now, let me allow us to assume that this program eventually
gets more complicated.
Let's imagine a world where I don't want to support just dash n.
Maybe I want to support dash a, and dash b, and dash c,
and dash d, and a whole lot of others.
Or heck, at that point, I should maybe give them words.
So maybe it's a dash, dash, number.
It's indeed a convention in computing typically
to use single dashes with a single letter, like n, but use double dashes
if you're actually using a whole word like number.
So the command line argument might be dash n, or maybe
it's dash, dash number.
But you can imagine just how complicated the code gets
if now you want to support dash n, dash a, dash b, dash c, and so forth.
You're going to have to be checking all over the place.
And what if they come in a different order?
You're going to have to check is dash n first,
or is it second, or is it third, or is it's fourth?
I mean, this just becomes very painful very quickly
just to do something relatively simple like allow the user to pass command
line arguments into your program.
Well, this is why, as always, there exist libraries.
And another library that comes with Python
that's probably worth knowing something about is this one here called argparse.
In fact, with a lot of the tools I myself or CS50's team writes in Python,
we very frequently use argparse whenever they
are more complicated than a lot of our class demos and a little more similar
to this one where we want to allow the user to pass in configuration options
at the command line.
And by supporting things like dash n, or dash a, or dash b, or dash c,
argparse is a library that per its documentation
just handles all of this parsing so to speak, this analysis of command line
arguments for you automatically so you can focus
on writing the interesting parts of your program, not the command line
arguments part.
So how might we use this?
Well, let me go back to VS Code here.
Let me clear my terminal window.
And let me propose that I rewrite this using not sys
but actually using argparse.
And I'm going to start a little simple and then build back up.
So let me throw all of this away for now and instead import argparse.
Argparse stands for argument parser.
To parse something means to read it, kind of pick it apart to analyze it.
So this is indeed going to do just that for me.
Now, let me go ahead and do this.
And for this library, it's helpful to know
a little object-oriented programming like we all now do.
I'm going to create a variable called parser.
Though I could call it anything I want.
I'm going to set it equal to the return value of argparse.ArgumentParser,
with a capital A and a capital P. A constructor
for a class called argument parser that comes with Python itself
within this library here.
Now, I'm going to configure this argument parser
to know about the specific command line arguments
that I myself want to support in my program.
So I'm going to do this, parser.add_argument.
So that's apparently a method in the parser object.
I'm going to add an argument of dash n.
Easy enough.
Now I'm going to go ahead and actually parse the command line arguments.
I'm going to do args, or I could call the variable anything
I want, parser.parseargs.
And by default, parseargs is going to automatically look at sys.argv for me.
I don't need to import sys myself.
I can leave the argument parser, its code to import sys, look at sys.argv,
and figure out where dash n or anything else actually is.
And what's nice now, because this line of code
here results in the parser having parsed all of the command line arguments,
I now have this object in this variable called args inside
of which are all of the values of those command
line arguments, no matter what order they appeared in.
Not such a big deal when I've only got one because it's only
going to go in one place at the end.
But if I've got dash n, dash a, dash b, dash c,
you could imagine them being in all different orders.
They definitely don't have to be alphabetical.
The user should be able to type them in any order they want.
That's better for usability.
Arg parser is going to figure all of that out for me.
And all I have to do now is this.
If I want to iterate over that many numbers of arguments
and that many meows rather, I can do this.
For underscore in the range of the int conversion of args.n.
So dot is the syntax we kept using to access things
like properties inside of an object.
And that's what args is.
It's the object returned by the parse args function for me.
I'm going to go ahead now and print out, quote unquote, meow this many times.
So it's not super simple.
These are three new lines of code I need to write and rather understand.
But it's already a little simpler and more compact
than my if, and my elif, and my ors, and my ands, and all of that Boolean logic.
It's handling a lot of this for me.
So if I didn't make any mistakes, let me run Python now of meows.py Enter.
And I did make a mistake here.
I did make a mistake.
What's wrong here now?
What's wrong?
Well, I definitely didn't run it the way I intend.
So dash n three Enter.
So it does work.
But if I don't cooperate, this actually seems to be a worse version.
If I don't pass in dash n and a number, it just errors with a type error.
Int must be a string.
None is what came back.
So there's clearly an error here.
But the library is more flexible.
I can actually provide some documentation on how to use this thing.
So how do I know how to use this?
Well, typically it's conventional in Python
and in a lot of programming environments to run a program
with a special argument, dash h or dash dash help.
And almost always I will claim you'll then
see some kind of usage information.
And indeed, that's what I'm looking at now.
I just ran Python of meows.py space dash h.
I'll do it again.
Let me clear my screen and this time do dash dash help in English, Enter.
And I see the same thing.
It's not very useful at the moment.
It just shows me what the usage is up here.
And this is kind of interesting.
This is a standard syntax in computing.
And we've kind of seen it in Python's documentation before.
This just means that the program's name is, of course, meows.py.
Square brackets as almost always in documentation means it's optional.
So I don't have to type dash h, but I can.
I don't have to type dash n and another value, but I can.
And then, down here is some explanation of these options,
and more verbosely showing me that I can also
do dash dash help and not just dash h.
But this is so generic.
This has nothing to do with my program.
This is not going to help my users when I actually
release this software for the world.
So let me go ahead and improve it.
Let me add a description to my argument parser that the humans will see.
Meow like a cat, quote unquote, is going to be the value of this named
parameter called description.
And let me also add a help parameter to my dash n
argument that just explains what dash n means, number of times to meow,
quote unquote.
I'm not going to change anything else.
But I am going to go back to my terminal window and run Python of meow.
I'm going to run Python of meows.py dash h, or equivalently dash dash help.
And now notice that this is a little more user friendly.
If I scroll up, we still see the same usage.
But there's a quick sentence in English of explanation
that this program meows like a cat.
And if I look at the options now, oh, that's what n means.
It's the number of times to meow.
And this capital N, a mental variable, if you will,
is just indicating to me that I need to type a number by convention
after the lower case dash n.
So it would be nice though, all that said,
if my program still didn't just break when I run it without any command line
arguments.
Ideally, my program would handle this just like my manual version
did when I used sys.argv myself.
So we just need to add a little more functionality to this library.
And if I read the documentation, I'll see that add argument
takes yet another named argument.
If you want, you can specify a default value for dash n, for instance, one.
And I'll do that there.
And you can further specify that it's got to be an int.
And what this will additionally allow me to do
is if I tell arg parser to make sure that the value of dash n is an int,
I don't need to do the conversion manually.
I can just trust down on line seven that when
I access the property called n inside of my args object,
it's going to be automatically an int for me.
And again, this is the value of a library.
Let it do all of the work for you so you can get back
to focusing on the interesting project at hand,
whatever problem it is you're trying to solve.
In this case, granted, not that interesting, but meowing like a cat.
Let me go ahead now and run Python of meows.py and hit Enter.
This time, no arguments.
And now it meows.
Why?
Because I specified that if I don't, as a user, specify dash n,
it's going to have a default value of one apparently.
And I don't have to convert that value from a str to an int
because I told arg parser please just make this an int for me.
Any questions now on argparse, or really this principle
of just outsourcing the commodity stuff, the stuff that everyone's program
eventually needs to do so that you can focus on the juicy part yourself.
STUDENT: What does args.n contain?
DAVID J. MALAN: What does args.n contain?
It contains the integer that the human typed after a space after dash n.
Good question.
Other questions?
STUDENT: Yeah.
When you specify the type for the argument, what
happens if-- does that basically handle the exception if the user inputs
a string in this case?
DAVID J. MALAN: A really good question.
Suppose that the human does not type a number and therefore not an in.
Well, let's see what happens.
So Python of meows.py dash n dog, where dog is obviously not a number.
Enter.
And voila, we see an automatically generated error message.
A little cryptic, admittedly.
But I'm seeing a reminder of what the usage is
and a minor explanation of what is invalid about this.
And again, this is what allows you, this is
what allows me to focus on writing the actual code we care about
and just letting the library automate some of this stuff for us.
All right.
Well, allow me to propose now that we take a look at one other feature
of Python that we've seen before.
But it turns out we can use it even more powerfully as our programs become
more sophisticated and the problems we're trying to solve themselves become
more involved.
Let me go ahead and turn to VS Code, closing out meows.py,
and creating a new file for instance, called unpack.py.
So code of unpack.py.
And let me just remind us what we mean by unpacking.
Because this is actually a feature of Python that we've seen before.
For instance, suppose that I write a program that
prompts the user for their name, like David space Malan.
Wouldn't it be nice if we could split the user's name
into two separate variables?
And when we've done this in the past, we've done it in a few different ways.
But one of them involved unpacking a single value that comes back from that,
like a list or some other data structure,
and putting it immediately into two variables.
So let's do this here.
Let me go ahead and call the input function,
asking someone what's your name, question mark.
Then, let me go ahead and just split a little naively on a single space.
So I'm assuming that the only users at the moment
are people like me, David space Malan.
No middle names, no multiple names.
It's just one and two, which itself could be buggy for other users.
But for now, I'm keeping it simple, just to remind us
that I can now unpack that return value with something like first
underscore last equals the return value of input.
And now I can go ahead and do something like this,
like printing out with an f string, Hello, comma, and then in curly braces,
first.
If I just want to greet myself or any other user as, Hello, David,
without the last name.
And frankly, if I'm not using the last name,
recall that a Python convention is just to name it underscore to make clear
that you know you're not using that value.
But it does need to be there because you're unpacking two values at once.
So if I run this, it won't be all that unfamiliar.
I'm just going to run now Python of unpack.py.
I'll type in David Malan, which has a single space.
And there we have it, Hello, comma, David.
Well, it turns out that there's other ways to unpack values.
And there's other features that Python offers, especially
when it comes to defining and using functions.
And this is slightly more intermediate functionality,
if you will, that's useful, because you can
start to write even more elegant and powerful code once you
get comfortable with syntax like this.
So let me go ahead and propose that we not just play with,
Hello names anymore, but instead do something maybe
involving some coinage again.
So maybe not dollars and cents, but maybe,
again, as in the past, some galleons, and sickles,
and knuts which among which there's a mathematical relationship as
to how many of those in the wizarding world equal each other.
And let me go ahead and do this.
Let me define a simple function called total that just tells me
the total value of someone's vault in Gringotts, the wizarding bank, based
on how many galleons, sickles, and knuts that they have,
which again, are currencies from the wizarding world as opposed
to our actual human world.
So this total function might take a variable like galleons and sickles
and knuts like this.
And then, it's going to return the formula, which I admittedly
had to look up myself.
And it turns out that the formula for converting galleons and sickles
to knuts would be this.
Galleons times 17 plus sickles, then times all of that by 29 and then
add in the individual knuts.
Not sure in what detail this came up in the books or movies.
But here we have it, the official formula.
All right.
Now let's go ahead and do this.
Let me go ahead and call the total function with just some sample inputs.
Suppose that someone like Harry has 100 galleons, 50 sickles, and 25 knuts.
Let me go ahead and print that out on the screen.
Well, if total returns an integer, which I think this arithmetic expression
will do, let me go ahead and store, rather, pass the return value of total
to print.
And then just for clarity, let me write knuts at the end So?
I know that the unit of measure here is indeed knuts in total.
Now, let me go ahead in my terminal window
and run Python of unpack.py and hit Enter.
And it turns out mathematically that if I got my math correct,
100 galleons plus 50 sickles plus 25 knuts equals in total 50,775 knuts.
Just avoiding having to use our own human currency here.
But I'm not doing anything along the lines of unpacking at least just yet.
Let me propose now that I do this.
Just for the sake of discussion, let me propose
that I leave the total function as is.
But let me go ahead and just store all of my coins in a list.
So coins in order from left to right, 100, 50, 25, it
just because for whatever purposes in this story
I have all of my coinage in a list in this order.
Kind of a purse or wallet of sorts.
Well, how can I pass this in?
Well, I'm not going to hard-code the same values twice.
Just for the sake of discussion, how could I
pass in the individual elements of a list to my total function?
Well, of course, I could treat this list as I always
do using numeric indices by doing coins bracket zero,
coins bracket one, coins bracket two.
So this is old-school stuff with lists.
If I've got a list called coins and there's three elements,
the indices or indexes of those elements are zero, one, and two respectively
from left to right.
So all I'm doing here now is passing in the first element
from that list as galleons, the second element of that list as sickles,
and the third element of this list as my knuts.
And that lines up with, of course, the signature
of this function, which as total expects that I've passed in those three
things in that order left to right.
Let me go ahead and run, just to make sure I haven't broken
anything, unpack.py and hit Enter.
And the math still checks out.
But this is getting a little verbose--
a little verbose.
And wouldn't it be nice if I could just pass the list of coins
to this total function?
Wouldn't it be nice if I could just say something like this, coins.
But let me pause and ask the group, why would this not actually work as is?
It technically is passing in all three.
But why would I get some kind of error when I run this?
Eric?
STUDENT: Because you are passing a list to galleons.
DAVID J. MALAN: Yeah.
I'm passing a list to galleons and nothing for sickles and knuts.
And notice, those don't have default values.
There's no equal signs on that first line
of code, which means Python is not going to know
what value should be assumed there.
So it just seems like it's not going to work.
Plus, it's the wrong type, as Eric notes.
It's a list and it's not an integer as it was before.
So let's actually run this incorrect version,
Python of unpack.py Enter, type error.
And that is probably what you might expect,
like I'm messing up with the types here.
And I am required to pass in two positional arguments, sickles
and knuts, that were not even passed.
So I've definitely erred here.
But it certainly seems unfortunate if the only solution to this
is to do what I previously did, which is index into the first element, index
into the second element, index into the third.
You can imagine, with bigger fancier functions that
take even more arguments, this is going to get very verbose and honestly
very vulnerable, potentially, to just mistakes, typos on my part.
But here too is where you can do what's known, again,
as unpacking a value in Python.
Right now, a list is packed with multiple values.
My current list has these three values, 100, 50, and 25 respectively.
But they're all packed up in this one list.
Wouldn't it be nice if I could unpack that list,
just like I previously unpacked the return value of the str class's split
function into multiple things too.
And indeed, I can do just that.
Python actually allows me to pass in not coins but star coins.
So if you use a single asterisk at the beginning of your variable,
that will unpack it.
And it will take one sequence, in this case coins of size three,
and explode it, if you will, unpack it into three individual arguments.
No commas are needed.
Python just handles this for you.
But the effect of passing in star coins is
to pass in the individual members of that list.
Which in this case are going to be 100, 50, and 25 respectively.
Which is perfect, because now it's going to line up with galleons,
sickles, knuts respectively.
So now when I run Python of unpack.py, we're back in business
and the math checks out.
But I've cleaned up my code by just introducing this new symbol, which
we've used, of course, in other contexts for multiplication and the like.
But now, it's also used for unpacking in this way.
Questions on what we've just done?
It's a single operator, but it's already quite powerful.
Because it allows us to take a data structure and unpack it
and pass it in individually.
STUDENT: Does that work for tuples, sets, dicts, dictionaries as well?
DAVID J. MALAN: Tuples, yes.
Sets I don't know.
[? Ranshin? ?] I don't know if order is preserved.
No.
Oh, is that no it does not, or you're checking?
Order's not preserved.
So it wouldn't work with set?
It does not work with set.
Does not work with set.
Sorry, I'm verbally googling here just to save us some keystrokes.
So it would work for enumerations that where order is indeed preserved.
And we'll see another example in a moment
where it actually can be used in a different way for dictionaries,
which nowadays do preserve order.
Other questions on unpacking in this way?
STUDENT: Yes.
Hi.
DAVID J. MALAN: Hello.
STUDENT: Can you use unpacking to get the value, for example 10 plus 50
plus 25 instead of a for loop and then result plus?
DAVID J. MALAN: Short answer, no.
If you want the individual values, you should be just indexing, in this case,
into those specific locations.
This is returning multiple values, the equivalent of a comma separated list.
So you would use the earlier approach if you
cared about the individual locations.
How about one other question on unpacking?
STUDENT: What if we have declared--
we declare some default values.
And if you use this as two points, will it go out right, or will it skip it?
DAVID J. MALAN: Good question.
If I heard you right, what if, for instance,
the list has four values like this here and you're still unpacking it
when it's only three that's expected.
Well, let's try it.
Python of unpack.py Enter.
Another type error.
This time it takes three positional arguments but four were given.
So the onus is on us as the programmer not to do that in this case.
So potentially fragile, but avoidable if I'm
controlling the contents of this list.
In fact, let me propose now that we take a look at another variant of this.
Whereby we use not just positional arguments,
whereby we trust that the first is galleons, the second is sickles,
the third is knuts.
Suppose that we actually passed in the names as we're allowed to do in Python.
And then, technically, we could pass them in in any order
and Python would figure it out using named parameters instead.
Well, how might I do this?
Well, it's going to be a bit of a regression at first.
So let me get rid of this list here.
Let me change this now to just manually passing the values I care about.
Galleons I want to still equal 100.
Sickles I want to equal 50.
And knuts I want to equal 25.
So this is old-school parameter passing.
It's no longer positional.
I'm explicitly specifying the names of these arguments.
But that's just going to work because that's
exactly what the names of these parameters are in my total function
as before.
Let's make sure I, nonetheless, did not break anything.
Let's run Python of unpack.py Enter.
And there we have it, still 50,775 knuts.
Well, once you start giving things names and values, names and values,
that probably should bring to mind one of our most versatile data
structures in Python and even other languages, that of a dictionary.
Remember that a dictionary is just a collection of key value pairs, names
and their respective values.
So this kind of opens up an opportunity.
What if I did this.
What if I actually had for some reason in my program on a variable
as before called coins.
But instead of making it a list of three values like before,
what if it's a proper dictionary?
So what if it's galleons, quote unquote, colon 100 for 100 of those,
sickles quote unquote and 50 of those, and knuts quote unquote 25 of those,
each of those separated by colons.
And let me fix my square brackets to this time
be curly braces, which, recall, is the symbol we use for dictionaries
or dict objects in Python.
So now, I have a dictionary called coins.
Not a list.
It's a collection of keys and values, three keys, galleons, sickles, knuts,
and three values, 100, 50, and 25 respectively.
If I were to now pass these individual values into my total function,
I could do it as always with my dictionary.
So I'm doing it old-school now.
Coins is the name of my dictionary.
I index into it not with numbers like with lists, but with words.
So galleons, strings like this, coins, quote unquote,
sickles in square brackets there.
And then lastly, coins, square brackets, quote unquote, knuts.
So it's getting-- it's verbose again.
This is not maybe the best road to go down.
But we'll backpedal in a moment.
This is just how, if you happen to have all of your coins stored
in a dictionary, you could pass the galleons, sickles, and knuts
into your function respectively.
Let's make sure I didn't break anything.
Let's rerun Python of unpack.py, and we're still good.
Now, how could we get to a situation like this?
Well, as always, imagine this program is a little longer than this one here.
And somehow you're using a dictionary maybe
just to keep track of someone's purse or wallet,
like how many coins of each type that they have.
And as such, it's perfectly reasonable to use a dictionary.
But then you want to print out the total.
And darn it.
If that total function does not expect a dictionary so you cannot just do
something nice and simple like pass in coins.
For reasons we saw earlier, that would be a type error.
Total expects three arguments, three integers.
You can't just pass in a dictionary.
But if that's the data structure you're using to store the person's purse
or wallet, well, it's kind of unfortunate
that we have this clash between these data types.
Well, here's what we can do.
We can't pass in coins.
Because watch, if I try doing that and run Python of unpack.py,
we're getting another type error.
Missing two required positional arguments.
Sickles and knuts, I have to pass in three things.
But, wonderfully, Python allows you to unpack dictionaries as well.
For a dictionary, you don't use a single asterisk, you use two.
And what this syntax has the effect of doing
is passing in three values with names.
It has the effect of passing in galleons equals 100 comma,
sickles equals 50 comma, knuts equals 25.
And so, it has the similar effect to the list unpacking.
But that just passed in the values, 100, 50, 25 separated by commas in effect.
When unpacking a dictionary, it passes in the keys
and the values separated conceptually with equal signs
just like our function expects.
So if I now run Python of unpack.py again, we're still good,
but we've tightened our code up again.
And now, I'm giving myself yet another option.
I can either store a wizard's purse or wallets in their--
in a list as we did earlier.
Or I can store it in a little more versatility-- with even more
specificity using a dictionary instead.
And so, to be clear, let me rewind.
Star star coins is the same thing if I rewind
a little bit to our first example of named arguments
is equivalent to what I've highlighted here.
When you unpack a dictionary, it passes in all of the keys
and all of the values much like the syntax here.
But let me tighten it up and go to where we left off.
Questions now on unpacking?
STUDENT: Can we have a-- in this dictionary,
can we have instead of having a constant name value pair, can we have
a variable number of name value pairs?
DAVID J. MALAN: Short answer, yes.
You can have more than three key value pairs as I have here.
But it's not going to work unpacking it if the total function is
expecting only three.
So if I were to add something here, like let me introduce pennies
to the wizarding world.
And suppose I have one penny, for instance.
And now I run this same code, Python of unpack.py, we're back to a type error
again.
Whereby I got an unexpected keyword argument pennies,
because that is not expected by the total function.
We will see in just a moment, wonderfully, a solution though to that.
But for now, it does not work.
Other questions on unpacking with dictionaries or lists?
STUDENT: In list-- in list values, we gave the same number of arguments
and we declared a default value in the function.
Now, if you use this asterisk, will it overwrite that value
or will it skip that default value?
DAVID J. MALAN: A good question.
If we did have default values up here, for instance,
equals zero, equals zero, equals zero, the upside of that,
recall, from our discussion of arguments to functions a while back,
is that now you don't have to pass in all of those values.
They will default to those zeros.
Therefore, you could pass in fewer than three values,
either using a list or a dictionary that's unpacked in this scenario.
I deliberately did not do that because I wanted us to encounter
this specific error in this case.
But you could absolutely go back and add those defaults.
So it turns out that this single asterisks or this double asterisk
is not only used in the context of unpacking.
That same syntax is actually used as a visual indicator in Python
when in a function itself might very well
take a variable number of arguments.
That is to say, a function can be variadic.
Which means that it doesn't necessarily have to take, say,
three arguments specifically.
Even if they do or don't have default values,
it can take maybe zero, or one, or two, or three.
And it turns out the syntax for implementing the same idea is
quite similar in spirit.
In fact, let me go back to VS Code here.
And let me propose that we start over with this code
and get rid of our notion of galleons, and sickles, and knuts,
and do something just a little more generic just so that we've
seen the syntax for this.
Suppose that I define a function as follows.
Define a function, let's call it f.
And that function is not going to take a specific number of arguments
but a variable one.
And so, I'm going to go ahead and use this syntax here,
star args, which indicates that this function is indeed variadic.
It takes some variable number of positional arguments.
Positional in the sense that they go typically from left to right.
But I don't know how many just yet I want to support.
Suppose that I additionally want to support
some number of keyword arguments, that is,
named parameters that can be called optionally
and individually by their own name.
Well, the convention syntactically here would
be to use two stars and then kwargs.
I could call args, or kwargs, anything else that I want.
But a convention you'll frequently see in Python's own documentation
is that when you have placeholders like this for some number of arguments
and some number of keyword arguments, the world tends to use args and kwargs.
Well, inside of this function, let's do something super simple just for now.
Let me go ahead and print out literally quote unquote positional,
just to indicate to myself while wrapping my mind around what's
going on here what the positional arguments are.
And let me quite simply print out those args.
This is not something you would typically do.
You don't typically just take in these arguments
and print them, no matter how many there are.
I'm just doing this diagnostically for now to show you how the syntax works.
Now, let me go ahead at the bottom of my file--
and I won't bother with a main function this time so we can focus only
on this function f.
Let me go ahead and just call f with three arguments.
I'll use the same arguments as before.
But I didn't bother giving them names just yet, like galleons,
and sickles, and knuts, and the like.
So what do I have?
A program that no matter what calls this function f,
but it first defines f at the top of the file.
It's taking some number of positional arguments,
some number of named arguments.
And for the moment, I'm just printing out the positional ones.
Let me go ahead and in my terminal window
run Python of unpack.py and hit Enter.
And you'll see that the positional arguments passed in
are apparently this--
a sequence, 100, 50, 25.
But notice this.
If I clear my terminal window there and pass in something else,
like five, a fourth argument.
Previously, if I tried to change the number of arguments
I'm passing in to my total function, which was only defined as taking three,
I would have gotten a type error, some visual indication
that, no, you can't pass in more or fewer arguments
than is actually in the function's definition.
But now watch.
If I run Python of unpack.py, this time passing
in 100, 50, 25, and 5, a fourth argument, all four of those
went through just fine.
I can get rid of all of those but one, for instance now, rerun my program
after clearing my screen.
And now, I'll see just one argument here.
And even though there's a comma and nothing after it,
this is actually the syntax when seeing a tuple, in effect, whereby
the comma just indicates this is indeed a list,
but there's only one element therein.
Well, let's get a little more curious too.
Let me go ahead and rewind here to where we
started with just those three values.
And this time, let me go ahead and print out my named argument, so to speak,
which isn't args but kwargs.
Again, the positional args in this syntax come first.
The named arguments, kwargs come second.
That's what Python prescribes.
So now, let me go ahead and not pass in just these numbers.
Let me go ahead and pass in actually named arguments.
So let me do something now more specifically, like galleons equals 100,
and sickles equals 50, and knuts equals 25.
I'm not going to bother doing any math with total.
I just want to poke around right now at this functionality of having
a variable number of arguments.
And what's neat now is if I run Python of unpack.py and hit Enter, no problem.
What kwargs is, is automatically a dictionary that
contains all of the named arguments that were passed to my function.
Which is to say, when designing your own functions,
if you want to support more than one argument,
maybe more than two, or three, or four, maybe a variable number of arguments,
indeed, you can support both a variable number of positional arguments
that are just value comma value comma value, or any number of named
arguments, where you actually put the name of the parameter
equals the value and then maybe a comma and some more of the same.
So now, it turns out we have seen this before in some of the functions
we've used to date.
We didn't necessarily see it called args or necessarily see it called kwargs.
But we have seen at least one example of this in the wild.
Recall our old friend print, which we've been using now for weeks.
And when we first looked at the documentation for print way
back when, it looked a little something like this.
The first argument to print was objects.
And I waved my hand at the time at the asterisk that
was at the start of that variable name.
But then we had sep for separator, the default value of which was a space.
We had n, the default value of which was a new line.
And then some other names arguments that we waved our hands at then
and I'll again do now.
But what you can now perhaps infer from our emphasis
on these asterisks today, the single stars or the double stars,
is that you know what?
This is the convention in Python's documentation
to indicate that print takes a variable number of arguments.
So if we were to look at the actual implementation of the print
function implemented by Python's own authors,
it might very well look something like this.
Def print, and then the first our argument
would be star objects, thereby indicating that print
takes a variable number of arguments.
The next one of which might be sep equals quote unquote
either using double quotes or as in the documentation single quotes too.
The next one of which might be n, the default value of which is a new line.
And then some of those other named arguments
that we've not looked at as well.
And then, maybe inside of the print function
implemented by the authors of Python, maybe
there's a for loop like for object in objects
that allows them to iterate over each of those variable number of objects
and print each of them.
And this is why in programs past, you and I
have been able to do just print open parentheses close parenthesis
with nothing inside.
Or you and I have been able to print out something
like, Hello, world, a single string inside of those parentheses.
Or you and I have been able to do a single string, Hello,
and then another string quote unquote world,
thereby passing in two arguments or even more.
So we've long had this ability to use variadic functions,
whereby you can pass in a variable number of arguments.
What you now have via this args and kwargs syntax--
but again, they do not need to be called that-- is
the ability using that star or two stars to implement those kinds of functions
yourself.
My own f function a moment ago did not do anything all that interesting.
But it hints at how you could, if in the future
you have a use case, for taking zero or one or more of either type of argument.
Any questions now on these types of arguments?
STUDENT: What will happen if you print kwargs and the argument is like a list?
DAVID J. MALAN: Ah.
So what would happen if you print the argument like it's a list?
So I think we saw that.
If I roll back in my history here to when I had that f function.
Which I called f just to be very generic just so
we could play around with the syntax.
This is what I had here.
So this is a--
I passed in 100 comma 50 comma 25.
That gets automatically stored in args.
And when I run it, you can actually see that sequence of values
by running Python of unpack.py.
There is that sequence all in the form of one single variable.
I'm printing it just for diagnostic purposes.
This is not really a useful or pretty program.
But it hints at how we can access that whole sequence of values.
Other questions on this approach here?
STUDENT: Can we pass the kwargs from one function to another function?
DAVID J. MALAN: Absolutely.
You can pass either of those to another function, which
you might want to do if you want to wrap another function,
provide some additional functionality, but still
pass in all of the supported arguments to the underlying function as well.
All right.
How about this next.
It turns out that a few other tools we can add to your tool kit
relate to the types of programming models that Python supports.
We started out quite some time ago focusing really
on procedural programming in Python.
Whereby we wrote code top to bottom, left to right, defining some functions,
or if you will, procedures along the way,
defining variables, and having side effects,
and assigning values as needed.
But we then eventually introduced or really revealed
that Python is also very much object-oriented.
And a lot of those variables, a lot of those types
that we were using all that time were in fact objects,
objects that came from certain classes.
And those classes were templates of sorts, blueprints,
via which you could encapsulate both data and functionality therein.
Well, we also saw along the way some hints
of a third paradigm of programming that Python also,
to some extent, supports, which is known as functional programming.
Whereby functions are ever more powerful in that
they tend not to have side effects, no printing or changing of state globally.
But rather, they're completely self-contained
and might take as inputs and return values.
And that's generally a paradigm we saw when we started sorting things,
particularly with functions like our sort function
or Lambda function when we passed in the function
we wanted to use to sort a list way back when.
Well, it turns out Python has other functionality that
is reminiscent of functional programming and indeed is a powerful way
to solve problems a little more differently still.
Let me propose this.
Let me propose that I whip up a new program here in VS Code
by closing our unpack.py and this time creating another program called yell.
Suppose the goal at hand is to implement some program that allows the user
to pass an input, and then it yells the response
by forcing everything to uppercase.
My apologies to those with headphones there.
I'll modulate.
So let me go ahead and run code of yell.py.
And within yell.py, let's go ahead and implement
a program that really does just that.
Let's go ahead and define a main function up here.
And let's assume for the moment that this yell function already exists
and yell something like, This is CS50, properly capitalized, not in all caps.
Now, let's go ahead and implement this yell function with def yell.
It's going to take, for now, a single word or phrase.
And let's go ahead.
And I'll call it phrase here.
And I'm going to go ahead and just print out the phrase.upper.
So phrase.upper is going to force the hole thing to uppercase.
And as usual, down here if the name of this file equals
equals quote unquote main, then let's go ahead, as always, and call main.
So let's just run this.
But for the most part, it should be fairly straightforward.
When I run Python of yell.py, THIS IS CS50 is yelled on the screen.
All right, that's nice.
But it's not great that yell only expects a single phrase.
Wouldn't it be nice, like print, if I could
pass in one phrase, or two, or three, or really multiple words more generally
but as individual words themselves.
So let me retool this a little bit and change yell to take in not a phrase
but how about something like a list of words.
So that ultimately, I can call yell like this.
Quote unquote, this inside of a list, quote unquote, "This" inside of a list,
and, quote unquote, "CS50" inside of a list.
I'm not going to bother with type hints or annotations for now.
But I'll just assume that yell has been defined now as taking a list of words
as defined here.
But now I want to force them all to lowercase.
So I don't quite want to do something as simple as this.
Like for word in words, I could, for instance, print that given word
and maybe end the line with nothing right now.
But I think if I do this, Python of yell.py, no, that's not right.
I haven't forced anything to uppercase.
So let's fix this.
Well, let's go ahead and do the following.
Let me go ahead and accumulate the uppercase words as follows.
Let me create a variable called uppercase
and initialize it to an empty list using square brackets or our more
verbose list syntax.
And now, let me go ahead and iterate over each of those words in words.
And for each of them, let's go into our upper cased list, append to it
the current words uppercase version.
So this is a way of creating a new list called uppercase
that is just appending, appending, appending to that list
each of the current words in the loop but uppercased instead.
And now, just let me go ahead and print out the uppercased list.
This isn't quite right.
Let's see what happens here.
Python of yell.py, OK.
It's not quite right, because I don't think I want
those quotes or those square brackets.
What am I seeing?
I'm actually printing a list.
But, but, but, here's where some of our unpacking syntax now can be useful.
I don't have to change my approach to this problem.
I can just unpack uppercase by adding a single star.
And now, let me go ahead and rerun Python of yell.py.
And now, it's actually just English.
There's no remnants of Python syntax like the quotes,
and the commas, and the square brackets.
I've now unpacked, this is CS50 as three separate arguments to print.
So already now, this unpacking technique would seem to be useful.
Well, it's a little unfortunate that I now
need to call yell though with a list of values in this way.
This is just not the norm.
Or it's at least, it's not nearly as user-friendly
as something like the print function where
I can pass in zero, or one, or two, or three, or any number of arguments.
Why are you making me for your yell function pass in only a list?
Well, we can do better.
Let's adopt some of the new conventions we've learned.
And let's go ahead and get rid of the list by removing the square brackets.
And let's just pass yell three arguments.
Now, I don't want to do something like change the definition of words
to take in word one, word two.
That's not going to scale and it's not going
to handle different number of words.
But we have a technique now.
We can say star args, which will allow the yell function
to accept any number of arguments.
And just for specificity, let's not call it generically args.
Let's name it something a little more self explanatory like star words.
This just means I have a variable number of words being passed in.
Now, I think, I've made a marginal improvement.
Let me run this again, Python of yell.py.
This is CS50 is in all caps.
But it's just a little better.
Because now I can treat yell just like I've long treated print,
pass in as many things as you want, and print will deal with it.
Now, my yell function is just as powerful it would seem.
And better still, it also forces everything to uppercase.
Well, it turns out Python comes with this function called
map, whose purpose in life is to allow you to map, that is, apply
some function to every element of some sequence like a list.
So for instance, if we want to force to uppercase each of the words,
this is CS50 in the list of words that's been passed in, well,
we essentially want to map the upper case function to each of those values.
So using map in Python can I do just that?
Let me go back here to VS Code.
And let me propose now that I re-implement this as follows.
I get rid of all three of these lines here, getting rid of that loop
in particular.
Let me still declare a variable called uppercased.
But let me set it equal to the return value of this new function called map.
Map takes two arguments here.
In this case, the name of a function that I want to
map on to a sequence of values.
Well, what function do I want to apply to every word that's been passed in?
Well, it turns out, thanks to my knowledge
now of object-oriented programming, I know that in the str class
there is a function called upper.
We've usually called it by using the name of a string
variable.upper open paren close paren.
But if you read the documentation for the str class,
you'll see that the function is described indeed as str.upper.
I'm not using parentheses, open and close, at the end of str.upper.
Because I don't want to call it now.
I want to pass this function to the map function,
so that map can somehow add those parentheses, so to speak, and call it
on every one of these words.
And this is what map does quite powerfully,
and is an instance, indeed, of functional programming.
Whereby I'm passing to this map function another function.
Not calling it.
I'm just passing it in by a reference of sorts.
And what map is going to do for me is iterate over each of those words,
call str.upper on each of those words, and return
to me a brand new list containing all of those results together in one list.
It completely obviates the need for me to do this more manually using
that list.
I'm still going to print the whole thing using star uppercase.
So that if I get back a list of three uppercase words,
I'm going to unpack them and print them all out.
So let's run this again.
Python of yell.py Enter.
And voila, it's still working.
But the code now is even more tight--
even tighter than before.
So it turns out there's another way we can
solve this problem in a way that's even more
Pythonic, or at least quite common.
And that's using a feature known as a list comprehension.
And it's a big phrase, if you will.
But it refers to the ability in Python for you
to very easily construct a list on the fly without using a loop,
without calling append and append, but to do everything
in one, daresay, elegant one-liner.
So how can I go about using this notion of a list comprehension?
Well, let me go ahead and do this.
In yell.py, in VS Code here, let me go ahead
and change my approach as follows.
Instead of using map, which is perfectly fine and correct in this way,
let me just show you this other way as well.
A list comprehension is the opportunity to create a list like this,
using square brackets like this.
But inside of those square brackets to write a Python expression,
that in effect is going to dynamically generate a brand new list for you
using some logic you've written.
And the approach I might take here is this.
If I want to store in this list the uppercase version of every word
in that words list, I can do this--
word.upper for word in words.
Now, this is a mouthful.
But I dare say Python programmers love this capability
of being able to define on the fly a list inside of which
is any number of values that you would ordinarily, at least as we've done it,
construct with a loop and again calling append, and append, and append.
But that usually takes two, three, four or more lines.
This list comprehension that I've highlighted here
is now an alternative way to create the exact same thing--
a list inside of which are a whole bunch of uppercased words.
Which words?
For each word in the words list that was passed into yell
is what ends up in this list.
Questions on this syntax here?
It definitely takes a little bit of getting used to.
Because you've got this value on the left, this function call here.
You've got this loop inside of the square brackets.
But if you become accustomed to reading the code in this way from left
to right, this means give me the uppercase version
of the word for each word in my words list.
Questions here on list comprehensions?
STUDENT: Hi.
Can you do conditionals also, like if else, or combine if, elif, else?
DAVID J. MALAN: Indeed, you can.
And let me come back to that, where we'll see an opportunity
to do things conditionally.
But for now, I'm just uppercasing every word in the list.
Good question.
Other questions?
STUDENT: Yeah.
Is this functional programming?
Or I mean, this particular thing, we are using words.upper for a word in words?
DAVID J. MALAN: Not necessarily.
This is more of a feature of Python, I would say.
STUDENT: OK.
DAVID J. MALAN: Yeah.
Map was one very specific incarnation of thereof our use of Lambda
and passing it in as a key attribute to the sort function,
sorted function a while back was an example.
And we're about to see one other.
So we can even use these list comprehension
to filter values in or out of our resulting list.
So in fact, in VS Code here, let me close yell.py and close my terminal
window.
And let me create a new program here whose purpose in life
maybe is to take a same list of students as before with a shorter version
thereof, and just filter out all of the students in Gryffindor.
So let me go ahead and create a file called Gryffindors.py.
I'm going to go ahead and copy paste from before really my list of students,
at least Hermione, Harry, Ron, and Draco from the start
here, just so that I can focus on one student who
happens not to be from Slytherin.
And what I'm going to do here now, if I want
to filter out only the Gryffindor students, let me go ahead and do this.
Let me create another variable called Gryffindors, which
is going to equal the following list.
And this is going to be a bit of a longer line.
So I'm going to proactively move my square brackets
onto two separate lines.
And I'm going to create now a list comprehension.
I want to do this.
I want this new list called Gryffindors to contain every student's name
for each student in the student's list.
But, but, but, if the student's house equals equals quote unquote Gryffindor.
So this is nearly identical in spirit to what I just did earlier
to create a list comprehension out of each of the words passed
to my yell function.
But here, I'm doing so conditionally.
And so, I'm borrowing inspiration from our focus on loops,
borrowing some inspiration from our focus on conditionals,
combining that into this same square bracket notation.
So that what Gryffindors ultimately is, is zero or more students' names.
And the names that are included are the result
of iterating over each of those students and only including in the final result
the students whose house happens to be Gryffindor.
So when I go ahead and run this with Python of Gryffindors.py
and hit Enter, you'll see, huh, nothing actually happened here.
Well, that's because I didn't finish the program.
Let me go ahead and actually finish the program with this.
How about for each Gryffindor in Gryffindors plural--
and better yet, so that it's sensible that I did all of this work in advance,
let me go ahead and sort all of those names
with our familiar sorted function.
Let's go ahead now and print out each of these Gryffindors.
So now, notice, if familiar with the books and the movies,
you'll know that only three of these four students
are actually in Gryffindor.
And if I run Python of Gryffindor.py, there we see Harry, Hermione, and Ron,
but now in sorted order as well.
So that's just one way we can solve this same problem using not just a list
comprehension but a list comprehension that has this conditional therein.
But there's yet other ways to solve this same problem too.
And we come back to some functional features of Python.
In addition to functions like map, there's
also this one called filter that can be used to achieve the same effect,
but with a more functional approach, if you will.
Let me go back to VS Code here.
And with the same example, let me do this.
Let me leave the original list of above as before, including Draco,
who's not in fact from Gryffindor.
And let me temporarily define a function called
is Gryffindor that takes in as a value something like a student S.
And then, let's do this.
Let's go ahead and say if s quote unquote house equals equals Gryffindor,
then go ahead and return true.
Otherwise, go ahead and return false.
Now, we've seen before conditionals like this
that are a bit unnecessarily verbose.
I don't need to have a conditional if I'm already
asking a Boolean question up here.
So I can actually tighten this up as we've done in the past
and just return does the student's house equal equal Gryffindor?
Either it does and it's true, or it doesn't in it's false.
I don't need to explicitly return true or false.
I can just return the value of that Boolean.
Let's go ahead now and do this.
I'm going to create, as before, a variable
called Gryffindors, a list for all of my Gryffindor students
that equals to, this time, the result of calling filter.
Filter takes at least two arguments here,
one of which is the name of a function to call is Gryffindor.
And I'm going to apply that function to each of the elements of this sequence
here.
So similar in spirit to map, I'm passing in a function
that's going to be applied to each of the elements in the sequence.
But map returns one value for each element in the sequence.
That's how we forced all of the words to uppercase.
But if I want to conditionally include a student in my resulting Gryffindors
list, I can use filter instead.
Filter expects its first function to be not something like str.upper,
but a function that returns true or false.
Tell me whether or not I should include or not include the current student
from the final list.
And the question being asked is, do they live in Gryffindor?
We're checking the dictionary's house key for that answer.
And so, ultimately, I think we'll be left with something quite similar.
For Gryffindor in the sorted version-- let's do for Gryffindor in Gryffindors,
let's go ahead then and print out the current students, Gryffindor name.
It's not going to be sorted just yet.
But when I run this version here Python of Gryffindors.py and hit Enter,
we're back in business.
It's unsorted.
But we have Hermione, Harry, and Ron, but not Draco.
And if you recall from a few weeks back, if we
want to even a list of dictionaries, we can still do that too.
I can call sorted on Gryffindors plural.
And I can pass in a key.
And that key can have a anonymous function, a.k.a.
A Lambda function, that takes in a student as input,
call it s, and then returns the value s quote unquote name, if my goal is
to sort by, indeed, students own names.
If I go ahead now and run Python of Gryffindors.py,
I see the same list of students.
But this time, it's sorted.
So here we've seen two approaches to this particular problem
of Gryffindor students.
Whereby we can either use something like a list comprehension,
and inside of that list comprehension do a bit of filtration,
including an if conditional as I did.
Or we can take a more functional approach
by just using this filter function, passing into it the function
that I want to make these decisions for me,
and then include only those for whom true is returned.
Any questions on either of these two approaches?
STUDENT: Yeah.
I just had a question, that if we write a code
like in the previous version, where everything is stuffed into one line,
won't the--
if we check for the style of the code, then won't it have a problem with it
because it's less readable?
DAVID J. MALAN: So would a formatter like black
have a problem with the style of some of this code?
STUDENT: The previous one, where the everything was tucked into one line.
[INTERPOSING VOICES]
DAVID J. MALAN: Oh, a good question.
Would something like Black have a problem with this code?
Well, let me rewind to that version, which
was using the somewhat longer list comprehension, which
looked like, if we go far enough back, give me a few more undoes,
which looked like this ultimately.
Let me go ahead and run Black on Gryffindors.py.
And you'll see that I actually-- it reformatted ever so slightly.
But I proactively fix this myself.
Had I done this and done it on just one line,
but I knew that Black might not like that, it would have fixed it for me.
So I just proactively fixed it before writing the code myself.
How about time for one other question on Gryffindors.py
and this approach of using a list comprehension or filter?
STUDENT: Yeah.
When using filter, instead of calling the function is Gryffindor,
can you use it right there inside filter?
DAVID J. MALAN: Can you use the function is Gryffindor?
So you don't want to call it like this.
Because you don't want to call it then.
You want filter to call the function for you, if that's what you mean.
So I pass it in only by its name instead.
STUDENT: No, I mean, if you can write the return as house equals
equals Gryffindor inside [INAUDIBLE].
DAVID J. MALAN: Yes, indeed.
In fact, so recall that we indeed used these Lambda functions way back
when we wanted to pass in a quick and dirty function anonymously
to allow sorted to filter by a different key of a dictionary.
We can do that here.
I can actually take the essence of this is Gryffindor function.
I can change the name of this function in my filter call
to be another Lambda function, passing in an argument like s,
and returning exactly that.
I can now delete my is Gryffindor function all together.
And now, when I run Python of Gryffindors.py,
I still get the same answer.
And I've not bothered defining a function only to then use it
in one and only one place.
Well, let me propose too that we equip you
with one other tool for your toolkit, namely dictionary
comprehensions as well.
And admittedly, the syntax is starting to get even weirder.
But as you get more comfortable with all of these primitives and others,
these are just tools that you can optionally but perhaps powerfully use
to solve future problems down the road.
And with a dictionary comprehension, we have the ability to create on the fly
a dictionary with keys and some values without having
to do it old-school by creating an empty dictionary, and creating a for loop,
and iterating over that loop, and inserting more and more keys
and values into the dictionary.
We can rather do it all at once.
So in fact, let me go back to VS Code here.
And let me propose now that I do this.
Let me go ahead and initially do it the old-fashioned way here as follows.
Let me go ahead and simplify and get rid of the houses
all together so that we can focus for now just on a list of students' names.
I'm going to go ahead and run students.
I'm going to go ahead and write students equals quote unquote Hermione, quote
unquote Harry, and we'll keep it even shorter this time,
quote unquote Ron, only those three students in Gryffindor.
I'm going to now proactively as we've done in the past
give myself an empty list, so that I have something to accumulate
some answers to this problem in.
And now, I'm going to do something like this.
For students and students, so I can iterate over each of them,
Let's go ahead and with the Gryffindors list
append to it the name of the student.
So quote unquote name and then student, which
is indeed their name from that list.
And now, let's go ahead and just put these students all in Gryffindor.
I know these three students are in Gryffindor.
So suppose that the problem at hand is that I
want to build up a list of dictionaries that only contains the Gryffindor
students.
So it's sort of a step back from the previous version
where I already had the names and the houses.
For now, just assume that the problem is I have all of their names,
but I don't yet have the student dictionaries themselves.
So I'm rebuilding that same structure that I previously took for granted.
Now, let's go ahead and just for the sake of discussion just
print out these Gryffindors so we can see what we've built.
If I run Python of Gryffindors.py in my prompt,
I see a bit of a cryptic syntax.
But again, look for our little hints.
I've got a square bracket at the end and a square bracket at the beginning.
And that indicates, as always, this is a list.
I then have a whole bunch of curly braces
with a whole bunch of quoted keys.
They happen to be single quotes by convention
when using print on a dictionary.
But that's just a visual indicator that that is my key.
And the first value thereof is Hermione.
Second key is a house.
This value thereof is Gryffindor.
Then there's a comma, which separates one dict object from the next.
And if we look past Harry and Gryffindor,
there's a second comma which separates Harry and Gryffindor from Ron
and Gryffindor as well.
So in short, here is some code whereby I fairly manually
built up with a for loop in an otherwise initially empty list
the same data structure as before minus Draco just for Gryffindor students.
But here's where, again, with dictionary comprehensions
or really list comprehensions first, can we do this a little more succinctly?
Let me clear my terminal window.
Let's get rid of this initially empty list and this for loop
that appends, appends, appends to it.
And let's just do this.
A Gryffindors variable will equal the following list comprehension.
Inside of that list, I want a dictionary structured
with someone's name and their name.
Someone's house and only for now Gryffindor.
And that's it.
But I want one of these dict objects here in these curly braces
for each student in students.
So here too, inside of my list comprehension with my square brackets,
I want an object as indicate--
I want a dictionary as indicated by the curly braces.
I want each of those dictionaries to have two keys--
name and house respectively, the values thereof
are the student's name from earlier here and Gryffindor only.
Which students do I want to create those dict objects from?
Well, for student in students.
So again, on the left I have what I want in the final list.
And on the right, I have a loop, and this time, no conditional.
I want all of these students in Gryffindor as their house.
Now, let's print this again, Python of Gryffindors.py and hit Enter.
And now, we have the exact same output.
So instead of three lines it's just one.
It's a little more cryptic to read at first glance.
But once familiar with list comprehensions and this sort of syntax,
it's just another way of solving that same problem.
What if I want to change this and simplify?
What if I don't want a list of dictionaries, which I now have.
Again, per the square brackets I have a list of three dict objects here.
What if I just want one bigger dictionary
inside of which is a key like Hermione colon Gryffindor, Harry colon
Gryffindor, Ron colon Gryffindor.
I don't need a list.
I don't need separate objects per student.
I just want instead one big dictionary where
the keys are the students' names and the values of their house.
And I'm assuming for now no one's going to have
the same first name in this world.
Well, I can do this.
Let me get rid of this here and not create a list comprehension, but again,
this thing known as a dictionary comprehension.
And the visual indicator or difference here
is that instead of being square brackets on the very outside, this time
it's going to be curly braces instead.
So inside of these curly braces, what do I want every key to be?
I want every key to be the student's name.
I want every value for now to be Gryffindor.
And I want to do this for each student in students.
And now, things are getting really interesting.
And this is another manifestation of Python in some views being
very readable from left to right.
Absolutely takes practice and comfort.
But this is creating a variable called Gryffindor
which is going to be a dictionary per these curly braces.
Every key is going to be the name of some student.
Every value is going to be Gryffindor.
What names of what students?
Well, this dictionary comprehension will be
constructed from the list of students one at a time.
So when I print this now, the syntax will look a little different
because it's not a list of dictionary objects.
It's just one bigger dictionary object itself.
But now printing Gryffindors gives me Hermione colon Gryffindor, Harry colon
Gryffindor, and Ron colon Gryffindor as well.
Any questions now on what we've called dictionary comprehensions as well?
Any questions on here?
No?
Well, let's introduce one other function from Python's toolkit followed
by one final feature and flourish.
And then you're off on your way.
Well, let's go ahead and think back to this.
Recall some time ago that we had just a simple list of students
as we have here, Hermione, Harry, and Ron.
And for instance, way back when, we wanted to print out,
for instance, their ranking from one, to two, to three.
Unfortunately, when you do something like this for student in students,
you can print out the student's name quite easily.
Of course, if I do Python of Gryffindors.py,
I get Hermione, Harry, Ron in that same order.
But I don't see any numerical rank.
I see no number one, two, or three.
So I could maybe do this with maybe a different type of for loop.
Instead of this, why don't I try this?
So maybe I could do for i in the range of the length of the students list--
and we've done something like this before.
And then I could print out i, and I could print out the student's name
by indexing into that list at location i.
Well, what does this look like?
If I run Python of Gryffindors.py it's close.
But these aren't programmers.
They don't necessarily think of themselves as zero-indexed.
Hermione he probably wants to be first not zero.
So how can we fix this?
Well.
Just a little bit of arithmetic.
I could print out i plus one, of course, and then the student's name.
So if I clear my terminal window and run Python of Gryffindors.py doorstop
once more, now we have this enumeration, one, two,
three of each of these students.
But it turns out that Python actually has
had all this time another built-in function
that you might now find useful.
That is namely enumerate.
And enumerate allows you to solve this kind of problem
much more simply by iterating over some sequence
but finding out not each value one at a time,
but both the value one at a time and the index thereof.
It gives you back two answers at once.
So if I go back to VS Code here now and take this approach,
I don't need to do this complicated range, and length,
and then i all over the place.
I can more succinctly do this.
I can say for i comma student in the enumerate return
value passing in students.
So this gives me back an enumeration, if you will.
And now, I can go about printing i plus 1 as before.
And I can print out the student.
So I don't need to index into the list with bracket i notation.
I don't need to call range.
I don't need to call length.
Again, enumerate takes a sequence of values like these students,
and it allows me to get back the current index zero, one, two,
and the current value Hermione, Harry, Ron respectively.
So now, just tighten things up further.
And indeed, that's been our theme here.
Can we solve the same problems as we've been solving for weeks
but tighten things up using just more of this toolkit?
Allow us to equip you with one final tool for your tool kit,
namely this ability to generate values in Python from functions.
This is not a problem that we've necessarily encountered before.
But it turns out, if you're writing a function that reads or generates
lots of data, your function, your program,
your computer might very well run out of memory.
And your program might not be able to run any further.
But it turns out there's a solution to this problem that's
something you might have in your back pocket,
particularly if after this course you start crunching quite a few numbers
and analyzing all the more data.
In fact, let's go back to VS Code here.
And let's go ahead and create a program that's
perhaps timely at this time of day, particularly
depending on your time zone, you might be feeling all the more sleepy.
But here in the US, it's quite common to be
lulled to sleep when you're struggling otherwise
by counting sheep in your head.
And typically, as depicted in cartoons, you
might see in your mind's eye one sheep jumping over a fence, and then two,
and then three sheep, and then four.
And then, eventually, you presumably get so bored
counting these sheep you actually do fall asleep.
So in VS Code here, let's create a program called sleep.py
that allows me to print out some number of sheep
as though I'm counting them in my mind's eye.
And via this program, let's do this.
Let's prompt the user for a variable n, setting
it equal to the integer conversion of the return value of input,
asking the user what's n?
For how many sheep do they want to try counting?
And then, let's do a familiar for loop here.
And we'll start counting from zero as always.
So we'll first have zero sheep, then one sheep, then two sheep, and so
on for i in the range of that value n.
Go ahead and print out.
And I'll paste here an emoji representing a sheep times i.
So the first iteration I'll see zero sheep.
The second iteration you'll see one, and then two, and then however many
specified by n ultimately minus one.
Let's go down into my terminal window here and run Python of sleep.py.
And I should see, indeed, after typing in, say, three
for my value of n, zero sheep, then one sheep, then two sheep, and so forth.
And if I make my terminal window even bigger here,
we can, of course, do many more than this, typing in, for instance, 10.
And you'll see that we get more and more sheep as time passes,
presumably becoming all the more tedious to envision in my mind's eye.
So let's now go ahead and practice what we've
been preaching when it comes to the design of this program
and see if and when we actually run into a problem.
Let me go ahead here now and put all of this in a main function
by defining main up here as always.
Let me go ahead and indent all of this code here.
And then, let me just do this conditionally as always,
if the name of this file equals equals quote unquote main, let's go ahead
and call main.
Let's make sure I didn't break anything just yet, even though functionally this
should be nearly the same.
And if I type in three, I still have zero, then one,
then two sheep on the screen.
But we've been in the habit of course of creating helper functions
for ourselves.
That is, factoring our code in a way that
allows us to abstract away certain functionality, like generating
some number of sheep into separate functions.
So that, one, they're indeed abstracted.
And we no longer have to think about how they're implemented.
And we can even reuse them in projects as in libraries.
But we've also been in the habit too of now testing those functions
as with unit tests.
So I probably shouldn't keep all of my logic anyway in main.
And let's factor some of this out.
Wouldn't it be nice if I could, for instance, just call a sheep function
as by taking this line of code here.
And instead of just printing it here, let's print
out the return value of a new function called
sheep that tells the function how many sheep to print, i in this case.
Let's go down as always and create another function here called sheep.
The sheep function now will take a parameter n that specifies
how many sheep do you want to return.
And so, that we can test this as with a unit test,
though we won't do that here, let me go ahead and not print the number of sheep
as via a side effect.
But let me go ahead and return one of those sheep times n
so that the user gets back a whole string of sheep that's
the appropriate number to print.
So here too functionally, I don't think we've
changed anything too fundamentally.
Python of sleep.py typing three still gives us
zero, then one, and then two sheep.
But now, we at least have a framework for focusing on the implementation
of this sheep function.
But it's a little inelegant now that it's still up to the main function
to do this iteration.
We've seen in the past way back in week zero,
wouldn't it be nice to define a function that actually handles
the process of returning the entire string
that we want rather than just one row of sheep at a time.
Well, I think we can do this.
Why don't I go ahead and change sheep as follows.
Let me go ahead here and first create a flock of sheep that's initially empty
using an empty list.
Then for i in the range of n, let's go ahead and append to that flock,
for instance, one sheep times i, so that I
keep adding to this list zero sheep, then one sheep, then two sheep, then
three, and so forth.
And then, ultimately, I'm going to return the whole flock of sheep
at once.
So this is going to return the equivalent of all
of those strings of sheep so that, ah, main can handle the printing thereof.
So back up here in main, let's do this.
How about for each sheep, I'll call it s since sheep
is both singular and plural, for s in sheep of n, which again returns
to me a list of all of the sheep, the whole flock, let's just print
out each sheep, s, one at a time.
So, so far so good here, I think.
Let me go ahead and run Python of sleep.py and hit Enter.
What's n three.
And that's still seems to work just fine.
But let me get a little creative here and see
not just three sheep on my screen but maybe 10 rows of sheep.
And that too seems to work fine.
Let me get a little more adventurous and type in maybe 100 sheep.
And it's starting to look ugly, to be fair,
but they're all printing out pretty fast.
Let me go ahead and try again with maybe 1,000 sheep on the screen.
And they flew by pretty fast.
It's still pretty messy.
But they're all there.
We could count them all up.
How about not just 1,000 but 10,000 sheep?
Well, that too seems OK.
It's taking like 10 times as long.
And that's why you see this flickering on the screen.
All of the sheep are still printing.
But, but, but, it's a lot of data being printed.
If I hang in there a little longer, hopefully we'll
see all 10,000 sheep coming to pass.
This is here in the video where we will speed up time, a real online.
Oh my God.
This is a lot of sheep.
There we go.
OK.
And now all of my sheep have been printed.
So it seems to be working just fine.
Well, let me just be even more adventurous
and, OK, let me try my luck.
Let me try, how about one million sheep this time and hit Enter?
Ha.
Something's no longer working.
While we wait for a spoiler here, does anyone
have any intuition for why my program suddenly stopped printing sheep?
What is going wrong in this version, wherein I'm generating
this really big flock of sheep?
STUDENT: We might have run out of memory or computation power.
DAVID J. MALAN: Yeah.
So maybe we're actually pushing the limits of my Mac,
my PCs, my cloud server's memory or CPU, the brains of the computer's
capabilities because it's just trying to generate
massive, massive, massive lists of sheep, one million
of those rows of sheep, each of which has a huge number of sheep.
And it seems that my computer here is honestly just really struggling.
And this is really unfortunate now.
Because it would seem that even though this program clearly
works pretty well for 1,000 sheep, 10,000 sheep, once
you cross some threshold, it just stops working altogether.
Or it just takes way too long for the program to be useful anymore.
But this seems a little silly, because theoretically, I should absolutely
be able to print all of these same sheep if I just printed one
right away, then print two right away, then print three, then four, then five.
It seems that the essence of this problem, if I go back to my code,
is that per my best practices that I'm trying to practice what I'm preaching,
it seems that the fundamental problem is that I've
modularized my code by creating this helper function called sheep,
whose purpose in life is to do all of the generation of sheep
and then return all of them at once.
Wouldn't it be better--
and I can actually hear my fan turning on now even just
trying to generate these sheep--
wouldn't it be better then to just print the sheep one, two, three,
four at a time?
Well, we could do that.
But that's really a step backwards.
That rather contradicts all of the lessons learned of the past few weeks.
Where generally not putting everything in main is a good thing.
Generally having an additional function that you can then test separately
with unit tests is a good thing.
Do we really need to give up all of those best practices just
to print out some sheep and here fall asleep?
Well, it turns out there's a solution to this problem,
and namely in the form of these generators in Python.
You can define a function as a generator,
whereby it can still generate a massive amount of data for your users.
But you can have it return just a little bit of that data at a time.
And you yourself can implement the code in almost the same way.
But you don't have to worry about too much getting returned all at once.
These two, like all features of Python, are documented
in the official documentation therein.
But what you'll find, ultimately, that it all boils down to this keyword here,
yield.
Up until now, when we've been making functions,
we have been defining functions that return values, if at all,
using the keyword return.
And indeed, if we go back to our code here,
that's exactly what I've been waiting for.
I've been waiting to return the whole flock at once.
Unfortunately, if you wait too long, and here we have it,
my program was quote unquote killed.
That is to say, my computer got so fed up with how much memory and CPU
it was trying to use it just said, nope, you're not going to run at all.
And that's unfortunate.
Now my program no longer works for large numbers
of sleeps-- sheeps, which is not good if I'm really having trouble
falling asleep some night.
So how can I use yield to solve this problem instead?
Well, let me do this.
Instead of building up this massive list of sheep in this big list called flock,
let's just do this instead.
Let me go ahead and simplify this whole function
as follows, whereby I iterate for i in the range of n.
And then on each iteration, in the past I
might have been inclined to use return and return
something like one sheep times i.
But this won't work here.
Because if you want a million sheep and you
start a for loop saying for i in the range of a million,
you're going to return accidentally zero sheep right away.
And then this function is essentially useless.
You shouldn't return a value in the middle of a loop like this
because you're not going to get to any of these subsequent iterations
of the loop.
It's going to iterate once, and boom, you return.
But thanks to this other keyword in Python
called yield, you can tell Python to effectively return just one
value at a time from this loop.
So if I go back to this version of my code here.
And I say not return but yield, this is like saying return one value at a time,
return one value at a time, return one value at a time.
The for loop will keep working and I will
keep counting from zero, to one, to two, all the way up toward one million.
But each time, the function is just going to hand you
back a little piece of data.
It's going to generate, so to speak, just a little bit of that data,
not all of the data at once.
And that's good.
Because my computer has a decent amount of RAM, certainly enough to fit
one row of sheep.
It just doesn't have enough memory to fit, apparently,
one million rows of so many sheep.
So now, if I go to my terminal window and run Python of sleep.py and hit
Enter, what's n?
Three would still work-- zero, then one, and then two.
Let me go ahead and increase the size of this here and run Python of sleep.py.
Let's try one million as before and hit Enter.
And now I immediately see results.
I don't think we'll wait for all of these sheep
to be printed, because then we will literally all be asleep.
But what you'll notice happening now is the program is not hanging,
so to speak.
It's not waiting, and waiting, and thinking, and thinking,
and trying to generate the entire flock at once.
It's just generating one row of sheep at a time.
And it's flickering on the screen because there are so many of them.
And that's all thanks to yield.
It's generating a little bit of data at a time, not all at once.
Any questions now on this feature called generators?
Any questions at all?
To add one more piece of terminology to the mix, just so you've heard it.
This same feature here is returning what we'll technically now call an iterator.
Yield is returning an iterator that allows
your own code, your own for loop in main to iterate over these generated values
one at a time.
STUDENT: How does this yield actually works under the hood?
I mean, is it using multithreading?
DAVID J. MALAN: You can think of the implementation
as being asynchronous in this sense.
Whereby the function is returning a value immediately and then
subsequently giving you back another one as well.
Underneath the hood, what's really happening
is the generator is just retaining state for you.
It does not going to run the entire loop from top to bottom
and then return a value.
It's going to do one iteration and yield a result.
And the Python for you is going to suspend the function, if you will,
but remember on what iteration it was.
So the next time you iterate over it, as is
going to happen again and again in this for loop in main,
you get back another value again and again.
So yield returns, indeed, this thing called an iterator.
And that iterator can be stepped over as in a loop one element at a time.
But the language, Python handles all of that for you
so that you don't need to do all of the underlying plumbing yourself.
How about time for one other question on these generators and integrators
as our sheep continue to fly by?
STUDENT: So if every iteration, the program
will return the memory to the system so the program will not crash?
DAVID J. MALAN: Correct.
On each iteration, it's only returning the one string of sheep that's
appropriate for the current value of i.
It is not trying to return all million rows of the same.
And therefore, it uses really one millionth the amount of memory,
although that's a bit of an oversimplification.
All right.
As these sheep continue to fly across the,
screen let me now go ahead and interrupt this,
as you might have had to in the past with infinite loops in your own code.
Even though this isn't infinite, it's just really long-- control
C will interrupt with your keyboard that program, giving me
back control of my computer.
Well, here we are at the end of CS50's Introduction
to Programming with Python.
And if today in particular of all days felt like a real escalation
real quickly, realize that these are really--
these are just additional perhaps optional tools in your toolkit
that you can add to all of the past lessons learned.
So that as you exit from this course and tackle other courses or projects
of your own, you have all the more of a mental model
and all the more of a toolbox with which to solve those same problems.
If we think back now just a few weeks ago,
it was probably in our focus on functions and variables
that you first started struggling.
But now in retrospect, if you look back at those problems
and those same problems sets, odds are those same problems
would come all too easily to you now.
Conditionals was the next step in the class, wherein
we gave you the ability to ask questions and get answers
and therefore do things conditionally in your code.
We came full circle today.
And you can see that you can now use those same kinds of conditionals
now to do fancier things with list comprehension and dictionary
comprehensions and the like.
Loops, of course, have been omnipresent now for weeks, including today
as we built up those same structures.
And of course, something can go wrong.
And exceptions and exception handling was our mechanism
for not only catching errors in code but also raising your own exception.
So that if you're laying the foundation to write code for other people,
as in the form of libraries, you can do that too.
Libraries, of course, are things you can not only use but now write on your own,
be it a small module or whole package of code
that you want to share with others around the world.
And even better, can you write tests for your own code, for your libraries,
for other's code as well.
So that ultimately, you can be all the more confident
that not only your code is correct today.
But if you make a change to your code tomorrow,
you haven't broken anything, at least according to your tests
if they continue to pass.
File I/O though, meanwhile, was a way of now storing
data not just in the computer's memory like all of these sheep,
but actually storing things persistently, longer term to disk,
being in a CSV or something more like a binary file like an image.
With regular expressions, you then had the ability
to express patterns and actually validate data or extract data
from information.
All the more of a useful technique nowadays when so much of the world
is trying to analyze and process data at scale, some of which
might in fact be quite messy from the get-go.
And then, of course, most recently, object-oriented programming,
an opportunity to solve the same kinds of problems
but with a slightly different perspective, a way
to encapsulate and to represent real world entities, this time in code.
And today, of course, et cetera with so many other tools
that you can add that didn't necessarily fall under any of those earlier
umbrellas but are useful functions, and data types,
and techniques just to have, again, in your back pocket
as yet other mechanisms for solving problems as well.
Not just putting everyone to sleep, but I thought another way to end
might be a little more vocally to try writing one final program together,
this one using a library we've seen in the past, as well as one other.
I've taken the liberty of installing a text to speech library on my computer
here.
And I'm going to go ahead, perhaps, and open a new file here called say.py
in VS Code.
And I'm going to go ahead here and first import our own friend, import cowsay.
And I'm going to import this new library here, import pyttsx3,
the Python text to speech library.
And now, per its documentation, which I read in advance,
I'm going to go ahead and create a variable for myself here,
engine equals pyttsx3 init to initialize that library for text to speech.
I'm going to then ask the user, well, what do I want to hear spoken?
And I might do something like this.
A variable called this equals the return value of input.
What's this shall be my simple question.
And I'm going to keep it this time as a string.
We've seen how to use cowsay.
We can do cowsay.cow of this.
Turns out this new library can allow me to use its own engine
to say this as well.
But then, ultimately, I'm going to have to run the engine.runAndWait,
just in case it's a long phrase or sentence to be said.
But that's it.
In just eight lines of code, not only am I apparently going to have a cow
appear on the screen to close us out now but also some synthesized text.
Ultimately then we hope with this course that you've not only learned Python,
that you've not only learned programming,
but you've really learned how to solve problems,
and ultimately how to teach yourself new languages.
Funny enough, I myself only learned Python just a few years ago.
And even though I certainly went through some formal documentation and resources
online, I mostly learned what I know now and even what
I had to learn again for today by just asking lots of questions,
be it of Google, or friends, who are more versed in this language than I.
And so, having that instinct, having that vocabulary [INAUDIBLE]
which to ask questions of others to search for answers to questions,
you absolutely now have enough of a foundation in Python
and programming to go off and stand on your own.
So you can certainly-- and you're welcome and encouraged to go
on and take other courses in Python and programming specifically.
But better still, as quickly as you can, is
to find some project that's personally of interest that
uses Python or some other language.
Because at least from my own experience, I tend to learn best
and I hope you might too by actually applying these skills.
Not to problems in the classroom, but really truly
to problems in the real world.
Allow me with all that said to look at my full screen terminal window here.
Run Python of say.py, crossing my fingers one final time in
hopes that I've not made any mistakes or bugs.
And here we go.
Python of say.py prompting me, what's this?
How about we end on this note here.
COMPUTER: This was CS50.