71、Python之函数式编程:不能定义常量,Python如何支持不可变性?
引言
其他编程语言中可以使用const或者final等关键字来定义不可修改的变量,也就是常量。但是,Python中似乎没有提供类似的关键字。所以,时不时总会有同学提出这样的质疑:都不能定义常量,Python对函数式编程的支持也太弱了吧。其实,如果对函数式编程的不可变性有更加深入的理解,就不会有这样的疑问了。
本文打算从软件架构作为展开,逐步对上面的疑问进行解答。
本文的主要内容有:
1、简单聊下软件架构
2、三种主流范式对架构的支持
3、Python关于不可变性的设计哲学
1、简单聊下软件架构
由于生产、生活中的不确定性越来越大,软件系统所需要支持的业务需求也变得更加不确定,所以,一个真正满足业务需求的软件系统,大部分工作都是在对系统进行修改、扩展、维护。我们曾经提过的各种设计原则,比如开闭原则(OCP)、单一职能原则(SRP)等,其实都是为了让软件系统在设计之初,就考虑了后续能够更加容易扩展和维护。
所谓的“软件架构”就是根据业务需求,综合应用各种设计规则,进行软件系统的整体设计,从而可以用最小的人力成本来满足构建和维护系统的需求。
所以,衡量一个软件架构的优劣,只要看它满足业务需求所需要的成本即可。如果成本比较低,并且在整个软件系统的生命周期内一直都能维持这样的低成本,那么这个系统的架构就是优良的。反之,如果该系统的每次发布都会提升下一次变更的成本,那么这个架构就是不好的。
如果进一步深挖,软件系统的架构设计是为了保证易于扩展和维护,落脚到程序代码的组织上,就要保证代码的可读性、易于调试、测试。
要保证代码的可读性和易于调试、测试,就要尽量规避编码的随意性,一般会从这些方面考虑:
1)程序代码格式清晰、有必要的注释。
2)程序代码的控制逻辑相对清晰,控制逻辑的转移更加明确。
3)程序代码的状态、结果更加具有确定性。
2、三种主流范式对软件架构的支持
如果从软件架构的质量,以及代码的可读性和易于调试、测试的角度,同时结合编程范式与编程语言之间相互独立的关系,来重新看编程范式的话,所谓的“编程范式”,似乎是计算机科学领域的大佬和先贤,关于如何开发更加易于扩展和维护的软件系统,所总结出的“苦口婆心”的真知灼见。
层出不穷的编程语言新特性,以及代码组织编写的自由度,给程序员进行创造性的发挥,带来了极大的便利的同时,也在代码编写上引入了更大的随意性。
编程范式,从表面看来,似乎是关于”应该怎么做“的建议。但是,如果进行更加本质的思考,编程范式的核心似乎是关于”最好不要做什么“的规劝。
所以,编程范式对软件架构的支持,其实是一种”做减法“的支持:
1)过程式编程范式,其实是对程序员不要随意使用goto进行无限制的直接控制转移的规劝,是对控制逻辑清晰度的保证。
2)面向对象编程范式,本质上是对函数指针随意使用的限制,也就是对程序控制逻辑进行隐性的间接转移的限制,也是对控制逻辑清晰度的保证。
3)函数式编程范式,最核心的特性不可变性,本质上是对赋值操作加上了限制,是对程序的状态和结果的确定性的保证。
范式都是建议性的,你可以做,但是我强烈建议你不要做。当然,如果一门编程语言对某种编程范式是完全支持的,那么这种编程范式就变成了强制性的。所以,还是要看具体的编程语言的设计哲学以及最终实现。
3、Python关于不可变性的设计哲学
Python中确实没有内置的常量定义机制,这与Python的设计哲学有关。Python更加倾向于使用约定而不是强制性规则来指导编程实践。
当然,尽管没有内置常量,在Python中,我们仍然有其他方式来支持函数式编程的不可变性。
1)通过约定使用常量
我们可以使用全大写字母命名变量,从而表示它们应该被视为常量,这是Python社区的约定。
2)使用不可变的数据结构
函数式编程强调使用不可变的数据结构,Python中的元组、字符串以及不可变集合(frozenset)等,都是不可变的(虽然其存储的元素对象的可变性决定了这种不可变性的彻底性)。
3)使用dataclass中的frozen
from dataclasses import dataclass@dataclass(frozen=True)
class Point:x: inty: intif __name__ == '__main__':p1 = Point(10, 20)print(p1)print(p1.x)# 尝试进行重新赋值,会报错p1.x = 100
执行结果:
4)自定义不可变类型
类似于dataclass的frozen=True,本质上都是通过对__setattr__()方法的覆盖,来禁止修改实例属性,从而实现了对不可变性的支持。
所以,我们也是可以通过这种机制来实现不可变性的。
直接看代码:
class Point:def __init__(self, x, y):object.__setattr__(self, 'x', x)object.__setattr__(self, 'y', y)def __setattr__(self, key, value):raise AttributeError(f'属性{key}不允许修改')if __name__ == '__main__':p1 = Point(10, 20)print(p1)print(p1.x)# 尝试进行重新赋值,会报错p1.x = 100
执行结果:
尽管Python对函数式编程的不可变性有足够的支持,我们还是需要对函数式范式关于不可变性要求的初衷,有更加深入的理解,避免机械性地执行,而使得编程行为变得僵化。
在Python等面向对象的语言中,不可变数据结构似乎有些另类,但当我们从函数式编程的角度思考时,就会发现状态变化是许多令人困惑的问题的根源。使用不可变数据结构有助于我们拨云见日,直抵问题的核心。
所以,不可变性其实是对状态和结果更加具有确定性的一种追求,只要心里始终有这个清晰的目标,并向着这个目标努力,对于不可变性,就已经有了足够深入的理解。
总结
本文从软件架构作为切入点,将程序代码的可读性、易于调试、测试的建议,和编程范式的各种减法型规范,联系在一起,从而对设计原则、软件架构及编程范式有了更深入的理解。最后,通过Python关于不可变性的支持,探讨了关于程序状态和结果的确定性的本质。
以上,就是本文的全部内容了,感谢您的拨冗阅读,希望对您有所帮助。