首页 > 动态语言的灵活性是把双刃剑 -- 以Python语言为例

动态语言的灵活性是把双刃剑 -- 以Python语言为例

本文有些零碎,总题来说,包括两个问题:

(1)可变对象(最常见的是list dict)被意外修改的问题,

(2)对参数(parameter)的检查问题。

这两个问题,本质都是因为动态语言(动态类型语言)的特性造成了,动态语言的好处就不细说了,本文是要讨论因为动态--这种灵活性带来的一些问题。

什么是动态语言(Dynamic Programming language)呢,是相对于静态语言而言,将很多静态语言编译(compilation)时期所做的事情推迟到运行时,在运行时修改代码的行为,比如添加新的对象和函数,修改既有代码的功能,改变类型。

绝大多数动态语言都是动态类型(Dynamic Typed),所谓动态类型,是在运行时确定数据类型,变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。Python就是属于典型的动态语言。

动态语言的魅力在于让开发人员更好的关注需要解决的问题本身,而不是冗杂的语言规范,也不用干啥都得写个类。运行时改变代码的行为也是非常有用,比如python的热更新,可以做到不关服务器就替换代码的逻辑,而静态语言如C++就很难做到这一点。笔者使用得最多的就是C++和Python,C++中的一些复杂的点,比如模板(泛型编程)、设计模式(比如template method),在Python中使用起来非常自然。我也看到过有一些文章指出,设计模式往往是特定静态语言的补丁 -- 为了弥补语言的缺陷或者限制。

以笔者的知识水平,远远不足以评价动态语言与静态语言的优劣。本文也只是记录在我使用Python这门动态语言的时候,由于语言的灵活性,由于动态类型,踩过的坑,一点思考,以及困惑。

 

第一个问题:Mutable对象被误改

这个是在线上环境出现过的一个BUG

事后说起来很简单,服务端数据(放在dict里面的)被意外修改了,但查证的时候也花了许多时间,伪代码如下:

 

上述的代码很简单,dct是一个dict,极大概率会调用一个不用修改dct的子函数,极小概率出会调用到可能修改dct的子函数。问题就在于,调用routine函数的参数是服务端全局变量,理论上是不能被修改的。当然,上述的代码简单到一眼就能看出问题,但在实际环境中,调用链有七八层,而且,在routine这个函数的doc里面,声明不会修改dct,该函数本身确实没有修改dct,但调用的子函数或者子函数的子函数没有遵守这个约定。

 

从python语言特性看这个问题

本小节解释上面的代码为什么会出问题,简单来说两点:dict是mutable对象; dict实例作为参数传入函数,然后被函数修改了。

  Python中一切都是对象(evething is object),不管是int str dict 还是类。比如 a =5, 5是一个整数类型的对象(实例);那么a是什么,a是5这个对象吗? 不是的,a只是一个名字,这个名字暂时指向(绑定、映射)到5这个对象。b = a  是什么意思呢, 是b指向a指向的对象,即a, b都指向整数5这个对象

  那么什么是mutable 什么是immutable呢,mutable是说这个对象是可以修改的,immutable是说这个对象是不可修改的(废话)。还是看Python官方怎么说的吧

Mutable objects can change their value but keep their id().

  Immutable:An object with a fixed value. Immutable objects include numbers, strings and tuples. Such an object cannot be altered. A new object has to be created if a different value has to be stored. They play an important role in places where a constant hash value is needed, for example as a key in a dictionary.

承接上面的例子(a = 5),int类型就是immutable,你可能说不对啊,比如对a赋值, a=6, 现在a不是变成6了吗?是的,a现在"变成"6了,但本质是a指向了6这个对象 -- a不再指向5了

  检验对象的唯一标准是id,id函数返回对象的地址,每个对象在都有唯一的地址。看下面两个例子就知道了

>>> a = 5;id(a)

  35170056

  >>> a = 6;id(a)

  35170044

 

  >>> lst = [1,2,3]; id(lst)

  39117168

  >>> lst.append(4); id(lst)

  39117168

或者这么说,对于非可变对象,在对象的生命周期内,没有办法改变对象所在内存地址上的值。

  python中,不可变对象包括:int, long, float, bool, str, tuple, frozenset;而其他的dict list 自定义的对象等属于可变对象。注意: str也是不可变对象,这也是为什么在多个字符串连接操作的时候,推荐使用join而不是+

  而且python没有机制,让一个可变对象不可被修改(此处类比的是C++中的const)

 

dict是可变对象!

那在python中,调用函数时的参数传递是什么意思呢,是传值、传引用?事实上都不正确,我不清楚有没有专业而统一的说法,但简单理解,就是形参(parameter)和实参(argument)都指向同一个对象,仅此而已。来看一下面的代码:

