改善 python 程序的 91 个建议(一)-mile米乐体育

建议 1:理解 pythonic 概念

pythonic

tim peters 的 《the zen of python》相信学过 python 的都耳熟能详,在交互式环境中输入import this可以查看,其实有意思的是这段 python 之禅的源码:

d = {} for c in (65, 97):     for i in range(26):         d[chr(i c)] = chr((i 13) % 26   c)  print "".join([d.get(c, c) for c in s])

哈哈哈,相信这是大佬在跟我们举反例吧。

书中还举了一个快排的例子:

def quicksort(array):     less = []     greater = []     if len(array) <= 1:         return array     pivot =array.pop()     for x in array:         if x <= pivot:             less.append(x)         else:             greater.append(x)     return quicksort(less)   [pivot]   quicksort(greater)

代码风格

通过对语法、库和应用程序的理解来编写代码,充分体现 python 自身的特色:

# 变量交换 a, b = b, a # 上下文管理 with open(path, 'r') as f:     do_sth_with(f) # 不应当过分地追求奇技淫巧 a = [1, 2, 3, 4] a[::-1] # 不推荐。好吧,自从学了切片我一直用的这个 list(reversed(a))   # 推荐

然后表扬了 flask 框架,提到了 generator 之类的特性尤为 pythonic,有个包和模块的约束:

  • 包和模块的命名采用小写、单数形式,而且短小
  • 包通常仅作为命名空间,如只含空的__init__.py文件

建议 2:编写 pythonic 代码

命名的规范:

def find_num(searchlist, num):     for listvalue in searchlist:         if num == listvalue:             return true         else:             pass

尝试去通读官方手册,掌握不断发展的新特性,这将使你编写代码的执行效率更高,推荐深入学习 flask、gevent 和 requests。

建议 3:理解 python 与 c 语言的不同之处

提到了三点:

  • python 使用代码缩进的方式来分割代码块,不要混用 tab 键和空格
  • python 中单、双引号的使用
  • 三元操作符:x if bool else y

建议 4:在代码中适当添加注释

这一点已经受教了,现在编写代码都会合理地加入块注释、行注释和文档注释,可以使用__doc__输出。

建议 5:通过适当添加空行使代码布局更为优雅、合理

建议 6:编写函数的 4 个原则

  1. 函数设计要尽量短小,嵌套层次不宜过深
  2. 函数申明应该做到合理、简单、易于使用
  3. 函数参数设计应该考虑向下兼容
  4. 一个函数只做一件事,尽量保证函数语句粒度的一致性

python 中函数设计的好习惯还包括:不要在函数中定义可变对象作为默认值,使用异常替换返回错误,保证通过单元测试等。

# 关于函数设计的向下兼容 def readfile(filename):         # 第一版本     pass def readfile(filename, log):    # 第二版本     pass def readfile(filename, logger=logger.info):     # 合理的设计     pass

最后还有个函数可读性良好的例子:

def getcontent(serveradr, pagepath):     http = httplib.http(serveradr)     http.putrequest('get', pagepath)     http.putheader('accept', 'text/html')     http.putheader('accept', 'text/plain')     http.endheaders()     httpcode, httpmsg, headers = http.getreply()     if httpcode != 200:         raise "could not get document: check url and path."     doc = http.getfile()     data = doc.read()       # 此处是不是应该使用 with ?     doc.close     return data  def extractdata(inputstring, start_line, end_line):     lstr = inputstring.splitlines()             # split     j = 0     for i in lstr:         j  = 1         if i.strip() == start_line: slice_start = j         elif i.strip() == end_line: slice_end = j     return lstr[slice_start:slice_end]  def sendemail(sender, receiver, smtpserver, username, password, content):     subject = "contented get from the web"     msg = mimetext(content, 'plain', 'utf-8')     msg['subject'] = header(subject, 'utf-8')     smtp = smtplib.smtp()     smtp.connect(smtpserver)     smtp.login(username, password)     smtp.sendmail(sender, receiver, msg.as_string())     smtp.quit()

