三种实现有状态函数的方式:
- 基于全局变量:
- 学习成本低,最容易理解;
- 会增加模块级的全局状态,封装性和可维护性最差;
- 基于函数闭包:
- 学习成本适中,可读性较好;
- 适合用来实现变量较少、较简单的有状态函数;
- 创建类来封装状态:
- 学习成本较高;
- 当变量较多、行为较复杂时,类代码比闭包代码更易读,也更容易维护;
Ethan 正在自学 Python,一天,他从网上看到一道和字符串处理有关的练习题:
有一段文字,里面包含各类数字,比如数量、价格等,编写一段代码把文字里的所有数字都用星号替代,实现脱敏效果。
使用 re.sub() 函数:
1 | def mosaic_string(s): |
现在进一步修改函数,保留每个被替换数字的原始长度。
在使用 re.sub(pattern, repl, string) 函数时,第二个参数 repl 不光可以是普通字符串,还可以是一个可调用的函数对象:
1 | def mosaic_match_obj(match_obj): |
请在替换数字时加入一些更有趣的逻辑——全部使用星号 * 来替换,显得有些单调,如果能轮换使用 * 和 x 两种符号就好了。
之前所写的 mosaic_match_obj() 只是一个无状态函数,但为了满足新需求,Ethan 需要调整 mosaic_match_obj() 函数,把它从一个无状态函数改为有状态函数。这里的状态,当然就是指它需要记录每次调用时应该使用 * 还是 x 符号:
1 | _mosaic_char_index = 0 |
使用全局变量保存状态,其实是写代码时最应该避开的事情之一。上面这种方式封装性特别差,代码里的 mosaic_match_obj() 函数不是一个完整可用的对象,必须配合一个模块级状态 _mosaic_char_index 使用。如果多个模块在不同线程里,同时导入并使用 mosaic_match_obj() 函数,整个字符轮换的逻辑就会乱掉,因为调用方共享同一个全局标记变量。
闭包(Closure)是一种允许函数访问已执行完成的其他函数里的私有变量的技术,是为函数增加状态的另一种方式:
1 | def mosaic_match_obj(): |
在一个类(Class)中,状态和行为可以被很好地封装在一起,因此它天生适合用来实现有状态对象:
1 | class CyclicMosaic: |
这个方案最终依赖的 CyclicMosaic.mosaic_match_obj,并非一个有状态函数,而是一个有状态的实例方法,但它们都是可调用对象的一种,都可以作为 re.sub() 函数的 repl 参数使用。