场景:实现有状态函数

三种实现有状态函数的方式:

  • 基于全局变量
    • 学习成本低,最容易理解;
    • 会增加模块级的全局状态,封装性和可维护性最差;
  • 基于函数闭包
    • 学习成本适中,可读性较好;
    • 适合用来实现变量较少、较简单的有状态函数;
  • 创建类来封装状态
    • 学习成本较高;
    • 当变量较多、行为较复杂时,类代码比闭包代码更易读,也更容易维护;

Ethan 正在自学 Python,一天,他从网上看到一道和字符串处理有关的练习题:

有一段文字,里面包含各类数字,比如数量、价格等,编写一段代码把文字里的所有数字都用星号替代,实现脱敏效果。

使用 re.sub() 函数:

1
2
def mosaic_string(s):
return re.sub(r'\d+', '*', s)

现在进一步修改函数,保留每个被替换数字的原始长度。

在使用 re.sub(pattern, repl, string) 函数时,第二个参数 repl 不光可以是普通字符串,还可以是一个可调用的函数对象:

1
2
3
4
5
6
def mosaic_match_obj(match_obj):
length = len(match_obj.group())
return '*' * length

def mosaic_string(s):
return re.sub(r'\d+', mosaic_match_obj, s)

请在替换数字时加入一些更有趣的逻辑——全部使用星号 * 来替换,显得有些单调,如果能轮换使用 * 和 x 两种符号就好了。

之前所写的 mosaic_match_obj() 只是一个无状态函数,但为了满足新需求,Ethan 需要调整 mosaic_match_obj() 函数,把它从一个无状态函数改为有状态函数。这里的状态,当然就是指它需要记录每次调用时应该使用 * 还是 x 符号:

1
2
3
4
5
6
7
8
9
10
11
_mosaic_char_index = 0

def mosaic_match_obj(match_obj):
# 使用 global 关键字声明一个全局变量
global _mosaic_char_index
mosaic_chars = ['*', 'x']
char = mosaic_chars[_mosaic_char_index]
_mosaic_char_index = (_mosaic_char_index + 1) % len(mosaic_chars)

length = len(match_obj.group())
return char * length

使用全局变量保存状态,其实是写代码时最应该避开的事情之一。上面这种方式封装性特别差,代码里的 mosaic_match_obj() 函数不是一个完整可用的对象,必须配合一个模块级状态 _mosaic_char_index 使用。如果多个模块在不同线程里,同时导入并使用 mosaic_match_obj() 函数,整个字符轮换的逻辑就会乱掉,因为调用方共享同一个全局标记变量

闭包(Closure)是一种允许函数访问已执行完成的其他函数里的私有变量的技术,是为函数增加状态的另一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def mosaic_match_obj():
char_index = 0
mosaic_chars = ['*', 'x']

def _mosaic(match_obj):
# nonlocal 用来标注变量来自上层作用域
nonlocal char_index
char = mosaic_chars[char_index]
char_index = (char_index + 1) % len(mosaic_chars)

length = len(match_obj.group())
return char * length

return _mosaic

在一个类(Class)中,状态和行为可以被很好地封装在一起,因此它天生适合用来实现有状态对象:

1
2
3
4
5
6
7
8
9
10
11
12
class CyclicMosaic:
_chars = ['*', 'x']

def __init__(self):
self._char_index = 0

def mosaic_match_obj(self, match_obj):
char = self._chars[self._char_index]
self._char_index = (self._char_index + 1) % len(self._chars)

length = len(match_obj.group())
return char * length

这个方案最终依赖的 CyclicMosaic.mosaic_match_obj,并非一个有状态函数,而是一个有状态的实例方法,但它们都是可调用对象的一种,都可以作为 re.sub() 函数的 repl 参数使用。