Holding references on classes and functions
Insert yourself a joke here, I'm not inspired today
Summary
Functions and classes are objects in Python. You can assign them to variables, pass them as parameters and return them from function calls.
Of course, this implies you can put them in collections too, such as lists or dicts. And that you can create them dynamically.
While it's not something you will do every day, mastering that part of Python will help you read your favorite libraries source code.
The many ways to create variables
If you ask any dev how to create a variable in Python, the first answer would be an assignment:
animal = "ghoti"
However there are many other ways to create a variable in Python.
The first one is, surprisingly, the import statement:
import datetime
This will set the variable datetime
in the current namespace, and make it hold a reference to the module object loaded from "datetime.py" in the stdlib.
You can use the variable to access the module content:
>>> print(datetime.date.today())
2023-12-20
But as with any other variable, you can assign its content to another one, and even delete the original one:
>>> still_contains_datetime = datetime
>>> del datetime
>>> datetime
NameError: name 'datetime' is not defined
>>> print(still_contains_datetime.date.today())
2023-12-20
Another way to create a variable is def
. When you use this keyword, you effectively create a function object, then you assign said object to the variable. You can then use the variable to call the function:
>>> def a_variable_that_contains_a_function():
... print("The function runs")
...
>>> a_variable_that_contains_a_function()
The function runs
Just like with modules, you can assign the function to another variable, no biggie:
still_contains_the_function = a_variable_that_contains_a_function
Note what we DON'T DO. We don't use parenthesis:
still_contains_the_function = a_variable_that_contains_a_function() # NO
That's because we don't call to execute the function. We want to take the content of the variable a_variable_that_contains_a_function
and assign it to still_contains_the_function
.
Once it's done, you can use the function from the other variable:
>>> still_contains_the_function()
The function runs
And you can even delete the original variable, the function is still there:
>>> del a_variable_that_contains_a_function
>>> still_contains_the_function()
The function runs
In fact, if you ask Python what object it contains, you will see the name of the function is still the original one:
>>> still_contains_the_function
<function a_variable_that_contains_a_function at 0x7f17f2252290>
The variable a_variable_that_contains_a_function
is gone, but the function object holds the original name in a __name__
attribute:
>>> still_contains_the_function.__name__
'a_variable_that_contains_a_function'
Because in Python, function are objects, not just a name on a piece of code. So they get attributes and methods as well. Everything in Python is an object. It's weird to think of functions as objects, but yes, functions really do have methods! Look at this:
>>> still_contain_the_function.__hash__()
8733800285136
Functions are very special objects you can pass arguments to, to trigger code, and get back a return value.
And as objects, you can put them in variables. This has some interesting applications.
References on functions
Putting functions in other variables is of limited use. However, if you can do this, you can use the reference to the function in many, many other places.
First, you can put functions in a list, a tuple, a set or a dict:
>>> def step_1():
... print("Collect underpants.")
...
... def step_2():
... print("...")
...
... def step_3():
... print('Profit!')
...
... steps = [step_1, step_2, step_3] # no parenthesis!
...
... for step in steps:
... step() # we call the functions here
...
Collect underpants.
...
Profit!
This is used in many design patterns to queue commands dynamically, create event systems, or any kind of things requiring associating a state with code to run.
In fact, for a long time, Python didn't have match
/ case
, and never had a switch
, so putting functions in a dict was a common way to choose what function to call depending of the value of a key.
More commonly, though, references to functions will be used for callbacks. Meaning passing a function as a parameter to another function, so that it can be called when something happens inside. The canonical example is sorting, and we also discussed it in sculpting a function, but in short:
>>> def i_accept_another_function(a_function_as_a_param):
... print("Start")
... a_function_as_a_param() # we call code from outside
... print("End")
...
... def something_i_want_to_do_in_the_middle():
... print('In the middle')
...
... def something_else():
... print('Malcolm')
...
>>> i_accept_another_function(something_i_want_to_do_in_the_middle)
Start
In the middle
End
>>> i_accept_another_function(something_else)
Start
Malcolm
End
This technique enable any function to say "I don't know how to do this part, please give me the code to do it".
Here i_accepts_another_function
knows how to start, and end, but not what to do in the middle. So it expects another function as parameter, and call it in the middle. It's a way to delegate part of your behavior to the rest of the world, to be changed dynamically. It's part of a general concept called "dependency injection". One day, I will make a full design pattern course to explain things like this in details.
If you can pass a function as a parameter, you can also return a function.
Because you can create a function inside another function, dynamically. And return it. And hold the result in a variable.
Fancy stuff:
def create_speak(mode="normal"):
if mode == "scream":
def speak(sentence):
print(sentence.upper() + "!!")
elif mode == "whisper":
def speak(sentence):
print(sentence.lower() + "...")
else:
def speak(sentence):
print(sentence)
return speak
This function creates another function. But which one it creates depends of the parameter it receives.
>>> speak = create_speak("whisper")
>>> speak("Hello")
hello...
>>> speak = create_speak("scream")
>>> speak("Hello")
HELLO!!
This is an advanced topic, and you don't really need to know that to be productive in Python. But the next article will be about decorators, and we will use this :)
References on classes
As I said almost everything is an object in Python.
And if you have done a little Object Oriented Programming, you'll know in this language, we create objects from classes:
>>> class Car(): # the blueprint
... pass
...
>>> toyota = Car() # the object created from the blueprint
>>> toyota
<__main__.Car object at 0x7f17f025c3a0>
But classes...
Well...
They are objects.
Wait, what?
Well, yes, everything is an object. Functions are objects. And so are classes.
In fact, the keyword class
is just another way to create a variable in Python, but this time, the variable contains a class object. Look, Car
is just another regular variable, and you can reassign it:
>>> Vehicule = Car
>>> del Car
>>> Vehicule()
<__main__.Car object at 0x7f17f02657e0>
So there is something in Car
, and if it's in a variable, it's an object.
It's a very special object, like functions are special objects too. It's an object that has the power to create other objects.
So in Python, you use class
to create a special object, the class, that serves as the blueprint to create a lot of regular objects.
It's why I hate the word "object", because when something means "everything", it effectively means nothing.
Now you may wonder, if classes create objects in Python, but classes are objects, what's the stuff that creates those super special classes object?
This stuff is what we call metaclasses, but honestly, there is no rush for you to learn about that one. I'll write about it in a year, maybe.
For now, you can just remember, classes are objects, and you can store them in variables.
In fact, just like any objects, you can store classes in lists, pass them as parameters to functions, and return them.
Which means you can create classes dynamically too.
Again, you probably won't do it much. Meta-programming is not a common task.
But we will see class decorators in another article soon, and that knowledge will be useful.