Writing pure functions
A function with no side effects fits the pure mathematical abstraction of a function: there are no global changes to variables. If we avoid the global statement, we will almost meet this threshold. To be pure, a function should also avoid changing the state mutable objects.
Here's an example of a pure function:
def m(n: int) -> int:
return 2**n-1
This result depends only on the parameter, n. There are no changes to global variables and the function doesn't update any mutable data structures.
Any references to values in the Python global namespace (using a free variable) is something we can rework into a proper parameter. In most cases, it's quite easy. Here is an example that depends on a free variable:
def some_function(a: float, b: float, t: float) -> float: return a+b*t+global_adjustment
We can refactor this function to turn the global_adjustment variable into a proper parameter. We would need to change each reference to this function, which may have a large ripple effect through a complex application. A function with global references will include free variables in the body of a function.
There are many internal Python objects that are stateful. Instances of the file class and other file-like objects, are examples of stateful objects in common use. We observe that some of the commonly used stateful objects in Python generally behave as context managers. In a few cases, stateful objects don't completely implement the context manager interface; in these cases, there's often a close() method. We can use the contextlib.closing() function to provide these objects with the proper context manager interface.
We can't easily eliminate all stateful Python objects. Therefore, we must strike a balance between managing state while still exploiting the strengths of functional design. Toward this end, we should always use the with statement to encapsulate stateful file objects into a well-defined scope.
We should always avoid global file objects, global database connections, and the associated stateful object issues. The global file object is a common pattern for handling open files. We may have a function as shown in the following command snippet:
def open(iname: str, oname: str): global ifile, ofile ifile= open(iname, "r") ofile= open(oname, "w")
Given this context, numerous other functions can use the ifile and ofile variables, hoping they properly refer to the global files, which are left open for the application to use.
This is not a very functional design, and we need to avoid it. The files should be proper parameters to functions, and the open files should be nested in a with statement to assure that their stateful behavior is handled properly. This is an important rewrite to change these variables from globals to formal parameters: it makes the file operations more visible.
This design pattern also applies to databases. A database connection object should generally be provided as a formal argument to an application's functions. This is contrary to the way some popular web frameworks work: some frameworks rely on a global database connection in an effort to make the database a transparent feature of the application. This transparency obscures a dependency between a web operation and the database; it can make unit testing more complex than necessary. Additionally, a multithreaded web server may not benefit from sharing a single database connection: a connection pool is often better. This suggests that there are some benefits of a hybrid approach that uses functional design with a few isolated stateful features.