# As explained earlier, closures happen when a function refers to a # local variable of another function. This is really only relevant # in languages: # # 1. with lexical scoping – variables are looked up syntactically, # in the surrounding «text» (i.e. in a surrounding function, or # a block, etc.) and not on the «execution stack» (that would be # dynamic scope), # 2. where functions are first class entities – that is, we can return # them from other functions, bind them to local or global names, # or accept them as parameters, etc., # 3. in which functions can be defined in local scopes (i.e. other # than global/module and class scopes). # # A language with all the above properties (e.g. Python) will # naturally gain lexical closures, at least unless they are # specifically forbidden. If we look away from implementation # details, it is clear why this must be so: from typing import Callable Writer = Callable[ [ int ], None ] Reader = Callable[ [], int ] # Let us implement a simple ‘machine’ for keeping a median # (upward-biased, for simplicity) of a sequence, without exposing # the sequence in any way. We will call it a ‹median_logger›. The # idea is to only use the features from the above list and derive # closures. def median_logger() -> tuple[ Writer, Reader ]: # When ‹median_logger› executes, the first thing it does is # «create» a new object – an empty list – and bind it to a local # name, ‹items›. items: list[ int ] = [] # Now we define a function that adds a value to the list. We can # define a function here as per (3) and we can refer to ‹items› # because it is in the lexical scope, as per (1). Do keep in # mind that ‹items› is bound «during execution» of # ‹median_logger›: it starts existing after the function is # entered, and stops existing when it is left.¹ def writer( value: int ) -> None: items.append( value ) # And another function to pull out the median (but nothing # else). Again, ‹items› is in the lexical scope, so we can refer # to it. It is the same ‹items› as above, used by ‹writer›, # because we are in the same scope. def reader() -> int: items.sort() return items[ len( items ) // 2 ] # Since functions are first-class objects, per (2), we can # return them. So we do: return writer, reader # For reference, let's add a very simple function which binds an # empty list the same way, but returns the bound value directly # (please excuse the redundancy): def make_list() -> list[ int ]: items: list[ int ] = [] return items def test_median_logger() -> None: # demo # It should be obvious, but let's triple check that value (cell) # construction in functions behaves the way we expect it to: l_1 = make_list() l_2 = make_list() assert l_1 is not l_2 l_1.append( 1 ) assert l_1 != l_2 # Now with that sorted, let's get back to ‹median_logger›. Like # above, let us make two ‘copies’ of whatever the function # returns. This is interesting, because normally you would # expect a function to be only defined once (and the function # ‘body’ actually is) and the returned value to be the same. w_1, r_1 = median_logger() w_2, r_2 = median_logger() # But alas, it is not: the behaviour aligns with ‹make_list›, # which is just as well. While an implementation detail, we can # also check that the ‘bodies’ actually «are» the same. assert w_1 is not w_2 assert w_1.__code__ is w_2.__code__ # Let's check the behaviour. We expect that ‹w_1› and ‹r_1› # internally share the same list, i.e. adding elements using # ‹w_1› will influence the result of ‹r_1›. And this is so: w_1( 5 ) assert r_1() == 5 w_1( 2 ) w_1( 3 ) assert r_1() == 3 # While ‹w_1› is entirely independent from ‹r_2› and vice-versa: w_2( 10 ) w_2( 10 ) assert r_2() == 10 # and not 5 assert r_1() == 3 # also not 5 # If the above is clear, we can have a little peek at the # internals. First, we check that it is the ‘captures’ that are # different between ‹w_1› and ‹w_2›: assert w_1.__closure__ is not w_2.__closure__ # And also that they are actually the same between ‹w_1› and # ‹r_1›, even though those have different bodies: assert w_1.__code__ is not r_1.__code__ assert len( w_1.__closure__ ) == 1 assert len( r_1.__closure__ ) == 1 assert w_1.__closure__[ 0 ] is r_1.__closure__[ 0 ] # ¹ Except it doesn't, because it is captured. But normally, that's # exactly what would happen. if __name__ == '__main__': test_median_logger()