What Does It Mean to "Call" a Function in Python

What does it mean to call a function in Python?

When you "call" a function you are basically just telling the program to execute that function. So if you had a function that added two numbers such as:

def add(a,b):
return a + b

you would call the function like this:

add(3,5)

which would return 8. You can put any two numbers in the parentheses in this case. You can also call a function like this:

answer = add(4,7)

Which would set the variable answer equal to 11 in this case.

How are we able to call functions before they are defined in Python?

You can't call a function before it's defined. disp_result isn't called until increment_5 is called.

For example, this will fail:

def increment_5(num):
number = 5
number += num
disp_result(num)

increment_5() # -> NameError: name 'disp_result' is not defined

def disp_result(num):
print(f"5 was increased by {num}")

While this will succeed:

def increment_5(num):
number = 5
number += num
disp_result(num)

def disp_result(num):
print(f"5 was increased by {num}")

increment_5(4) # -> 5 was increased by 4

What does - mean in Python function definitions?

It's a function annotation.

In more detail, Python 2.x has docstrings, which allow you to attach a metadata string to various types of object. This is amazingly handy, so Python 3 extends the feature by allowing you to attach metadata to functions describing their parameters and return values.

There's no preconceived use case, but the PEP suggests several. One very handy one is to allow you to annotate parameters with their expected types; it would then be easy to write a decorator that verifies the annotations or coerces the arguments to the right type. Another is to allow parameter-specific documentation instead of encoding it into the docstring.

What issues can arise when you call a function within a function?

Well, you may get a stack overflow!

When you call a new function, some information about it is saved in memory. While it runs, its local variables are saved there as well. This structure is called a "stack frame". When you call a function within a function, the stack frames that describe the caller stay there since control must (or at least is expected to) return to the caller at some point (there are techniques like tail-call optimisation to prevent that, but they don't apply to most cases), so the deeper you go into recursion, the more stack frames are pushed onto the stack.

It may happen that you'll run out of memory solely because of the excessive amount of stack frames on the stack, which is known as a stack overflow, which causes your program to crash.

As an example, I once wrote a recursive function that kept crashing my Python interpreter because it was running in an environment which was very low on memory. Once I removed one single local variable from said function, it stopped crashing. As you can see, sometimes one local variable (that's copied over and over again in new stack frames) can make a difference.

Is there a way to do some pre-process/post-process every time I call a function in python

This is typically done with a context manager.

import contextlib

@contextlib.contextmanager
def with_preparation():
prepare()
yield
done()

with preparation():
xyz.foo(<args>)

with preparation():
xyz.bar(<args>)

with preparation():
xyz.foobar()

preparation defines a function that returns a context manager. The with statement works by invoking the context manager's __enter__ method, then executing the body, then ensuring that the context manager's __exit__ method is invoked before moving on (whether due to an exception being raised or the body completing normally).

contextlib.contextmanager provides a simple way to define a context manager using a generator function, rather than making you define a class with explicit __enter__ and __exit__ methods.


You mentioned you need this for every function in a particular module. Without exact details about the module, this may not be entirely correct, but you might be able to build up on it.

class XYZWrapper:
def __getattr__(self, name):
# Intentionally let an AttributeError propagate upwards
f = getattr(xyz, name)
def _(self, *args, **kwargs):
prepare()
return f(*args, **kwargs)
done()
setattr(XYZWrapper, name, _)
return _

prepared = XYZWrapper()

prepared.foo(<args>)
prepared.bar(<args>)
prepared.foobar()


In short, any attribute access on the XYZWrapper instance tries to find an identical attribute on the xyz module, and if successful, defines a wrapper that calls prepare() and done() as needed and patches the XYZWrapper instance with the new wrapper.



Related Topics



Leave a reply



Submit