建议 7:将常量集中到一个文件

在 python 中应当如何使用常量:

  • 通过命名风格提醒使用者该变量代表常量,如常量名全部大写
  • 通过自定义类实现常量功能:将存放常量的文件命名为constant.py,并在其中定义一系列常量
class _const:     class consterror(typeerror): pass     class constcaseerror(consterror): pass      def __setattr__(self, name, value):         if self.__dict__.has_key(name):             raise self.consterror, "can't change const.%s" % name         if not name.isupper():             raise self.constcaseerror, \                     'const name "%s" is not all uppercase' % name         self.__dict__(name) = value  import sys sys.modules[__name__] = _const() import const const.my_constant = 1 const.my_second_constant = 2 const.my_third_constant = 'a' const.my_forth_constant = 'b'

其他模块中引用这些常量时,按照如下方式进行即可:

from constant import const print(const.my_constant)

建议 8:利用 assert 语句来发现问题

>>> y = 2 >>> assert x == y, "not equals" traceback (most recent call last):   file "", line 1, in  assertionerror: not equals >>> x = 1 >>> y = 2 # 以上代码相当于 >>> if __debug__ and not x == y: ...     raise assertionerror("not equals") ...  traceback (most recent call last):   file "", line 2, in  assertionerror: not equals

运行是加入-o参数可以禁用断言。

建议 9:数据交换的时候不推荐使用中间变量

>>> timer('temp = x; x = y; y = temp;', 'x = 2; y = 3').timeit() 0.059251302998745814 >>> timer('x, y = y, x', 'x = 2; y = 3').timeit() 0.05007316499904846

对于表达式x, y = y, x,在内存中执行的顺序如下:

  1. 先计算右边的表达式y, x,因此先在内存中创建元组(y, x),其标识符和值分别为y, x及其对应的值,其中y和x是在初始化已经存在于内存中的对象
  2. 计算表达式左边的值并进行赋值,元组被依次分配给左边的标识符,通过解压缩,元组第一标识符y分配给左边第一个元素x,元组第二标识符x分配给左边第一个元素y,从而达到交换的目的

下面是通过字节码的分析:

>>> import dis >>> def swap1(): ...     x = 2 ...     y = 3 ...     x, y = y, x ...  >>> def swap2(): ...     x = 2 ...     y = 3 ...     temp = x ...     x = y ...     y = temp ...  >>> dis.dis(swap1)   2           0 load_const               1 (2)               3 store_fast               0 (x)    3           6 load_const               2 (3)               9 store_fast               1 (y)    4          12 load_fast                1 (y)              15 load_fast                0 (x)              18 rot_two                             # 交换两个栈的最顶层元素              19 store_fast               0 (x)              22 store_fast               1 (y)              25 load_const               0 (none)              28 return_value >>> dis.dis(swap2)                                                                                                                                       2           0 load_const               1 (2)               3 store_fast               0 (x)    3           6 load_const               2 (3)               9 store_fast               1 (y)    4          12 load_fast                0 (x)              15 store_fast               2 (temp)    5          18 load_fast                1 (y)              21 store_fast               0 (x)    6          24 load_fast                2 (temp)              27 store_fast               1 (y)              30 load_const               0 (none)              33 return_value

建议 10:充分利用 lazy evaluation 的特性

def fib():     a, b = 0, 1     while true:         yield a         a, b = b, a   b

哈哈哈,我猜到肯定是生成器实现菲波拉契序列的例子,不过对比我写的版本,唉。。。

建议 11:理解枚举替代实现的缺陷

利用 python 的动态特征,可以实现枚举:

