查看原文
其他

吐槽:Python正在从简明转向臃肿,从实用转向媚俗

Python猫 2022-04-12

The following article is from Python作业辅导员 Author 天元浪子

 △点击上方“Python猫”关注 ,回复“1”领取电子书

花下猫语:Python毫无疑问正在Type Hint上投入越来越多的精力,不过,我一直都不喜欢这东西,一看到静态检查的写法就感觉难受。最近看到一篇文章,它从静态类型检查开始,共写了10条无情的吐槽。有些点,我十分赞同,但还有些则持保留意见。分享给大家一读。
国庆长假期间,Python3.9正式推出,各大IT平台和众多自媒体纷纷火力全开,热推Python3.9的新增特性。然而,除了媒体的自娱自乐,几乎所有的程序员都对此表示无感。我甚至觉得 ,每一次的版本升级都是在抬升Python的学习门槛,令初学者望而生畏。
简单和优雅,是Python创始人吉多 · 范罗苏姆(龟叔)开立山门之时为Python确立的哲学理念。现在,Pyton的发展显然已经背离了这一原则:不管有用无用,但凡别家有的,一概收入;不管是否适合,只要能充门面,悉数拿来。此情势正如当年Pandas之父韦斯·麦金尼面对Pandas的快速扩张时所表达出的无奈:“Pandas正在背离我最初所期望的简洁和易用,变得越来越臃肿和不可控制。”
曾经,我非常地喜欢Python。今天,这份爱依然在,但多了一些冷静和理性。十几年的陪伴,从默默无闻到炙手可热,Python终于在软件排行榜上占据了显耀的位置。在席卷软件行业的扩张风暴中,在Python连续不断地版本升级中,我却分明看到了一种危险的转变:Python正在从简明转向臃肿,从实用转向媚俗。

忍了很久,今日得闲,吐槽一下Python的新增语言特性。以下总结了导致Python危险转向的十大槽点,排名不分先后。

1. 媚俗的静态类型检查

大约从Py3.5开始,直至最新的Py3.9,感觉Python语言项目团队就在做一件事儿:静态类型检查。为了实现类型检查,他们推出了typing模块,定义了一大堆的应用规则。相应的,有一批静态类型检查工具应运而生,比如Mypy、Pylint或者Pyflakes,PyCharm也可以很好地集成这些工具。对此,我只想说一句:Python是动态语言吧?既然是动态语言,那你检查个毛线?即便检查通过,就能像编译语言那样运行了?一切不过是虚幻的假象而已,请别用其他语言的理念戕害质朴的Python。Python追求的是用最简捷的方式解决问题,而不是制造麻烦,然后再解决麻烦。Python强推静态类型检查,不过是迎合那些集成开发环境(IDE),同时满足不明就里的程序员的成就感和虚荣心罢了。

2.  画蛇添足的变量类型声明

当一个拥有5年工作经验的Python程序员读到下面的代码时,会有什么样的感受呢?

>>> name: str
>>> age: int = 18
>>> gilrfriends: list = ['奥黛丽·赫本''卡米拉·贝勒''安吉丽娜·朱莉']
相信他一定会说,天哪,这是什么?是Python吗?没错,这的确是Python。看起来像是指定了变量的类型,但即使像下面这样用字符串给被指定为列表类型的变量赋值,运行时也不会发出任何的提示或警告。
>>> gilrfriends: list = '奥黛丽·赫本, 卡米拉·贝勒, 安吉丽娜·朱莉' # 打着右转灯左转
>>> gilrfriends
'奥黛丽·赫本, 卡米拉·贝勒, 安吉丽娜·朱莉'
你看,打着右转灯左转,照样通行无误。这样画蛇添足的操作有什么用呢?原来仅仅是为了让IED或者类型检查工具知道变量的类型。工具原本是为程序员服务的,现在好了,你得先为工具服务。差评!

3. 鸡肋般的函数及参数的类型注解

我一直在寻找一种可以限定函数参数类型的方法,以避免程序运行时出现意外的情况。受限于Python的机制,大多数情况下我只能使用断言(assert)在函数内部对参数类型作出限制,同时尽可能在doc中对参数类型及含义作详尽描述,就像下面的代码一样。
def sphere(center, radius, color, slices=90):
    """绘制球体

    center      - 球心坐标,长度为3的元组或列表
    radius      - 半径,浮点型
    color       - 顶点颜色,字符串
    slices      - 球面分片数(数值越大越精细),整型
    """


    assert isinstance(center, (tuple, list)) and len(center)==3'期望参数center是长度为3的元组或列表'
    print('Hello')
    return True    
现在,如果按照Python指导委员会所倡导的方式,上面的代码应该写成如下的样式。
from typing import Union
def sphere(center:Union[tuple,list], radius:float, color:str, slices:int=90) -> bool:
    """绘制球体

    center      - 球心坐标,长度为3的元组或列表
    radius      - 半径,浮点型
    color       - 顶点颜色,字符串
    slices      - 球面分片数(数值越大越精细),整型
    """


    print('Hello')
    return True     
