本文有些零碎,总题来说,包括两个问题:(1)可变对象(最常见的是list dict)被意外修改的问题,(2)对参数(parameter)的检查问题。这两个问题,本质都是因为动态语言(动态类型语言)的特性造成了,动态语言的好处就不细说了,本文是要讨论因为动态--这种灵活性带来的一些问题。

  什么是动态语言(Dynamic Programming language)呢,是相对于静态语言而言,将很多静态语言编译(compilation)时期所做的事情推迟到运行时,在运行时修改代码的行为,比如添加新的对象和函数,修改既有代码的功能,改变类型。绝大多数动态语言都是动态类型(Dynamic Typed),所谓动态类型,是在运行时确定数据类型,变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。Python就是属于典型的动态语言。

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

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

  本文地址:http://www.cnblogs.com/xybaby/p/7208496.html

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

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

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

 def routine(dct):
if high_propability:
sub_routine_no_change_dct(dct)
else:
sub_routine_will_change_dct(dct)

  上述的代码很简单,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)都指向同一个对象,仅此而已。来看一下面的代码:

  

 def double(v):
print 'argument before', id(v)
v *= 2
print 'argument after', id(v)
return v def test_double(a):
print 'parameter bdfore', id(a), a
double(a)
print 'parameter after', id(a), a if __name__=='__main__':
print 'test_double with int'
test_double(1)
print 'test_double with list'
test_double([1])

  运行结果:

  test_double with int
  parameter bdfore 30516936 1
  argument before 30516936
  argument after 30516924
  parameter after 30516936 1

  test_double with list
  parameter bdfore 37758256 [1]
  argument before 37758256
  argument after 37758256
  parameter after 37758256 [1, 1]

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

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

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

  The difference between shallow and deep copying is only relevant for compound objects (objects that contain other objects, like lists or class instances):

  • shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.
  • deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

  简单来说,深拷贝会递归拷贝,遍历任何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)

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

  

 import copy
def test_copy(inv):
    return copy.copy(inv)
def test_deepcopy(inv):
    return copy.deepcopy(inv)
dct = {str(i): i for i in xrange(100)} def timeit_copy():
    import timeit     print timeit.Timer('test_copy(dct)', 'from __main__ import test_copy, dct').timeit(100000)
    print timeit.Timer('test_deepcopy(dct)', 'from __main__ import test_deepcopy, dct').timeit(100000) if __name__ == '__main__':
    timeit_copy()

  运行结果:

  1.19009837668
  113.11954377 

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

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

好的API

  好的API应该是easy to use right; hard to use wrong。API应该提供一种契约,约定如果使用者按照特定的方式调用,那么API就能实现预期的效果。

  在静态语言如C++中,函数签名就是最好的契约。

  在C++中,参数传递大约有三种形式,传值、传指针、传引用(这里不考虑右值引用)。指针和引用虽然表现形式上差异,但效果上是差不多的,因此这里主要考虑传值和传引用。比如下面四个函数签名:

  int func(int a)
  int func(const int a)
  int func(int &a)
  int func(const int &a)

  对于第1、2个函数,对于调用者来说都是一样的,因为都会进行拷贝(深拷贝),无论func函数内部怎么操作,都不会影响到实参。二者的区别在于函数中能否对a进行修改,比如能否写 a *= 2。

  第3个函数,非const引用,任何对a的修改都会影响到实参。调用者看到这个API就知道预期的行为:函数会改变实参的值。

  第4个函数,const引用,函数承诺绝对不会修改实参,因此调用者可以放心大胆的传引用,无需拷贝。

  从上面几个API,可以看到,通过函数签名,调用者就能知道函数调用对传入的参数有没有影响。

  python是动态类型检查,除了运行时,没法做参数做任何检查。有人说,那就通过python doc或者变量名来实现契约吧,比如:

  def func(dct_only_read):

      “”“param: dct_only_read will be only read, never upate”“”

  但是人是靠不住的,也是不可靠的,也许在这个函数的子函数(子函数的子函数,。。。)就会修改这个dict。怎么办,对可变类型强制copy(deepcopy),但拷贝又非常耗时。。。

第二个问题:参数检查

  上一节说明没有签名 对 函数调用者是多么不爽,而本章节则说明没有签名对函数提供者有多么不爽。没有类型检查真的蛋疼,我也遇到过有人为了方便,给一个约定是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__,如果这两个函数都没有定义,那么实例的布尔求值一定返回真。

  在PEP8,由以下关于对序列布尔求值的规范:

 For sequences, (strings, lists, tuples), use the fact that empty sequences are false.