# 方式一 class seasons:     spring, summer, autumn, winter = range(4) # 方式二 def enum(*posarg, **keysarg):     return type("enum", (object,), dict(zip(posarg, range(len(posarg))), **keysarg)) seasons = enum("spring", "summer", "autumn", winter=1) seasons.spring # 方式三 >>> from collections import namedtuple >>> seasons = namedtuple('seasons', 'spring summer autumn winter')._make(range(4)) >>> seasons.spring 0 # 但通过以上方式实现枚举都有不合理的地方 >>> seasons._replace(spring=2)                                             │ seasons(spring=2, summer=1, autumn=2, winter=3)   # python3.4 中加入了枚举,仅在父类没有任何枚举成员的时候才允许继承

建议 12:不推荐使用 type 来进行类型检查

作为动态语言,python 解释器会在运行时自动进行类型检查并根据需要进行隐式类型转换,当变量类型不同而两者之间又不能进行隐式类型转换时便抛出typeerror异常。

>>> def add(a, b): ...     return a   b ...  >>> add(1, 2j) (1 2j) >>> add('a', 'b') 'ab' >>> add(1, 2) 3 >>> add(1.0, 2.3) 3.3 >>> add([1, 2], [3, 4]) [1, 2, 3, 4] >>> add(1, 'a') traceback (most recent call last):   file "", line 1, in    file "", line 2, in add typeerror: unsupported operand type(s) for  : 'int' and 'str'

所以实际应用中,我们常常需要进行类型检查,但是不推荐使用type(),因为基于内建类型扩展的用户自定义类型,type()并不能准确返回结果:

class userint(int):     def __init__(self, val=0):         self._val = int(val)     def __add__(self, val):         if isinstance(val, userint):             return userint(self._val   val._val)         return self._val   val     def __iadd__(self, val):         raise notimplementederror("not support operation")     def __str__(self):         return str(self._val)     def __repr__(self):         return "integer %s" % self._val >>> n = userint() >>> n integer 0 >>> print(n) 0 >>> m = userint(2) >>> print(m) 2 >>> type(n) is int false                   # 显然不合理 >>> isinstance(n, int) true

我们可以使用isinstance来检查:isinstance(object, classinfo)

建议 13:尽量转换为浮点类型后再做除法

# 计算平均成绩绩点 >>> gpa = ((4*96 3*85 5*98 2*70)*4) / ((4 3 5 2)*100) >>> gpa 3.625714285714286   # 终于知道自己的绩点是咋算的了

建议 14:警惕 eval() 的安全漏洞

eval(expression[, globals[, locals]])将字符串 str 当成有效的表达式来求值并返回计算结果,globas为字典形式,locals为任何映射对象,它们分别表示全局和局部命名空间,两者都省略表达式将在调用的环境中执行,为什么需要警惕eval()呢:

# 合理正确地使用 >>> eval("1 1==2") true >>> eval('"a" "b"') 'ab' # 坏心眼的geek >>> eval('__import__("os").system("dir")') desktop  documents  downloads  examples.desktop  music  pictures  public  __pycache__  templates  videos 0 >>> eval('__import__("os").system("del * /q")')     # 嘿嘿嘿

如果确实需要使用eval,建议使用安全性更好的ast.literal_eval。

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

>>> li = ['a', 'b', 'c', 'd', 'e'] >>> for i, e in enumerate(li): ...     print('index: ', i, 'element: ', e) ...  index:  0 element:  a index:  1 element:  b index:  2 element:  c index:  3 element:  d index:  4 element:  e # enumerate(squence, start=0) 内部实现 def enumerate(squence, start=0):     n = start     for elem in sequence:         yield n, elem   # 666         n  = 1 # 明白了原理我们自己也来实现一个反序的 def reversed_enumerate(squence):     n = -1     for elem in reversed(sequence):         yield len(sequence)   n, elem         n -= 1

建议 16:分清 == 与 is 的适用场景

操作符意义isobject identity==equal

is的作用是用来检查对象的标示符是否一致,也就是比较两个对象在内存中是否拥有同一块内存空间,相当于id(x) == id(y),它并不适用于判断两个字符串是否相等。==才是用来判断两个对象的值是否相等,实际是调用了内部的__eq__,所以a==b相当于a.__eq__(b),也就是说==是可以被重载的,而is不能被重载。