看上去一切都是完美的,但结果令人遗憾。这个函数及参数的类型注解只是一个注解而已,中看不中用:不管你输入了什么类型的参数,只要数量够3个或者4个,就可以被执行。唯一的作用就是降低了代码的可读性,原本四个参数一眼可见,现在却要花费20秒钟来辨认。不会有类型验证(除非使用类型检查工具),更不会有长度检查。如果想要这些功能,一切还得靠自己。

4. 不加限制的扩展运算符

任何一门编程语言都是在不断地更新和扩展中发展起来的,Python也不例外。说实话,Python的一些语法扩展,还是非常精妙和必要的,比如Py3.8引入的海象运算符(:=),就是一个典型的例子。引入海象运算符之前,若要将一个不足16位长的字符串s用#补足16位(若大于或等于16位则原样输出),一般写成下面这样。
>>> s = 'Hello Wold'
>>> s + '#'*(16-len(s)) if len(s) < 16 else s
'Hello Wold######'
这个代码中两次计算字符串s的长度,显然,这不是一个高效的代码。但是如果使用一个临时变量保存字符串s的长度,又不够优雅。海象运算符正是为了解决这一类问题而诞生的。
>>> s = 'Hello Wold'
>>> s + '#'*(16-c) if (c:=len(s)) < 16 else s
'Hello Wold######'
可惜,不是所有的扩展都像海象运算符这样美妙,甚至其中的很大一部分扩展并未展现出足够的必要性和不可替代性,只是给程序员带来更多使用上的困惑。比如,Py3.9新增的字典合并(|)和更新(|=)运算符,当属此列。
>>> a = {'x':1'y':2}
>>> b = {'y':3'z':4}
>>> a | b # 返回一个新的字典
{'x'1'y'3'z'4}
>>> a |= b # 将b合并到a中
>>> a
{'x'1'y'3'z'4}
严重怀疑Python团队的人忘记了字典还有个update()方法可以实现这两个新的运算符的功能。
>>> a = {'x':1'y':2}
>>> b = {'y':3'z':4}
>>> a.update(b)
>>> a
{'x'1'y'3'z'4}
也许有人会说,就算字典更新运算符(|=)和update()功能完全重叠,可字典合并运算符(|)返回的是一个新字典,而update()改变了原先的字典。对于持有这种想法的人,我只能悄悄提醒一下,Python对此早有解决方案。
>>> {**a, **b}
{'x'1'y'3'z'4}

5. 越俎代庖的字符串方法

大量的内置函数或方法,固然可以为程序员带来更多的便利,但也会使系统臃肿,同时增加了入门难度,使学习曲线变得陡峭。如何平衡这个尺度,是一个见仁见智的问题。在我看来,不断增加函数和方法,不但违背了Python的初衷,破坏了API的稳定性,也让程序员失去了选择和乐趣。比如,Py3.9为str对象新增了removeprefix()和removesuffix()两个方法,分别用于删除字符串前缀和后缀。

>>> s = '[主播]上港获得前场任意球(1:1)'
>>> s.removeprefix('[主播]')
'上港获得前场任意球(1:1)'
>>> s.removesuffix('(1:1)')
'[主播]上港获得前场任意球'
类似的应用需求有很多,如果都要加入到str对象的方法库中,str对象将会变得无比庞大。此类函数的编写,分明应该交由程序员去做,Python语言项目组只需要关注基础的和核心的方法即可。实际上,这两个方法实现起来,也不过是一行代码而已。
>>> s = '[主播]上港获得前场任意球(1:1)'
>>> s if s.find('[主播]') < 0 else s[len('[主播]'):]
'上港获得前场任意球(1:1)'
>>> s if (n:=s.rfind('(1:1)')) < 0 else s[:n]
'[主播]上港获得前场任意球'

6. 啼笑皆非的字符串前缀

Py2时代,u是最常见的字符串前缀,Py3时代,u前缀销声匿迹,b成为了最常见的字符串前缀。无论是Py2还是Py3,原生字符串前缀r都是Python程序员眼中的熟客。从Py3.6开始,突然又冒出来一个新的字符串前缀。

>>> score = '1:1'
>>> f'当前比分{score}'
'当前比分1:1'
以前的前缀,仅仅是一个标识,现在的f前缀则变成了一种运算符,感觉美妙的理性世界瞬间崩塌了!然而,这只是一个开始,更糟糕的事情发生在Py3.8版本上,他们居然又为这个f前缀增加了等号(=)的支持。
>>> 语文=95
>>> 数学=100
>>> f'{语文=},{数学=}'
'语文=95,数学=100'

