Closures: replacing classes with functions
Sometimes we define a class with only one method (except _init _ ()), which can be replaced by using closures. A closure is an inner function surrounded by an outer function. It can obtain variables in the scope of the outer function (even if the outer function has been executed). Therefore, closures can hold additional variable environments for use in function calls. Consider the following example, which allows users to obtain URL s through some template scheme.
from urllib.request import urlopen class UrlTemplate: def __init__(self, template) -> None: self.template = template def open(self, **kwargs): return urlopen(self.template.format_map(kwargs)) yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}') for line in yahoo.open(names='IBM,AAPL,FB', fields = 'sllclv'): print(line.decode('utf-8'))
This class can be replaced by a simple function:
def urltempalte(template): # Later, when you call the function again with an object, you pass in kwargs def opener(**kwargs): return urlopen(template.format_map(kwargs)) return opener yahoo = urltempalte('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}') for line in yahoo(names='IBM,AAPL,FB', fields = 'sllclv'): print(line.decode('utf-8'))
In many cases, we use classes with only a single method because we need to save additional state for class methods. The only purpose of the UrlTemplate class we mentioned above is to save the value of the template somewhere and then use it in the open() method. Using closures to solve this problem will be shorter and more elegant. We use the opener() function to remember the value of the parameter template, and then use this value in subsequent calls.
Therefore, when you need to add additional state to the function in writing code, you must consider using closures.
Accessing variables defined within a closure
We know that the inner layer of closure can be used to store the variable environment required by the function. Next, we can extend the closure through functions so that the variables defined in the inner layer of the closure can be accessed and modified.
Generally speaking, the variables defined in the inner layer of the closure are completely isolated from the outside world. If you want to access and modify them, you need to write access functions (accessor function s, i.e. getter/setter methods) and attach them to the closure as function properties to provide access support for inner variables:
def sample(): n = 0 # Closure function def func(): print("n =", n) # Accessor function, i.e. getter/setter method def get_n(): return n def set_n(value): # You must add nolocal to modify the inner variable nonlocal n n = value # Attach as function attribute func.get_n = get_n func.set_n = set_n return func
The test results of the algorithm are as follows:
f = sample() f() # n = 0 f.set_n(10) f() # n = 10 print(f.get_n()) # 10
As you can see, get_n() and set_n() works much like an instance method. Be sure to put get_n() and set_n() is attached as a function attribute, otherwise set is called_ N () and get_n() will report an error: 'function' object has no attribute 'set_n'.
If we want the closure to be completely simulated as a class instance, we need to copy the inner function of the architecture into the dictionary of an instance and then return it. Examples are as follows:
import sys class ClosureInstance: def __init__(self, locals=None) -> None: if locals is None: locals = sys._getframe(1).f_locals # Update instance dictionary with callables self.__dict__.update( (key, value) for key, value in locals.items() if callable(value) ) # Redirect special methods def __len__(self): return self.__dict__['__len__']() # Example use def Stack(): items = [] def push(item): items.append(item) def pop(): return items.pop() def __len__(): return len(items) return ClosureInstance()
The corresponding test results are shown below:
s = Stack() print(s) # <__main__.ClosureInstance object at 0x101efc280> s.push(10) s.push(20) s.push('Hello') print(len(s)) # 3 print(s.pop()) # Hello print(s.pop()) # 20 print(s.pop()) # 10
The function of class with closure model is faster than that of traditional class implementation methods. For example, we use the following class as a test comparison.
class Stack2: def __init__(self) -> None: self.items = [] def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def __len__(self): return len(self.items)
Here are our test results:
from timeit import timeit s = Stack() print(timeit('s.push(1);s.pop()', 'from __main__ import s')) # 0.98746542 s = Stack2() print(timeit('s.push(1);s.pop()', 'from __main__ import s')) # 1.07070521
You can see that the version with closures is about 8% faster. Because for the instance, most of the test call cost is on the access to the instance variables, and the closure is faster because no additional self variables are involved.
However, this trick should be used with caution in the code, because compared with a real class, this method is actually quite strange. Features such as inheritance, properties, descriptors, or class methods are not available in this method. Moreover, we also need some tricks to make the special methods work properly (for example, our implementation of _len_ () in ClosureInstance above). However, this is still a very academic example, which tells us what the access mechanism provided inside the closure can do.
reference
- [1] https://www.python.org/
- [2] Martelli A, Ravenscroft A, Ascher D. Python cookbook[M]. " O'Reilly Media, Inc.", 2005.