第一个问题: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函数返回对象的地址,每个对象在都有唯一的地址。看下面两个例子就知道了

Python

>>> 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])

运行结果:

Python

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):

· A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.

· A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

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

Python

>>> 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中,对象的基本构造也是浅拷贝,例如

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()

运行结果:

Python

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

Python

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或者变量名来实现契约吧,比如:

Python

**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,由以下关于对序列布尔求值的规范:

Python

**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相同,另外还由两点比较有意思:如果你需要区分false和None, 你应该用像 if not x and x is not None: 这样的语句.

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

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

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留言指导!

python线上编辑问题_大型线上项目中动态语言诸多问题之二 -- 以 Python 语言为例...相关推荐

  1. python线上编辑问题_python django - static文件处理与线上部署测试

    static文件相关操作涉及: a. 文件位置与访问路径映射 b. setting.py与static相关配置 STATIC_URL STATIC_ROOT STATICFILES_DIRS c. h ...

  2. python图像特征提取与匹配_[OpenCV-Python] OpenCV 中图像特征提取与描述 部分 V (二)...

    部分 V 图像特征提取与描述 34 角点检测的 FAST 算法 目标 • 理解 FAST 算法的基础 • 使用 OpenCV 中的 FAST 算法相关函数进行角点检测 原理 我们前面学习了几个特征检测 ...

  3. python线上课堂_线上线下相结合的Python编程教学

    线上线下相结合的 Python编程教学 朱军强 广东省韶关市乳源瑶族自治县桂头中学 ,广东 韶关 512736 摘要:Python编程教学是初中信息课堂教学的重要组成模块,高质量的Python编程教学 ...

  4. python线上课程哪个好-Python培训是应该选择线上还是线下呢?

    想学习Python的朋友一定都会有Python培训是应该选择线上还是线下这个问题,今天我就给大家说一说. 首先我们来了解一下线上培训.线上培训是随着互联网的发展才兴起的,其实线上培训一直有个弊端就是, ...

  5. python线上课程-Python培训线上和线下有什么区别?

    大家想要学习Python的话,主要的方式主要是自学好参加培训,由于自学对自己的基础抗干扰能力要求比较大,好多小伙伴一般都会选择参加培训的方式来进行学习Python知识,而Python培训又包括线上和线 ...

  6. python线上教育培训

    为响应教育局办公厅印发<2020年教育信息化和网络安全工作要点>里的32项重点任务,落实<教育部关于实施全国中小学教师信息技术应用能力提升工程2.0的意见>,推动各地因地制宜开 ...

  7. 简述C#中IO的应用 RabbitMQ安装笔记 一次线上问题引发的对于C#中相等判断的思考 ef和mysql使用(一) ASP.NET/MVC/Core的HTTP请求流程...

    简述C#中IO的应用 在.NET Framework 中. System.IO 命名空间主要包含基于文件(和基于内存)的输入输出(I/O)服务的相关基础类库.和其他命名空间一样. System.IO ...

  8. python线上课程-零基础学Python量化投资,超值线上课程反复回看

    原标题:零基础学Python量化投资,超值线上课程反复回看 超值网络课程 量化投资是一种严谨.系统化的投资方式,相比起传统投资,量化投资风险低回报高,但是它要求投资者使用数据处理分析.计算机编程技术. ...

  9. 2月15日Python线上峰会免费学!6场精华分享,用代码“抗”疫

    截至截止2月12号09时43分,新型冠状病毒在全国已确诊44726例,疑似病例已达21675例.而专家所说的"拐点"始终未至,受疫的影响,各大公司开启远程办公模式,将返回工作场所办 ...

最新文章

  1. 【ES6】JS第7种数据类型:Symbol
  2. 程序员能力矩阵 你属于哪一层?
  3. uwsgi部署到nginx出现invalid request block size: 4161 (max 4096)...skip问题(亲测)
  4. java mp3数组_Java基础之数组(一)
  5. 腾讯地图 qq.map 设置鼠标样式
  6. 《windows核心编程》–Windows内存体结构(二)
  7. logistic回归分析优点_二元Logistic回归
  8. 奔图P3305DN安装官网windows驱动 打印乱码解决方法
  9. 获取深户股市列表api_获取股票api
  10. React开发(250):react项目理解 ant design loding控制页面转圈加载
  11. 翻译程序、编译程序和解释程序的区别和联系
  12. 分布式高频量化交易系统架构讲解(企业版,期货ctp,股票xtp,数字货币,附全部源码)(值得收藏)
  13. 江波龙入选国家级专精特新“小巨人”企业
  14. 【Spark Streaming】(四)基于 Spark Structured Streaming 的开发与数据处理
  15. CSS3实现倒影效果
  16. 时间格式转换(Date转时间戳)
  17. Freshman的插入排序实现
  18. WeOS 微信手机操作系统
  19. SuperSocket.WebSocket WebSocketServer设置文本编码
  20. 无心剑七绝《梅西封王》

热门文章

  1. C# MainWindowHandle为0的解决方法
  2. Laravel 中的异常处理
  3. Java 异常java.lang.IllegalArgumentException: Illegal group reference
  4. Python解决八皇后问题
  5. 解决java前后端分离端口跨域问题
  6. RedHat7.4最小化安装yum源不可用问题解决
  7. node.js require()缓存-可能无效?
  8. 如何设置TextView textStyle,例如粗体,斜体
  9. MATLAB编程与应用系列-第2章 数组及矩阵的创建及操作(4)
  10. Expo大作战(十九)--expo打包后,发布分用程序到商店的注意事项