>>> s1 = 'hello world' >>> s2 = 'hello world' >>> s1 == s2 true >>> s1 is s2 false >>> s1.__eq__(s2) true >>> a = 'hi' >>> b = 'hi' >>> a == b true >>> a is b true

咦~怎么上例中的a, b又是“同一对象”了?这跟 python 的 string interning 机制有关,为了提高系统性能,对于较小的字符串会保留其值的一个副本,当创建新的字符串时直接指向该副本,所以a和b的 id 值是一样的,同样对于小整数[-5, 257)也是如此:

>>> id(a) 140709793837832 >>> id(b) 140709793837832 >>> x = -5 >>> y = -5 >>> x is y true >>> id(x) == id(y) true

建议 17:考虑兼容性,尽可能使用 unicode

我之前也总结过编码的问题。由于最早的编码是 ascii 码,只能表示 128 个字符,显然这对其它语言编码并不适用,unicode就是为了不同的文字分配一套统一的编码。

建议 18:构建合理的包层次来管理 module

本质上每一个 python 文件都是一个模块,使用模块可以增强代码的可维护性和可重用性,在较大的项目中,我们需要合理地组织项目层次来管理模块,这就是包(package)的作用。

一句话说包:一个包含__init__.py 文件的目录。包中的模块可以通过.进行访问,即包名.模块名。那么这个__init__.py文件有什么用呢?最明显的作用就是它区分了包和普通目录,在该文件中申明模块级别的 import 语句从而变成了包级别可见,另外在该文件中定义__all__变量,可以控制需要导入的子包或模块。

这里给出一个较为合理的包组织方式,是flaskweb 开发:基于python的web应用开发实战一书中推荐而来的:

|-flasky     |-app/                      # flask 程序         |-templates/            # 存放模板         |-static/               # 静态文件资源         |-main/             |-__init__.py             |-errors.py         # 蓝本中的错误处理程序             |-forms.py          # 表单对象             |-views.py          # 蓝本中定义的程序路由         |-__init__.py         |-email.py              # 电子邮件支持         |-models.py             # 数据库模型     |-migrations/               # 数据库迁移脚本     |-tests/                    # 单元测试         |-__init__.py         |-test*.py     |-venv/                     # 虚拟环境     |-requirements/         |-dev.txt               # 开发过程中的依赖包         |-prod.txt              # 生产过程中的依赖包     |-config.py                 # 储存程序配置     |-manage.py                 # 启动程序以及其他的程序任务

建议 19:有节制地使用 from...import 语句

python 提供三种方式来引入外部模块:import语句、from...import语句以及__import__函数,其中__import__函数显式地将模块的名称作为字符串传递并赋值给命名空间的变量。

使用import需要注意以下几点:

  • 优先使用import a的形式
  • 有节制地使用from a import a
  • 尽量避免使用from a import *

为什么呢?我们来看看 python 的 import 机制,python 在初始化运行环境的时候会预先加载一批内建模块到内存中,同时将相关信息存放在sys.modules中,我们可以通过sys.modules.items()查看预加载的模块信息,当加载一个模块时,解释器实际上完成了如下动作:

  1. 在sys.modules中搜索该模块是否存在,如果存在就导入到当前局部命名空间,如果不存在就为其创建一个字典对象,插入到sys.modules中
  2. 加载前确认是否需要对模块对应的文件进行编译,如果需要则先进行编译
  3. 执行动态加载,在当前命名空间中执行编译后的字节码,并将其中所有的对象放入模块对应的字典中
>>> dir() ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__'] >>> import test testing module import >>> dir() ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'test'] >>> import sys >>> ‘test’ in sys.modules.keys() true >>> id(test) 140367239464744 >>> id(sys.modules['test']) 140367239464744 >>> dir(test) ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b'] >>> sys.modules['test'].__dict__.keys() dict_keys(['__file__', '__builtins__', '__doc__', '__loader__', '__package__', '__spec__', '__name__', 'b', 'a', '__cached__'])

