PROFESSOR: Hi.
Today I'd like to talk to you about some notable aspects of
Python that you'll encounter in the 6.01 software, as well
as in the general sense when working with Python.
A lot of these little tidbits of Python have some
interesting history associated with them, especially related
to the history of computer science.
And also, I want to indicate some things that tend to mess
up, especially, first time programmers, but also
especially people that are new to Python.
First, I'd like to bring us back to last week.
We were talking about object-oriented programming
and inheritance.
Object-oriented programming is a programming paradigm.
It's in the same category as imperative programming or
functional programming.
And you might say I have a good sense of the fact that in
object-oriented programming, everything is an object.
But I don't really have a good sense of what constitutes
imperative programming or functional programming.
I'm going to focus on functional programming right
now, because it has more of a historic root in the academic
development of computer science.
Functional programming is like object-oriented programming in
that everything is a function.
It refers to the idea that you want to write as much of your
code as possible in a purely functional manner.
And in particular, you're looking for the ability to
avoid things like side-effects or enable many different kinds
of evaluation.
Those kinds of things are not the subject of this course,
but I think they're worth noting to figure out why we're
bothering learning about things like lambdas and list
comprehensions in the first place.
Speaking of which, the first thing we need to talk about
before we even talk about things like lambdas and list
comprehensions, is the concept that in Python,
everything is an object.
But because functions are first-class objects, we can
also treat Python to a large extent like a functional
programming language.
What do I mean when I say functions
are first class objects?
I mean that a function can be the return
value of another function.
I mean that the function can be passed in as an argument to
a function.
And I mean that functions can be assigned variable names and
manipulated the same way that we manipulate any other piece
of data structure.
This is important, because if you want higher order
functions or functions that actually modify or use other
functions as a part of you know, whatever it is they do,
then you need to be able to interact with the function
like it's any other object, in part pass it in
or return it out.
Let's look at an example.
So let's say I start off with a very basic function, right?
If you want to square some sort of value, probably
numeric in this case, then all you have to do is
multiply it by itself.
Pretty simple.
Good place to start.
As a consequence of the idea that functions are first class
objects, I can write a function that takes in a
function as an argument and then develops a return
function that uses the function that I
passed in, on itself.
So any arguments that I would pass to someFunction, I would
pass into someFunction, take the return value of this
function call, and then pass that into someFunction again.
That's what returnFunction does.
And down here, I return returnFunction.
Note that the object type of this
return value is a function.
So once I have returnFunction, I can actually pass it
arguments, have them run through someFunction, not once
but twice, and then get out a value that's of the return
type of someFunction.
Note that I made some assumptions while
writing this function.
First, I've assumed that someFunction returns out the
same number of arguments, and either accept an arbitrary
number of arguments or accepts the same number of arguments
as it puts out.
The other assumption that I've made is that the data type
that someFunction returns is the same as the data types
that someFunction accepts.
Or the things that someFunction does to its
arguments can be done to multiple kinds of arguments.
But that's a whole different argument.
Right now, we're just focusing on the fact that we pass in
some arbitrary number of arguments, call someFunction
on it, call someFunction again on the return value of this,
and return that as something you can do to whatever it is,
someFunction operates on.
OK.
Let's review an example, because I promise it'll be
more clear.
Let's say f is going to be the
composition, a square on itself.
If I do that, I end up with a function that operates on an
arbitrary number of arguments--
that's not true.
I end up with a copy of square that takes in the same number
of arguments as square called on itself.
So here a square is substituted for someFunction.
Here a square is called on that call of square.
And here is what f is assigned to.
So when I call f of 2, I'm going to
substitute 2 in for args.
I'm going to call square on args.
If I call square on args, I get out 4.
And if I call square on args again, or if I call square of
args where args is defined as the return value of this, then
I'm going to call square on 4, I'll square 4,
and I'll get 16.
Take a second to type it into IDLE, and make it make sense
to yourself.
All of this code should compile.
Mess around with the parameters, if you're having
trouble convincing yourself that it does.
Ok.
Now I think we're ready to talk about lambdas.
If you can pass in functions as arguments or return them as
values or assign them to variable names and just treat
them like any other data type, then you should be able to
treat them as some sort of raw value.
And that's where lambdas come in.
Lambda is a key word in Python that tells you you're about to
use a function that you have not given any sort of name or
defined in any place in your code or environment
beforehand.
You're just going to start talking about something that
you would like to do to a given number of arguments, and
then what you want to return out.
Lambda has roots in lambda calculus which, if you're
familiar with the history of computer science, you've
probably heard of.
Now might be a good time to look up lambda calculus, if
you've never heard it before or possibly Alonzo Church.
But it's still available in code today, which is really
cool, and speaks to the continuing power or at least
recognition of importance of functional programming.
As I said before, the idea with lambda is that you could
write an anonymous function.
Over here, in order to write a function that's squared, I had
to write out a define line.
And because Python uses indentation, because in
Python, indentation carries meaning, I'd have to enter and
return over to a next line--
I'm actually not sure if that's strictly true.
I think you can do def square x, return
x, but I'm not positive.
I've only seen it written like this.
And I think it's because in the general sense, people try
to respect things like convention and
readability in Python.
If you're using lambda, people already know that you want to
describe a function really quickly.
Or you want to describe function without
assigning it any name.
Therefore, the two most common uses you'll see of lambda is
when you want a really fast function and you don't want to
spend extra time or lines writing out that function.
It's pretty clear what this is going to do.
Or if, in a particular sense, you need
an anonymous function.
You don't want to assign a name or memory space
associated with that function.
Note instead of using square, I can just write out this.
So I saved two lines of code defining square.
And then I don't have to refer back to some earlier part of
the code in order to understand
what this line does.
Pretty cool.
All right.
So there's functions of first class objects.
There's lambda.
Let's talk about how to use lambdas on lists.
In Python, you'll end up doing a lot of list manipulation.
One of the best uses of anonymized functions in any
functional programming language is the ability to
manipulate lists in a line.
If I've defined just a pretty straightforward list right
here, I can use functions like map filter and reduce to--
in one line--
take a list, apply a function to every element in that list,
and then return a copy of the result of that list.
I didn't have to write a separate function to do the
list handling.
I didn't have to write a separate function to multiply
the list by two.
And I've communicated what I'm doing effectively to people
that are used to functional programming.
So if you type this line in, you should end up with a
return in interactive mode of every element in this list
multiplied by two.
Now's a good time to try.
Once you've done that, I also recommend looking at the
original copied demolist.
Note that this is unaltered.
Map actually returned a new data structure that represents
performing this function on this list.
This becomes important later when I explain aliasing, but I
will do that in about two minutes.
If we want to get even more elegant, we can use list
comprehensions.
List comprehensions--
if you ask somebody where list comprehensions are from,
they'll probably say Haskell, because that is the most
popular use of list comprehensions, in terms of
pure lazy functional programming.
But it actually goes back further than that, and is in
Small Talk, and then something from the '60s.
I can't remember the name of it right now.
They're really nice because they borrow a syntactic
approach from mathematicians.
This looks a lot like set notation.
And when you read this, you can probably tell that the
list listComprehension is going to describe a set of
points from 1 to 4.
But you accomplish that in possibly one line of code.
I've written it on three here because I wanted to keep it in
this space.
But it's really concise.
If you type this into IDLE, you should see the kind of
list that it returns, and also what's
listComprehension is now--
what now constitutes listComprehension.
You can use listComprehensions with functions like map and
filter and reduce.
And, along with the anonymized functions, all of these tools
provide you with a lot of functionality in a very short
amount of space.
It's also good to be able to recognize what's going on when
you see something like this or something like this.
Because you'll probably run into it in, in particular, a
lot with Lisp code, but also in the artificial intelligence
community in particular.
Ok.
We've talked about all that.
Let's take a second to talk about the fact that lists are
actually mutable, and what that means, and what things
you have to be careful about if you're going to be working
with mutable objects.
If you're new to programming, or you're new to Python,
you've probably already worked with some of
these data types, right?
Any numbers, any strings, any tuples,
are going to be immutable.
What that means is if you have a variable and you have a
second variable that has a different assignment line
associated with it-- right?
I haven't assigned g to h here.
I've just signed g to "hello" and h to "hello."
If you look at the space in memory that Python associates
with both objects, they point to the same place.
This is the definition of an immutable object.
When g points to "hello" and h points to "hello," they both
point to the same place.
If x points to 5 and y points to 5, they're both pointing to
the same place.
If you point x at 6, it now points to a
different memory address.
This gets confounded when you're talking
about mutable objects.
The problem with mutable objects is that you select a
memory space to contain the object or memory ID in Python
to contain the object.
And then that object is changed in place.
You can use this to your advantage but it can
also mess with you.
And here's how.
Let's say I assigned a to a small list.
And I also assigned b to a.
b and a now point at the same place.
If I manipulate b, and I'm--
excuse me-- still working with an immutable object, or excuse
me, if I'm still working with a mutable object, the object
in that memory address has been altered.
And b and a still point to the same place in memory.
So if I look at a, it's going to look like b, which is going
to look like 1, 2, 3, 4 --
which is not what I assigned a to originally.
Again, can be powerful, but you have to keep it in mind.
And it might start to mess with, in particular, the 6.01
software when you're dealing with state machines.
In order to get around this, you can create a copy of the
list and then modify the copy.
This is actually what map does.
If you want to do it, the easiest way to do it on the
first layer is to specify the index into the
list with no bounce.
It'll copy the whole thing.
There's also a Python library copy or deep copy, if you want
to copy lists of lists of lists of lists of lists.
And just to clarify, you'll note that after you assign c
to a copy of a, c and a occupy different memory places.
So if you modify c, a will retain its original value.
I think that's all the notable things I have
to say about Python.
and gives us enough power to do some really powerful list
manipulation or array manipulation, and covers what
we want to say about functional programming before
we get into the notion of state, which I'll
talk about next time.