可以看到,刚进入子函数double的时候,a,v指向的同一个对象(相同的id)。对于test int的例子,v因为v*=2,指向了另外一个对象,但对实参a是没有任何影响的。对于testlst的时候,v*=2是通过v修改了v指向的对象(也是a指向的对象),因此函数调用完之后,a指向的对象内容发生了变化。

 

如何防止mutable对象被函数误改:

为了防止传入到子函数中的可变对象被修改,最简单的就是使用copy模块拷贝一份数据。具体来说,包括copy.copy, copy.deepcopy, 前者是浅拷贝,后者是深拷贝。二者的区别在于:

简单来说,深拷贝会递归拷贝,遍历任何compound object然后拷贝,例如:

>>> lst = [1, [2]]

  >>> import copy

  >>> lst1 = copy.copy(lst)

  >>> lst2 = copy.deepcopy(lst)

  >>> print id(lst[1]), id(lst1[1]), id(lst2[1])

  4402825264 4402825264 4402988816

  >>> lst[1].append(3)

  >>> print lst, lst1,lst2

  [1, [2, 3]] [1, [2, 3]] [1, [2]]

 

从例子可以看出浅拷贝的局限性,Python中,对象的基本构造也是浅拷贝,例如 dct = {1: [1]}; dct1 = dict(dct)

  正是由于浅拷贝与深拷贝本质上的区别,二者性能代价差异非常之大,即使对于被拷贝的对象来说毫无差异:

 

在上面的示例中,dct这个dict的values都是int类型,immutable对象,因为无论浅拷贝 深拷贝效果都是一样的,但是耗时差异巨大。如果在dct中存在自定义的对象,差异会更大

  那么为了安全起见,应该使用深拷贝;为了性能,应该使用浅拷贝。如果compound object包含的元素都是immutable,那么浅拷贝既安全又高效,but,对于python这种灵活性极强的语言,很可能某天某人就加入了一个mutable元素。

 

第二个问题:参数检查

上一节说明没有签名 对 函数调用者是多么不爽,而本章节则说明没有签名对函数提供者有多么不爽。没有类型检查真的蛋疼,我也遇到过有人为了方便,给一个约定是int类型的形参传入了一个int的list,而可怕的是代码不报错,只是表现不正常。

def func(arg):     

 if arg:        

   print 'do lots of things here'    

 else:        

   print 'do anothers'



上述的代码很糟糕,根本没法“望名知意”,也看不出有关形参 arg的任何信息。但事实上这样的代码是存在的,而且还有比这更严重的,比如挂羊头卖狗肉。

  这里有一个问题,函数期望arg是某种类型,是否应该写代码判断呢,比如:isinstance(arg, str)。因为没有编译器静态来做参数检查,那么要不要检查,如何检查就完全是函数提供者的事情。如果检查,那么影响性能,也容易违背python的灵活性 -- duck typing; 不检查,又容易被误用。

  但在这里,考虑的是另一个问题,看代码的第二行: if arg。python中,几乎是一切对象都可以当作布尔表达式求值,即这里的arg可以是一切python对象,可以是bool、int、dict、list以及任何自定义对象。不同的类型为“真”的条件不一样,比如数值类型(int float)非0即为真;序列类型(str、list、dict)非空即为真;而对于自定义对象,在python2.7种则是看是否定义了__nonzero__ 、__len__,如果这两个函数都没有定义,那么实例的布尔求值一定返回真。



 

总结

以上两个问题,是我使用Python语言以来遇到的诸多问题之二,也是我在同一个地方跌倒过两次的问题。Python语言以开发效率见长,但是我觉得需要良好的规范才能保证在大型线上项目中使用。而且,我也倾向于假设:人是不可靠的,不会永远遵守拟定的规范,不会每次修改代码之后更新docstring ...

  因此,为了保证代码的可持续发展,需要做到以下几点

  第一:拟定并遵守代码规范

  代码规范最好在项目启动时就应该拟定好,可以参照PEP8和google python styleguild。很多时候风格没有优劣之说,但是保证项目内的一致性很重要。并保持定期review、对新人review!

  第二:静态代码分析

  只要能静态发现的bug不要放到线上,比如对参数、返回值的检查,在python3.x中可以使用注解(Function Annotations),python2.x也可以自行封装decorator来做检查。对代码行为,既可以使用Coverity这种高大上的商业软件,或者王垠大神的Pysonar2,也可以使用ast编写简单的检查代码。

  第三:单元测试

  单元测试的重要性想必大家都知道,在python中出了官方自带的doctest、unittest,还有许多更强大的框架,比如nose、mock。

  第四:100%的覆盖率测试

  对于python这种动态语言,出了执行代码,几乎没有其他比较好的检查代码错误的手段,所以覆盖率测试是非常重要的。可以使用python原生的sys.settrace、sys.gettrace,也可以使用coverage等跟更高级的工具。

原文出处:http://www.cnblogs.com/xybaby/p/7208496.html

 




识别图中二维码,领取python全套视频资料

