编写高质量Python程序(二)编程惯用法

本系列文章为《编写高质量代码——改善Python程序的91个建议》的精炼汇总。

文章首发于公众号【Python与算法之路】

利用assert语句发现问题

assert语句的基本语法如下:

assert expression1 ["," expression2]

其中,expression1是判断语句,会返回True或False,当返回False时会引发AssertionError。[]中的内容表示是可选的,用来传递具体的异常信息。

>>> a = 1
>>> b = 2
>>> assert a == b, "a equals b"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: a equals b

利用assert语句来发现程序中的问题。断言(assert)在很多语言中都存在,主要为调试程序服务,能够快速方便检查程序的异常或不恰当的输入。

要注意的是使用assert是有代价的,它会对性能产生一定的影响,可以不用尽量不用。

两个变量进行数据交换

变量进行数据交换值时,不推荐使用中间变量

# 交换x,y
# 使用中间变量
temp = x
x = y
y = temp
# 不使用中间变量
x, y = y, x

第二种方法在内存中执行的顺序如下:

  • 先计算右边的表达式 y, x,在内存中创建元组(y, x),其标示符合值分别为 y、x 及其对应的值,其中 y 和 x 是在初始化时已经存在于内存中的对象。
  • 通过解包操作(unpacking),元组第一标识符(为 y)分配给左边第一个元素(此时为 x),元组第二个标识符(为 x)分配给左边第二个元素(为 y),从而达到实现 x、y 值交换的目的。

充分利用Lazy evaluation的特性

Lazy evaluation 常被译为“延迟计算”或“惰性计算”,指的是仅仅在真正需要执行的时候才计算表达式的值。

  • 避免不必要的计算,带来性能上的提升。对于 Python 中的条件表达式 if x and y,在 x 为 false 的情况下 y 表达式的值将不再计算。而对于 if x or y,当 x 的值为 true 的时候将直接返回,不再计算 y 的值。
  • 节省空间,使得无限循环的数据结构成为可能。Python 中最典型的使用延迟计算的例子就是生成器表达式了。比如斐波那契:
def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
from itertools import islice
print(list(islice(fib(), 5)))

不推荐使用type来进行类型检查

内建函数 type(object) 用于返回当前对象的类型。可以通过与 Python 自带模块 types 中所定义的名称进行比较,根据其返回值确定变量类型是否符合要求。

所有基本类型对应的名称都可以在 types 模块中找到,然而使用 type() 函数并不适合用来进行变量类型检查。这是因为:

  • 基于内建类型扩展的用户自定义类型,type 函数并不能准确返回结果
  • 在古典类中,所有类的实例的 type 值都相等

解决方法是,如果类型有对应的工厂函数,可以使用工厂函数对类型做相应转换,否则可以使用 isinstance() 函数来检测

isinstance(object, classinfo)

其中,classinfo 可以为直接或间接类名、基本类型名称或者由它们组成的元组,该函数在 classinfo 参数错误的情况下会抛出 TypeError 异常。

# isinstance 基本用法举例如下:
>>> isinstance(2, float)
False
>>> isinstance("a", (str, unicode))
True
>>> isinstance((2, 3), (str, list, tuple)) # 支持多种类型列表
True

警惕eval()的安全漏洞

Python中eval()函数将字符串当成有效的表达式来求值并返回计算结果。其函数声明如下:

eval(expression[, globals[, locals]])

其中,参数 globals 为字典形式,locals 为任何映射对象,它们分别表示全局和局部命名空间。如果传入 globals 参数的字典中缺少 builtins 的时候,当前的全局命名空间将作为 globals 参数输入并且在表达式计算之前被解析。locals 参数默认与 globals 相同,如果两者都省略的话,表达式将在 eval() 调用的环境中执行。

eval 存在安全漏洞,一个简单的例子:

import sys
from math import *
def ExpCalcBot(string):
    try:
        print "Your answer is", eval(user_func) # 计算输入的值
    except NameError:
        print "The expression you enter is not valid"
print 'Hi, I am ExpCalcBot. please input your expression or enter e to end'
inputstr = ''
while True:
    print 'Please enter a number or operation. Enter c to complete. :'
    inputstr = raw_input()
    if inputstr == str('e'): # 遇到输入为 e 的时候退出
        sys.exit()
    elif repr(inputstr) != repr(''):
        ExpCalcBot(inputstr)
        inputstr = ''