MyGod,这哪儿是代码,简直是神符!说实话,格式化字符串时,除了C语言风格的%外,我连format()函数都没用过,现在却有可能要面对同事写出来的包含加强版f前缀的代码。伤心的我,只能一笑了之。

7. 不讲逻辑的字典顺序

Py3.6发布的时候,就有人到处宣传说,字典有序了。起初我以为那不过自媒体出于猎奇的目的,随便说说,吸引眼球的。在逻辑上,字典是数据的无序集合,仅依赖于键检索,如果字典有序,那就不是字典了。没想到,Py3.7居然真的将“字典保持插入顺序”作为新增特性正式对外公布了。幸好,官方说的是“字典保持插入顺序”,而不是“字典有序”,总算为逻辑保留了最后的底裤。

我说字典无序,不是指字典在物理实体上实现的时候真的无序,而是指它的顺序对用户而言没有明确的界定,不能作为数据的特性使用。也许Py3.7使用了新的机制可以更高效地存储和检索字典数据,但这并不是程序员所关注的。新的机制带来的字典顺序稳定,并不能成为我们可以信赖和使用的字典属性。

8. 惟恐天下不乱的函数参数限定符

说到函数的定义和传参,恐怕没有比Python更灵活的语言了。方便的同时,位置参数、默认参数、可变参数、关键字参数等几种参数类型和使用顺序,也给程序员带来了很多使用上的困惑。然而,Py3.8坚定地认为,函数参数的使用还不够混乱,还需要再刺激一下,以期达到从大乱到大治的目的。于是,函数参数限定符登场了——为了增强效果,Python官方选择斜杠(/)作为这个限定符的表现形式。

让我们一起来尝试着读一读下面这段代码。
>>> def func(a, b, /, c, d, *, e, f):
        print('OK')

>>> func(1,2,3,4,e=5,f=6)
OK
>>> func(1,2,3,d=4,e=5,f=6)
OK

这里,a, b是位置参数,e, f是关键字参数,c, d既可以是位置参数,也可以是关键字参数。好了,关于函数参数限定符就说这么多吧,反正也用不着——除非你是在用Python函数完全模拟C代码编写的函数。

9. 毫无优雅感的泛型函数

说到泛型,Python程序员通常会有两种表情:一种是惊讶:“Python有泛型吗?”;另一种是哂笑:“Python有型吗?”且不讨论Python是否“有型”,作为动态语言,泛型难道不是与生俱来的吗?也许Python团队不这么认为,或者他们认为Python程序员不这么认为。为了配得上编程语言排行榜上大佬的位次,从Py3.4开始,Python团队就为Python定制了一个泛型函数装饰器。无奈这个机制实在太过牵强,用它写出来得代码丑得一批,估计没几个人会用它。

>>> from functools import singledispatch
>>> @singledispatch
def say_hello(x):
    print('抱歉,我不认识你')

>>> @say_hello.register(int)
def _(x):
    print('你好,整数')

>>> @say_hello.register(str)
def _(x):
    print('你好,字符串')

>>> say_hello(5)
你好,整数
>>> say_hello('abc')
你好,字符串
>>> say_hello([1,2,3])
抱歉,我不认识你
上面这一大堆的代码,只是定义了一个函数say_hello(),可以根据传入参数的类型不同而做出不同反应。如果不使用泛型装饰器,实现起来更加简单且易读。
>>> def say_hello(x):
        if isinstance(x, int):
            print('你好,整数')
        elif isinstance(x, str):
            print('你好,字符串')
        else:
            print('抱歉,我不认识你')

>>> say_hello(5)
你好,整数
>>> say_hello('abc')
你好,字符串
>>> say_hello([1,2,3])
抱歉,我不认识你

10. 前后打脸的亡羊补牢

当我第一次读到typing.List和typing.Dict的时候,有那么一小会儿,感觉像是在读java或者是C++的代码。为什么不是熟悉的list和dict呢?仅仅是为了类型提示(type hints) ,为了让集成开发工具读懂代码,生生搞出了比list和dict还复杂的概念。最终的结果是,IDE读懂了代码,程序员却看不明白了。

好在Py3.9对类型提示做了改进,为内置的以及标准库中的集合类型提供了用于类型提示中的泛型的支持,解决了一直以来Python代码中会出现两种list(list 和 typing.List)类型的尴尬情况,也算是亡羊补牢吧。

罗里吧嗦写了这么多,希望不会让Python程序员心生沮丧。事实上,不管我或者我们如何吐槽,Python依然是一门伟大的编程语言。本文的观点,仅是我作为一个原教旨主义者的个人观点,稍微掺杂了一点调侃。欢迎各位发表高见,不要顾及我的颜面。我会装作看不见,不解释不辩论。杠爷请绕行。

Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~

近期热门文章推荐:

2021年,你应该知道的Python打包指南
Python 为什么会有个奇怪的“...”对象?
最全的命名元组Namedtuple使用指南!!!
Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?

感谢创作者的好文

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存