One of the fundamentals of Python is that it treats everything as an object. So variables, lists, tuples, sets, dictionaries, functions - everything is an object in Python.
An object is an instance of a class. In other words, every object belongs to some class. In Python, we can get the class of an object on the __class__
property. Let's check this out in code π»:
language = "Python"
versions = [3.8, 3.9, 3.10]
def install():
pass
print(language.__class__.__name__) # str
print(versions.__class__.__name__) # list
print(install.__class__.__name__) # function
In the above example, we see that the variables and function do belong to some class - which states that they are objects. Let's confirm this using the isinstance()
method:
print(isinstance(language, str)) # True
print(isinstance(versions, list)) # True
Ok. Now we are sure that variables, data structures and functions are objects in Python. But wait.. what about classes? Are they too objects ?!! π€ Let's check:
class Test:
pass
print(Test.__class__.__name__) # type
print(language.__class__.__class__.__name__) # type
print(isinstance(Test, type)) # True
print(isinstance(str, type)) # True
Interesting!! π So, classes are also objects in Python. Every class is an instance of the type
class. Python uses this type class to create other classes, therefore, it is called a metaclass.
π‘ Everything in Python, including classes, are OBJECTS
Now that we have confirmed that a function is an object in Python, let's use it like real objects - which by the way, was the topic for this article! π
State and behaviour of a function
So an object, by its definition is a real-life entity, which has various attributes/data (state) and functions/methods that operate on those data (behaviour). If that's the case, then the function might be having some meta attributes/methods. We can use the dir()
to list all properties and methods of an object in Python.
def square(num=1):
""" Squares the supplied number """
return num * num
print(dir(square))
# ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
# Gets the default values of the arguments as tuple
print(square.__defaults__) # (1,)
# Gets the docstring
print(square.__doc__) # Squares the supplied number
# __call__ makes function object callable
print(square.__call__(2)) # 4
Ok. So function objects do have plenty of attributes and methods already defined on them. But, can we add custom attributes and methods? Let's see:
def upload(data, destination):
print(f"Uploaded data to {destination}")
# Adding attributes
upload.done = False
print(upload.done) # False
# Adding methods
upload.help = lambda : print("Uploads data to anywhere")
upload.help() # Uploads data to anywhere
Ok. That works fine as expected. But what if we were adding the state inside the function definition? π§
def upload(data, destination):
upload.done = True
print(f"Uploaded data to {destination}")
print(upload.done)
AttributeError: 'function' object has no attribute 'done'
upload("stars", "https://github.com/mochatek")
We are adding the attribute inside the function definition, so it will get assigned only at the function call. Since we tried to access it before the function call, it threw the AttributeError exception. So, if our idea is correct, then the done attribute must be accessible after the function call. Let's verify this π§:
def upload(data, destination):
print(hasattr(upload, 'done')) # False
upload.done = True
print(hasattr(upload, 'done')) # True
print(f"Uploaded data to {destination}")
upload("stars", "https://github.com/mochatek")
# Uploaded data to https://github.com/mochatek
print(upload.done) # True
Now that we have explored how a Python function can be treated as a real object, let's do some practice in code: Suppose our upload function uploads data to an API or a database or some other destination, and we want to upload it only once. So basically, we just want the upload function to upload the data in its first call and then simply exit out on successive calls.
YES, we can do it by having a flag variable for the upload state, or we can make use of closure - which gives additional benefit by hiding the state from outside modifications. But let's just keep these better solutions aside and try out the concepts we just explored π»:
def upload(data, destination):
if hasattr(upload, 'done'):
print("Already uploaded. Skipping..")
return
try:
# Perform actual upload
pass
except:
print("ERR: An error occured during upload. Please try again")
else:
upload.done = True
print(f"Uploaded data to {destination}")
upload("stars", "https://github.com/mochatek")
# Uploaded data to https://github.com/mochatek
upload("followers", "https://github.com/mochatek")
# Already uploaded. Skipping..
Can you write a toggle function using the same concept? I know you can. Try it. π«£
Assigning function to a variable
Functions can be assigned to other variables just like any other object in Python. The custom attributes as well as metadata will also be copied while assigning. (There are much more things to discuss on object assignment in Python. We will explore it in detail in a separate article π )
def sum(a, b):
sum.operator = "+"
print(a + b) # 3
add = sum
add(1, 2)
# Metadata is copied
print(sum.__name__, add.__name__) # sum sum
# Custom attributes are also copied
print(sum.operator, add.operator) # + +
Function as an element in a list
Let's see if we can add a function as an element to a list.
def square(num):
return num * num
def cube(num):
return square(num) * num
math_functions = [square, cube]
for func in math_functions:
print(func(2)) # 4 8
Function as the value in a dictionary
Just like any object in Python, a function too can be a value in the dictionary.
# Anonymouse function aka Lambda function
sum = lambda a, b: print(a + b)
def sub(a, b):
print(a - b)
operate = {"+": sum, "-": sub}
operate["+"](4, 2) # 6
operate["-"](4, 2) # 2
Function as an argument to a function OR return value of a function
Python functions can accept another function as its argument and can also return a function as its return value. Such functions that operate with another function are known as Higher-Order Functions. Eg: map
, filter
, decorator
( We will discuss this in detail once we explore decorators and closures π )
# Function returning another function
def shout(dialogue):
return dialogue.upper
# Function taking a function as argument
def say(dialogue, how):
print(how(dialogue)())
say('I Love Python', shout)
Thatβs all for this article. Thanks for reading! π
I hope you liked this article and if you have any questions or suggestions, please write them in the comments. Happy Coding.. π