The short answer is yes. The real question is how.
The reason why this topic is interesting is that when we talk about closures, we usually think of a single function nested inside an enclosing function. This inner function becomes the return value of the enclosing function and, when called later, still has access to the enclosing function’s local variables.
However, there is nothing preventing us from defining not just one, but multiple functions within the enclosing function. In such cases, the enclosing function returns not just a single function, but a sequence of nested functions, and are most often passed to the caller as a tuple.
To make this clearer, let’s look at an example.
Suppose we want to model a simple bank account system. To do this, we need to be able to open an account with a unique account number, deposit a given amount, withdraw an amount (if the balance allows), and check the balance. This requires implementing four operations: opening an account, depositing, withdrawing, and checking the balance.
As is usually the case, there are several ways to solve this problem. Typically, we would define a class with methods for opening an account, depositing, withdrawing, and checking the balance. The bank account data (account number and current balance) is stored in a data attribute that references an appropriate container.
Here, however, we take a different approach and implement the operations using closure functions nested inside a single enclosing function. Since these closure functions are defined within the same enclosing function, they all have access to the dictionary created as a local variable of the enclosing function, which maps account numbers to balances.
The function definition and its usage are shown below, with the latter presented in two ways. In the first case, the four closure functions are assigned to separate variables. In the second version, since the functions are logically related, they are implemented as attributes of a namedtuple. The results are, of course, the same in both cases given the same input.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
from collections import namedtuple def bank_account_management(): """ This enclosing function creates and returns a set of closure functions that operate on a shared state (the 'accounts' dictionary). The dictionary itself is local to this function, but remains accessible to the returned inner functions via closure. """ # This dictionary stores account number–balance mappings. # It is NOT accessible directly from outside this function. accounts = {} def open_account(account_number): """ Create a new account with the given account number. If the account already exists, this operation leaves it unchanged. """ accounts.setdefault(account_number, 0) def deposit(account_number, amount): """ Increase the balance of the given account by 'amount'. Returns the deposited amount if the account exists, otherwise returns 0. """ if account_number in accounts: accounts[account_number] += amount return amount return 0 def withdraw(account_number, amount): """ Decrease the balance of the given account by 'amount', but only if sufficient funds are available. Returns the withdrawn amount on success, otherwise 0. """ if account_number in accounts: if (balance := accounts.get(account_number)) >= amount: accounts[account_number] = balance - amount return amount return 0 def get_balance(account_number): """ Return the current balance of the given account. If the account does not exist, return 0. """ if account_number in accounts: return accounts.get(account_number) return 0 # The enclosing function returns all inner functions together. # They share access to the same 'accounts' dictionary. return open_account, deposit, withdraw, get_balance # ========================= # TEST / USAGE EXAMPLES # ========================= # 1) Using the returned closure functions as independent variables. open_acc, deposit_to_acc, withdraw_from_acc, acc_balance = bank_account_management() # 2) Grouping the same functions into a named tuple for clearer structure. Bank = namedtuple('Bank', 'open_account deposit withdraw get_balance') bank = Bank(*bank_account_management()) # Sample input data: account numbers and initial deposits. account_numbers = (111, 222) deposits = (10000, 250000) # --- Using standalone closure functions --- print("Using individual closure functions:\n") for account_number, deposit_amount in zip(account_numbers, deposits): open_acc(account_number) print(f'Deposit = {deposit_to_acc(account_number, deposit_amount)}') print(f'Account number: {account_number}, balance = {acc_balance(account_number)}') print(f'Withdrawal = {withdraw_from_acc(account_number, 50000)}') print(f'Account number: {account_number}, balance = {acc_balance(account_number)}') print() # --- Using namedtuple wrapper --- print("Using namedtuple attributes:\n") for account_number, deposit_amount in zip(account_numbers, deposits): bank.open_account(account_number) print(f'Deposit = {bank.deposit(account_number, deposit_amount)}') print(f'Account number: {account_number}, balance = {bank.get_balance(account_number)}') print(f'Withdrawal = {bank.withdraw(account_number, 50000)}') print(f'Account number: {account_number}, balance = {bank.get_balance(account_number)}') print() # Output: # # Using individual closure functions: # # Deposit = 10000 # Account number: 111, balance = 10000 # Withdrawal = 0 # Account number: 111, balance = 10000 # # Deposit = 250000 # Account number: 222, balance = 250000 # Withdrawal = 50000 # Account number: 222, balance = 200000 # # Using namedtuple attributes: # # Deposit = 10000 # Account number: 111, balance = 10000 # Withdrawal = 0 # Account number: 111, balance = 10000 # # Deposit = 250000 # Account number: 222, balance = 250000 # Withdrawal = 50000 # Account number: 222, balance = 200000 |
In this solution, the dictionary holding the account data is completely safe: it cannot be modified or overwritten from outside, not even accidentally. In contrast, this would not be the case if we had used the custom class-based approach, since in Python even variables intended to be private can still be accessed if their names are known.
Despite these advantages, closure-based solutions like this are not very common in everyday practice, for several reasons. On the one hand, the way closures work is not immediately intuitive to everyone. On the other hand, the object-oriented paradigm, with its class-based approach, is by far the most common. Another practical consideration is that if you need to create many objects rather than just a few, a class-based solution may be more appropriate.
But we presented this approach to illustrate that in Python, a wide range of problems can be solved using regular, nested, and closure functions without defining classes. The versatility of functions is why the e-book Python Knowledge Building Step by Step covers functions and their variations in great detail, and only after these chapters—building on that foundation—introduces class definitions and object instantiation and all related topics.