Yes: if not seq:
if seq: No: if len(seq):
if not len(seq):

  在google python styleguide中也有一节专门关于bool表达式,指出“尽可能使用隐式的false”。 对于序列,推荐的判断方法与pep8相同,另外还由两点比较有意思:

  1 如果你需要区分false和None, 你应该用像 if not x and x is not None: 这样的语句.

  2 处理整数时, 使用隐式false可能会得不偿失(即不小心将None当做0来处理). 你可以将一个已知是整型(且不是len()的返回结果)的值与0比较.

  第二点我个人很赞同;但第一点就觉得很别扭,因为这样的语句一点不直观,难以表达其真实目的。

  在pep20 the zen of python中,指出:

Explicit is better than implicit.

  这句话简单但实用!代码是写给人读的,清晰的表达代码的意图比什么都重要。也许有的人觉得代码写得复杂隐晦就显得牛逼,比如python中嵌套几层的list comprehension,且不知这样害人又害己。

  回到布尔表达式求值这个问题,我觉得很多时候直接使用if arg:这种形式都不是好主意,因为不直观而且容易出错。比如参数是int类型的情况,

def handle_age(age):
if not age:
return
# do lots with age

  很难说当age=0时是不是一个合理的输入,上面的代码对None、0一视同仁,看代码的人也搞不清传入0是否正确。

  另外一个具有争议性的例子就是对序列进行布尔求值,推荐的都是直接使用if seq: 的形式,但这种形式违背了”Explicit is better than implicit.“,因为这样写根本无法区分None和空序列,而这二者往往是由区别的,很多时候,空序列是一个合理的输入,而None不是。这个问题,stackoverflow上也有相关的讨论“如何检查列表为空”,诚然,如果写成 seq == [] 是不那么好的代码, 因为不那么灵活 -- 如果seq是tuple类型代码就不能工作了。python语言是典型的duck typing,不管你传入什么类型,只要具备相应的函数,那么代码就可以工作,但是否正确地工作就完完全全取决于使用者。个人觉得存在宽泛的约束比较好,比如Python中的ABC(abstract base class), 既满足了灵活性需求,后能做一些规范检查。

总结

  以上两个问题,是我使用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等跟更高级的工具。

  

  虽然我已经写了几年Python了,但是在Python使用规范上还是很欠缺。我也不知道在其他公司、项目中,是如何使用好Python的,如何扬长避短的。欢迎pythoner留言指导!

references:

Dynamic Programming language

instagram-pycon-2017

https://www.python.org/dev/peps/pep-0008/

google python styleguide

the zen of python

best-way-to-check-if-a-list-is-empty

最新文章

  1. Vertical Menu ver4
  2. JAVA简单工厂模式(从现实生活角度理解代码原理)
  3. joomla allvideo 去掉embed share
  4. acm 20140825
  5. 第五篇、iOS常用的工具 app icon 、office文件格式互转、在线HTML编辑器、16、10进制互转
  6. 纯原生js移动端图片压缩上传插件
  7. 动态加载js、css 代码
  8. .NET 4.6
  9. hdu1074 Doing Homework(状态压缩DP Y=Y)
  10. CentOS 安装nload(流量统计)
  11. Ajax Not Found,asp.net mvc 中
  12. 设置/修改wampserverd默认项目地址
  13. TCP/IP协议之ping和traceroute
  14. fetch简明学习
  15. Java学习点滴——对象实例化
  16. 手机QQ公众号亿级消息实时群发架构
  17. python小白——进阶之路——day3天-———运算符
  18. python实现简单的百度云自动下载
  19. Ajax之Json对象序列化传参
  20. 给iOS开发者的Android开发建议

热门文章

  1. js 实现倒计时效果
  2. Postgres Linux 维护 随笔1(启动篇)
  3. 微信JS-SDK开发 入门指南
  4. 【Selenium】idea的selenium环境配置
  5. Redis数据类型之List(三)
  6. Maven转化为Dynamic Web Module
  7. 表连接查询(2-n)
  8. 一个"Median Maintenance"问题
  9. MacTex XeLaTex xdvipdfmx:fatal: pdf_ref_obj(): passed invalid object. 报错的解决方法
  10. php生成Excel表格