从上可以看出,对于用户自定义的模块,import 机制会创建一个新的 module 将其加入当前的局部命名空间中,同时在 sys.modules 也加入该模块的信息,但本质上是在引用同一个对象,通过test.py所在的目录会多一个字节码文件。

建议 20:优先使用 absolute import 来导入模块

建议 21: i =1 不等于 i

首先 i或--i在 python 语法上是合法,但并不是我们通常理解的自增或自减操作:

>>>   1     #  ( 1) 1 >>> --1     # -(-1) 1 >>>    2 2 >>> ---2 -2

原来 或-只表示正负数符号。

建议 22:使用 with 自动关闭资源

对于打开的资源我们记得关闭它,如文件、数据库连接等,python 提供了一种简单优雅的mile米乐体育的解决方案:with。

先来看with实现的原理吧。

with的实现得益于一个称为上下文管理器(context manager)的东西,它定义程序运行时需要建立的上下文,处理程序的进入和退出,实现了上下文管理协议,即对象中定义了__enter__()和__exit__(),任何实现了上下文协议的对象都可以称为一个上下文管理器:

  • __enter__():返回运行时上下文相关的对象
  • __exit__(exception_type, exception_value, traceback):退出运行时的上下文,处理异常、清理现场等

包含with语句的代码块执行过程如下:

with 表达式 [as 目标]:     代码块 # 例 >>> with open('test.txt', 'w') as f: ...     f.write('test') ...  4 >>> f.__enter__  >>> f.__exit__ 
  1. 计算表达式的值,返回一个上下文管理器对象
  2. 加载上下文管理器对象的__exit__()以备后用
  3. 调用上下文管理器对象的__enter__()
  4. 将__enter__()的返回值赋给目标对象
  5. 执行代码块,正常结束调用__exit__(),其返回值直接忽略,如果发生异常,会调用__exit__()并将异常类型、值及 traceback 作为参数传递给__exit__(),__exit__()返回值为 false 异常将会重新抛出,返回值为 true 异常将被挂起,程序继续执行

于此,我们可以自定义一个上下文管理器:

>>> class mycontextmanager(object): ...     def __enter__(self): ...         print('entering...') ...     def __exit__(self, exception_type, exception_value, traceback): ...         print('leaving...') ...         if exception_type is none: ...             print('no exceptions!') ...             return false ...         elif exception_type is valueerror: ...             print('value error!') ...             return true ...         else: ...             print('other error') ...             return true ...  >>> with mycontextmanager(): ...     print('testing...') ...  entering... testing... leaving... no exceptions! >>> with mycontextmanager(): ...     print('testing...') ...     raise(valueerror) ...  entering... testing... leaving... value error!

python 还提供contextlib模块,通过 generator 实现,其中的 contextmanager 作为装饰器来提供一种针对函数级别上的上下文管理器,可以直接作用于函数/对象而不必关心__enter__()和__exit__()的实现。

推荐文章

建议 23:使用 else 子句简化循环(异常处理)

python 的 else 子句提供了隐含的对循环是否由 break 语句引发循环结束的判断,有点绕哈,来看例子:

>>> def print_prime(n): ...     for i in range(2, n): ...         for j in range(2, i): ...             if i % j == 0: ...                 break ...         else: ...             print('{} is a prime number'.format(i)) ...  >>> print_prime(7) 2 is a prime number 3 is a prime number 5 is a prime number

可以看出,else 子句在循环正常结束和循环条件不成立时被执行,由 break 语句中断时不执行,同样,我们可以利用这颗语法糖作用在 while 和 try...except 中。

改善 python 程序的 91 个建议(二)

展开全文
内容来源于互联网和用户投稿,文章中一旦含有米乐app官网登录的联系方式务必识别真假,本站仅做信息展示不承担任何相关责任,如有侵权或涉及法律问题请联系米乐app官网登录删除

最新文章

网站地图