转载于:https://www.cnblogs.com/IT-Scavenger/p/9642558.html

更多相关:

  • 点云PCL免费知识星球,点云论文速读。文章:DSP-SLAM: Object Oriented SLAM with Deep Shape Priors作者:Jingwen Wang Martin Runz Lourdes Agapito编译:点云PCL代码:https://github.com/JingwenWang95/DSP-S...

  • RAM缓存 新RAM缓存算法(CLFUS) 新的RAM缓存使用的创意来自许多缓存替换策略和算法,包括LRU,LFU,CLOCK,GDFS及2Q,它被命名为时钟周期内最小频繁使用大小算法CLFUS(Clocked Least Frequently Used by Size)。它避开了任何专利算法,具有如下特性: 均衡最近性(Rec...

  • MP4 |视频:AVC,1280×720 30 fps |音频:AAC,48 KHz,2 Ch |时长:2h 12m 语言:英语+中英文字幕(根据原英文字幕机译更准确)|大小解压后:560M C4D是一个有抱负的运动图形艺术家和设计师的重要工具。借助C4D,您可以使用3D对象、动态效果和动画来增强运动图形、模型和可视化效果。本课...

  • 文章目录先说问题:再说解决尝试1:尝试2(该尝试建议先在自己环境搭配对应业务测试通过后再现场尝试): 感谢 学无止境996同学的陪伴和vigourtyy美丽女友的支持,直到这个解决问题的深夜 先说问题: ceph 12.2.1生产环境:3副本 tier + 3副本data 机房在拥有业务的情况下重启集群交换机,产生如下场景...

  • 这周主要学习了java中的类和对象的知识点,发现和C++中的类和对象极为相似,对于类和对象的概念理解起来也简单。同时在自学的过程中也把类的知识重新复习巩固了一下(如类的三大特征:继承,封装和多态,构造,成员对象的访问权限,构造,无参有参函数的调用等),同时也了解到一些新的概念,比如类对象创建和引用占据堆内存和栈内存,输出对象时默认调...

  • THE START更新堪称轻量级MATLAB的一款软件最新版-Maplesoft Maple 2019.2 中文版。Maple是符号和数字计算环境,也是一种多范式编程语言,由Maplesoft开发,还涵盖了技术计算的其他方面,包括可视化,数据分析,矩阵计算和MATLAB连接。MapleSim工具箱添加了用于多域物理建模和代码生成的...

  • 同学们,你们在学习他人的代码,是否见过这样的代码 def main(): def user_info(gender): 当你还是个小萌新时,你一定会认为这是个很牛逼的语法。 当你有了一点基础时,你一定会想要了解这个语法,并且尝试去使用它。 那么今天,我们便来了解这个牛语法。 有了一点点的python基础,我们来看这段代...

  •     自从用了这些快捷键,鼓励师也不需要了,代码开发效率蹭蹭提升!!! ctrl+shift+[折叠代码 (这个比ctrl+k ctrl+l、ctrl+k ctr+j不知道好用多少倍!) ctrl+shift+]展开代码 ctrl+shift+T打开手贱不小心关掉的窗口 【推荐】ctrl+shift+O打开当前文件...

  • 在提交代码之前,建议最好先Fetch代码下来(如果有冲突,系统会提示),然后再操作Merge到本地分支,这样做是为了避免有其他人同时修改了当前分支,如果直接用Ctrl+T(pull代码)极有可能覆盖本地分支最新代码,安全起见先Fetch代码(Ctrl+Alt+Shift+1)——所谓:小心驶得万年船!...

  • 每次复制代码时,如果代码里有 // 这样的注释就容易让格式乱掉,通过下面的设置就可以避免这种情况。 粘贴代码时取消自动缩进 VIM在粘贴代码时会自动缩进,把代码搞得一团糟糕,甚至可能因为某行的一个注释造成后面的代码全部被注释掉,我知道有同学这个时候会用vi去打开文件再粘贴上去(鄙人以前就是这样),其实需要先设置一下 s...

  • 什么是操作系统 操作系统位于计算机硬件与应用软件之间 是一个协调 管理 控制计算机硬件资源与软件资源的控制程序   为何要有操作系统? 1 控制硬件 2 把对硬件的复杂的操作封装成优美简单的接口(文件),给用户,或者应用程序去使用   注意:一套完整的计算机系统包含三部分   应用程序   操作系统:个人windows,公司linux...

  • 前些天在看一本书,温伯格的《理解专业程序员》,其中提到有的程序员得了第二语言综合征——在学习第三、第四门语言的时候很容易,但是学习第二门简直能要了他们的命。我当时就确定我患了这个毛病,因为我一直想了解Java语言,但迟迟不愿开始,以前一直不知道为什么这么难开始,现在终于豁然开朗。把这本书读完不久,书很薄,我就开始计划打破这个综合征,第...