由于网络环境下运行它的用户并非都是可信任的,比如输入 __import__("os").system("dir"),会显示当前目录下的所有文件列表;如果恶意输入__import__("os").system("del * /Q"),会导致当前目录下的所有文件都被删除了,而这一切没有任何提示。

在 globals 参数中禁止全局命名空间的访问:

def ExpCalcBot(string):
    try:
        math_fun_list = ["acos", "asin", "atan", "cos", "e", "log", "log10", "pi", "pow", "sin", "sqrt", "tan"]
        math_fun_dict = dict([(k, globals().get(k)) for k in math_fun_list]) # 形成可以访问的函数的字典
        print "Your name is", eval(string, {"__builtins__": None}, math_fun_dict)
    except NameError:
        print "The expression you enter is not valid"

再次进行恶意输入:[c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == "Quitter"][0](0)()

# ().__class__.__bases__[0].__subclasses__() 用来显示 object 类的所有子类。类 Quitter 与 "quit" 功能绑定,因此上面的输入会导致程序退出。

对于有经验的侵入者来说,他可能会有一系列强大的手段,使得 eval 可以解释和调用这些方法,带来更大的破坏。此外,eval() 函数也给程序的调试带来一定困难,要查看 eval() 里面表达式具体的执行过程很难。因此在实际应用过程中如果使用对象不是信任源,应该避免使用 eval,在需要使用 eval 的地方可用安全性更好的ast.literal_eval替代。

使用enumerate()获取序列迭代的索引和值

使用函数 enumerate(),主要是为了解决在循环中获取索引以及对应值的问题。它具有一定的惰性(lazy),每次只在需要的时候才会产生一个(index, item)对。函数签名如下:

enumerate(sequence, start=0)

例子:

# 使用 enumerate() 获取序列迭代的索引和值
li = ['a', 'b', 'c', 'd', 'e']
for i, e in enumerate(li):
    print("index:", i, "element:", e)

区分==与is的适用场景

  • ==:用来检验两个对象的是否相等的。它实际调用内部 __eq__() 方法,因此 a == b 相当于 a.__eq__(b)

  • is:用来比较两个对象在内存中是否拥有同一块内存空间。仅当 x 和 y 是同一个对象的时候才返回 True,x is b 基本相当于 id(x) == id(y)

== 操作符也是可以被重载的,而 is 不能被重载。一般情况下,如果 x is y 为 True , x == y 的值一般也为 True(特殊情况除外,如 NaNa = float('NaN')a is a 为 True,a == a 为 false)。

构建合理的包层次来管理模块

每一个 Python 文件都可以看成一个模块(module),使用模块可以增强代码的可维护性和可重用性。

包即是目录,但与普通目录不同,它除了包含常规的 Python 文件(也就是模块)以外,还包含一个 __init__.py 文件,同时它允许嵌套

Package/__init__.py
    Module1.py
    Module2.py
    Subpackage/__init__.py
        Module1.py
        Module2.py

包中的模块可以通过"."访问符进行访问,即"包名.模块名"。有以下几种导入方法:

  • 直接导入一个包:

    import Package

  • 导入子模块或子包,包嵌套的情况下可以进行嵌套导入:

    from Package import Module1
    import Package.Module1
    from Package import Subpackage
    import Package.Subpackage
    from Package.Subpackage import Module1
    import Package.Subpackage.Module1
    

__init__.py 的作用:

  • 使包和普通目录区分
  • 可以在该文件中申明模块级别的 import 语句,从而使其变成包级别可见

如果 __init__.py 文件为空,当意图使用 from Package import * 将包 Package 中所有的模块导入当前名字空间时,并不能使得导入的模块生效,这是因为不同平台间的文件的命名规则不同,Python 解释器并不能正确判定模块在对应的平台该如何导入,因此仅仅执行 __init__.py 文件,如果要控制模块的导入,则需要对 __init__.py 文件做修改。

__init__.py 文件还有一个作用就是通过在该文件中定义 __all__ 变量,控制需要导入的子包或者模块。之后再运行 from ... import *,可以看到 __all__ 变量中定义的模块和包被导入当前名字空间。

包的使用能够带来以下便利:

  • 合理组织代码,便于维护和使用
  • 能够有效地避免名称空间冲突

如果模块包含的属性和方法存在同名冲突,使用 import module 可以有效地避免名称冲突。在嵌套的包结构中,每一个模块都以其所在的完整路径作为其前缀,因此,即使名称一样,但由于模块所对应的其前缀不同,就不会产生冲突。

本篇文章由一文多发平台ArtiPub自动发布

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容