Python Functions as Objects

Python Functions as Objects

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.. πŸ‘‹

Β