如何像Python高手(Pythonista)一样编程

jopen 9年前

最近在网上看到一篇介绍Pythonic编程的文章:Code Like a Pythonista: Idiomatic Python,其实作者在2006的PyCon会议后就写了这篇文章,写这篇文章的主要原因是作者发现很多有经验的Pythoner写出的代码不够Pythonic。我觉得这篇文章很不错,所以将它用中文写了下来(不是逐字的翻译,中间加了一些自己的理解),分享给大家。另:由于本人平时时间有限,这篇文章翻译了比较长的时间,如果你发现了什么不对的地方,欢迎指出。。


一、Python之禅(The Zen of Python)


The Zen of Python是Python语言的指导原则,遵循这些基本原则,你就可以像个Pythonista一样编程。具体内容你可以在Python命令行输入import this看到:

The Zen of Python, by Tim Peters    Beautiful is better than ugly.  # 优美胜于丑陋(Python以编写优美的代码为目标)    Explicit is better than implicit.  # 明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)    Simple is better than complex.  # 简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)    Complex is better than complicated.  # 复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)    Flat is better than nested.  # 扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)    Sparse is better than dense.  # 间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)    Readability counts.  # 可读性很重要(优美的代码是可读的)    Special cases aren't special enough to break the rules.  Although practicality beats purity.  # 即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)    Errors should never pass silently.  Unless explicitly silenced.  # 不要包容所有错误,除非你确定需要这样做(精准地捕获异常,不写except:pass风格的代码)    In the face of ambiguity, refuse the temptation to guess.  # 当存在多种可能,不要尝试去猜测    There should be one-- and preferably only one --obvious way to do it.  # 而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)    Although that way may not be obvious at first unless you're Dutch.  # 虽然这并不容易,因为你不是 Python 之父(这里的Dutch是指Guido)  Now is better than never.  Although never is often better than *right* now.  # 做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)  If the implementation is hard to explain, it's a bad idea.  If the implementation is easy to explain, it may be a good idea.  # 如果你无法向人描述你的方案,那肯定不是一个好方案;反之亦然(方案测评标准)  Namespaces are one honking great idea -- let's do more of those!  # 命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

这首特别的“诗”开始作为一个笑话,但它确实包含了很多关于Python背后的哲学真理。Python之禅已经正式成文PEP 20,具体内容见:PEP 20


二、PEP8: Python编码规范(PEP8: Style Guide for Python Code)


Abelson & Sussman在《计算机程序的构造和解释》一书中说道:程序是写来给人读的,只是顺带让机器执行。所以,我们在编码时应该尽量让它更易读懂。PEP8是Python的编码规范,官方文档见:PEP 8,PEP是Python Enhancement Proposal的缩写。PEP8包括很多编码的规范,下面主要介绍一下缩进和命名等内容。

空格和缩进(WhiteSpace and Indentation)

空格和缩进在Python语言中非常重要,它替代了其他语言中{}的作用,用来区分代码块和作用域。在这方面PEP8有以下的建议:

1、每次缩进使用4个空格  2、不要使用Tab,更不要Tab和空格混用  3、两个方法之间使用一个空行,两个Class之间使用两个空行  4、添加一个空格在字典、列表、序列、参数列表中的“,“后,以及在字典中的”:“之后,而不是之前  5、在赋值和比较两边放置一个空格(参数列表中除外)  6、紧随括号后面或者参数列表前一个字符不要存在空格

    Python命名

    命名规范是编程语言的基础,而且大部分的规范对于高级语言来说都是一样的,Python的基本规范如下:

1、方法 & 属性:joined_lower  2、常量:joined_lower or ALL_CAPS  3、类:StudlyCaps  4、类属性:interface, _internal, __private  5、camelCase only to conform to pre-existing conventions

    以上内容只是对PEP8做了非常简单的介绍,由于今天的主题不在于此,所以就不在这里多讲。想要更加深入的了解Python编码规范,可以阅读PEP8官方文档Google Python编码规范等内容。


    三、交换变量值(Swap Values)


    在其他语言中,交换两个变量值的时候,可以这样写:

temp = a  a = b  b = temp

    在Python中,我们可以简单的这样写:

b, a = a, b

    可能你已经在其他地方见过这种写法,但是你知道Python是如何实现这种语法的吗?首先,逗号(,)是Python中tuple数据结构的语法;上面的语法会执行一下的操作:

    1、Python会先将右边的a, b生成一个tuple(元组),存放在内存中;

    2、之后会执行赋值操作,这时候会将tuple拆开;

    3、然后将tuple的第一个元素赋值给左边的第一个变量,第二个元素赋值给左边第二个变量。

    再举个tuple拆分的例子:

In [1]: people = ['David', 'Pythonista', '15145551234']    In [2]: name, title, phone = people    In [3]: name  Out[3]: 'David'    In [4]: title  Out[4]: 'Pythonista'    In [5]: phone  Out[5]: '15145551234'

    这种语法在For循环中非常实用:

In [6]: people = [['David', 'Pythonista', '15145551234'], ['Wu', 'Student', '15101365547']]    In [7]: for name, title, phone in people:     ...:     print name,  phone     ...:       David 15145551234  Wu 15101365547

    PS:在使用这种语法时,需要确保左边的变量个数和右边tuple的个数一致,否则,Python会抛出ValueError异常。

    更多tuple的例子:

>>> 1,  (1,)  >>> (1,)  (1,)  >>> (1)  1  >>> value = 1,  >>> value  (1,)

    我们知道:逗号(,)在Python中是创建tuple的构造器,所以我们可以按照上面的方式很方便的创建一个tuple;需要注意的是:如果声明只有一个元素的tuple,末尾必须要带上逗号,两个以上的元素则不需要。声明tuple的语法很简单,但同时它也比较坑:如果你发现Python中的变量不可思议的变成了tuple,那很可能是因为你多写了一个逗号。。


    四、Python控制台的"_"(Interactive "_")


    这是Python中比较有用的一个功能,不过有很多人不知道(我也是接触Python很久之后才知道的)。。在Python的交互式控制台中,当你计算一个表达式或者调用一个方法的时候,运算的结果都会放在一个临时的变量 _ 里面。_(下划线)用来存储上一次的打印结果,比如:

>>> import math  >>> math.pi / 3  1.0471975511965976  >>> angle = _  >>> math.cos(angle)  0.50000000000000011  >>> _  0.50000000000000011

    PS:当返回结果为None的时候,控制台不会打印,里面存储的值也就不会改变。


    五、合并字符串(Building Strings from Sub strings)


    假如现在有一个list,里面是一些字符串,你现在需要将它们合并成一个字符串,最简单的方法,你可以按照下面的方式去处理:

colors = ['red', 'blue', 'green', 'yellow']    result = ''  for s in colors:      result += s

    但是,很快你会发现:这种方法非常低效,尤其当list非常大的时候。Python中的字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串。所以,上面的方法会消耗很大的内存:它需要计算,存储,同时扔掉中间的计算结果。正确的方法是使用Python中的join方法:

result = ','.join(colors)

    当合并元素比较少的时候,使用join方法看不出太大的效果;但是当元素多的时候,你会发现join的效率还是非常明显的。不过,在使用的时候请注意:join只能用于元素是字符串的list,它不会进行任何的强制类型转换。连接一个存在一个或多个非字符串元素的list时将抛出异常。


    六、使用关键字in(Use in where possible)


    当你需要判断一个KEY是否在dict中或者要遍历dict的KEY时,最好的方法是使用关键字in:

d = {'a': 1, 'b': 2}  if 'c' in d:      print True  # DO NOT USE  if d.has_key('c'):      print True    for key in d:      print key  # DO NOT USE  for key in d.keys():      print key

    Python的dict对象是对KEY做过hash的,而keys()方法会将dict中所有的KEY作为一个list对象;所以,直接使用in的时候执行效率会比较快,代码也更简洁。


    七、字典(Dictionary)


    dict是Python内置的数据结构,在写Python程序时会经常用到。这里介绍一下它的get方法和defaultdict方法。

    1、get

    在获取dict中的数据时,我们一般使用index的方式,但是如果KEY不存在的时候会抛出KeyError。这时候你可以使用get方法,使用方法:dict.get(key, default=None),可以避免异常。例如:

d = {'a': 1, 'b': 2}  print d.get('c')        # None  print d.get('c', 14)    # 14

    2、fromkeys

    dict本身有个fromkeys方法,可以通过一个list生成一个dict,不过得提供默认的value,例如:

# ⽤序列做 key,并提供默认value  >>> dict.fromkeys(['a', 'b', 'c'], 1)  # {'a': 1, 'c': 1, 'b': 1}

    3、setdefault

    有些情况下,我们需要给dict的KEY一个默认值,你可以这样写:

equities = {}  for (portfolio, equity) in data:      if portfolio in equities:          equities[portfolio].append(equity)      else:          equities[portfolio] = [equity]

    上面的实现方式很麻烦,使用dict的setdefault(key, default)方法会更简洁,更效率。

equities = {}  for (portfolio, equity) in data:      equities.setdefault(portfolio, []).append(equity)

    setdefault方法相当于"get, or set & get",或者相当于"set if necessary, then get"


    八、defaultdict


    defaultdict是Python2.5之后引入的功能,具体的用法我已经在另外一篇文章中详细介绍:Python的defaultdict模块和namedtuple模块


    九、字典的组装和拆分(Building & Splitting Dictionaries)


    在Python中,你可以使用zip方法将两个list组装成一个dict,其中一个list的值作为KEY,另外一个list的值作为VALUE:

>>> given = ['John', 'Eric', 'Terry', 'Michael']  >>> family = ['Cleese', 'Idle', 'Gilliam', 'Palin']  >>> pythons = dict(zip(given, family))  >>> print pythons  {'John': 'Cleese', 'Michael': 'Palin', 'Eric': 'Idle', 'Terry': 'Gilliam'}

    相反的,你可以使用dict的keys()和values()方法来获取KEY和VALUE的列表:

>>> pythons.keys()  ['John', 'Michael', 'Eric', 'Terry']  >>> pythons.values()  ['Cleese', 'Palin', 'Idle', 'Gilliam']

    需要注意的是:由于dict本身是无序的,所以通过keys()和values()方法获得的list的顺序已经和原始的list不一样了。。


    十、Python的True值(Truth Values)


    在Python中,判断一个变量是否为True的时候,你可以这样做:

# 这样写  if x:      pass  # !不要这样写  if x == True:      pass    # 对于list,要这样写  if items:      pass  # !不要这样写  if len(items) == 0:      pass

    Python中的真值对象有以下几个:

False True
False (== 0) True (== 1)
"" (空字符串) 除 "" 之外的字符串(" ""anything")
0.0   之外的数字(1, 0.1, -1, 3.14)
[](){}set() 非空的list,tuple,set和dict ([0](None,)[''])
None 大部分的对象,除了明确指定为False的对象

    对于自己声明的class,如果你想明确地指定它的实例是True或False,你可以自己实现class的__nonzero__或__len__方法。当你的class是一个container时,你可以实现__len__方法,如下:

class MyContainer(object):        def __init__(self, data):          self.data = data        def __len__(self):          """ Return my length. """          return len(self.data)

如果你的class不是container,你可以实现__nonzero__方法,如下:

class MyClass(object):        def __init__(self, value):          self.value = value        def __nonzero__(self):          """ Return my truth value (True or False). """          # This could be arbitrarily complex:          return bool(self.value)

    在Python 3.x中,__nonzero__方法被__bool__方法替代。考虑到兼容性,你可以在class定义中加上以下的代码:

__bool__ = __nonzero__


    十一、enumerate:索引和元素(Index & Item: enumerate)


    在Python中,我们在遍历列表的时候,可以通过enumerate方法来获取遍历时的index,比如:

>>> items = 'zero one two three'.split()  >>> print list(enumerate(items))  [(0, 'zero'), (1, 'one'), (2, 'two'), (3, 'three')]  >>> for (index, item) in enumerate(items):      print index, item

    enumerate方法是惰性方法,所以它只会在需要的时候生成一项,也因此在上述代码print的时候需要包装一个list。enumerate其实是一个生成器(generator),这个下面会讲到。使用enumerate之后,for循环变得很简单:

for (index, item) in enumerate(items):      print index, item    # compare:  index = 0  for item in items:      print index, item      index += 1    # compare:  for i in range(len(items)):      print i, items[i]

    使用enumerate的代码比其他两个都短,而且更简单,更容易读懂。下面的例子可以说明一下enumerate实际返回的数据:一个迭代器,

>>> enumerate(items)  <enumerate object at 0x011EA1C0>  >>> e = enumerate(items)  >>> e.next()  (0, 'zero')  >>> e.next()  (1, 'one')  >>> e.next()  (2, 'two')  >>> e.next()  (3, 'three')  >>> e.next()  Traceback (most recent call last):    File "<stdin>", line 1, in ?  StopIteration


    十二、Python中的变量 & 引用(variables & names)


    在很多其他高级语言中,给一个变量赋值时会将"value"放在一个"盒子"里:

int a = 1;

    如图:


如何像Python高手(Pythonista)一样编程

    现在,盒子"a"中包含了一个整数 1;将另外一个"value"赋值给同一个变量时,会将"盒子"中的内容替换掉:

a = 2;

    如图:


如何像Python高手(Pythonista)一样编程

    现在,盒子"a"中包含了一个整数 2;将变量赋值给其他一个变量时,会将"value"拷贝一份放在一个新的"盒子"中:

int b = a;

    如图:


如何像Python高手(Pythonista)一样编程

如何像Python高手(Pythonista)一样编程

    盒子"b"是第二个"盒子",里面是整数 2的一个拷贝,盒子"a"中是另外一个拷贝。


    在Python中,变量没有数据类型,是附属于对象的标示符名称,如下图:实际,这段表明了像python,PHP这类动态脚本语言中“变量”包含了两个内容:1 标识符名称 2 标识符所对应(引用)的值(对象),也就是说“变量”不在是一个容器。

a = 1

如何像Python高手(Pythonista)一样编程

    这里,整数 1 对象有一个名字为 "a" 的变量(tag)。如果我们给变量 "a" 重新赋值,对Python来说,只是将变量(tag) "a" 指向另外一个对象:

a = 2

如何像Python高手(Pythonista)一样编程

如何像Python高手(Pythonista)一样编程

    现在,变量 "a" 是附属在整数对象 2 上面。最初的整数对象 1 已经没有指向它的变量 "a",它可能还存在,但是我们已经不能通过变量 "a"获得。当一个对象没有了指向它的引用的时候,它将会被从内存中删除(垃圾回收)。如果我们将存在的变量赋值给一个新的变量,Python会在已经存在的对象上加上一个指向自己的变量(tag)。

b = a

如何像Python高手(Pythonista)一样编程

    变量 "a"和"b" 是指向同一个整数对象的。

    PS:Python中的变量,引用等设计和其他语言不同,这里只是将原文翻译说明了一下,更多的介绍可以参看:Python中的变量、引用、拷贝和作用域


    十三、Python方法中参数的默认值(Default Parameter Values)


    对于Python初学者来说,Python的方法默认参数有一个很容易犯错的地方:在默认参数中使用可变对象,甚至有不少Python老鸟也可能会在这个问题上掉坑里,如果他们不能理解Python的对象引用。。问题如下:

def bad_append(new_item, a_list=[]):      a_list.append(new_item)      return a_list    >>> print bad_append('one')  ['one']  >>> print bad_append('two')  ['one', 'two']


    这个问题的主要原因是:a_list参数的默认值是一个空的list,它在函数定义的时候已经被创建。所以,之后每次调用该函数的时候,a_list的默认值都是这个list对象。List,dict和set是可变对象,如果想在函数中获取一个默认的list(dict or set)对象,正确的做法是在函数中创建:

def good_append(new_item, a_list=None):      if a_list is None:          a_list = []      a_list.append(new_item)      return a_list


    十四、字符串格式化(String Formatting)


    在许多编程语言中都包含有格式化字符串的功能,比如C语言中的格式化输入输出。Python中内置有对字符串进行格式化的操作符 "%" 以及str.format()方法。


    1、操作符 "%"

    Python中的 "%" 操作符和C语言中的sprintf类似。简单来说,使用 "%" 来格式化字符串的时候,你需要提供一个字符串模板和用来插入的值。模板中有格式符,这些格式符为真实值预留位置,并说明真实数值应该呈现的格式。Python用一个tuple将多个值传递给模板,每个值对应一个格式符。注意:给定的值一定要和模板中的格式符一一对应!

name = 'xianglong'  messages = 3  text = ('Hello %s, you have %i messages' % (name, messages))  print text    # Output: Hello xianglong, you have 3 messages

    在上面的例子中,"Hello %s, you have %i messages" 是字符串模板。%s为第一个格式符,表示一个字符串。%i为第二个格式符,表示一个十进制整数。(name, messages)的两个元素为替换%s和%i的真实值。在模板和tuple之间,有一个%号分隔,它代表了格式化操作。

    常用的格式符如下:

格式 描述
%% 百分号 % 标记
%s 字符串 (采用str()的显示)
%r 字符串 (采用repr()的显示)
%c 字符及其ASCII码
%b 二进制整数
%d 十进制整数 (有符号整数)
%u 十进制整数 (无符号整数)
%i 十进制整数 (有符号整数)
%o 八进制整数 (无符号整数)
%x 十六进制整数 (无符号整数)
%X 十六进制整数 (无符号整数)
%e 指数 (基底写为e)
%E 指数 (基底写为E)
%f 浮点数
%F 浮点数,与上相同
%g 指数(e)或浮点数 (根据显示长度)
%G 指数(E)或浮点数 (根据显示长度)
%p 指针(用十六进制打印值的内存地址)
%n 存储输出字符的数量放进参数列表的下一个变量中


    使用操作符 "%" 也可以通过字典格式化字符串:

values = {'name': name, 'messages': messages}  print ('Hello %(name)s, you have %(messages)i messages' % values)    # Output: Hello xianglong, you have 3 messages

    上面的代码中,我们指定了用来格式化的值的名字,然后可以根据name在字典中查找相应的value。其实,上面的"name"和"messages"已经在local命名空间中定义,所以,我们可以利用这一点:

print ('Hello %(name)s, you have %(messages)i messages' % locals())

    locals()方法返回一个包含所有本地变量的字典。这个功能非常强大,你可以不必担心提供的values是否和模板匹配;但是同时这个也是非常危险的:你将会暴露整个本地命名空间给调用者,这一点需要你注意。

    在Python中,对象有一个__dict__属性,你可以在格式化字符串的时候使用;

print ("We found %(error_count)d errors" % self.__dict__)    # 等同于  print ("We found %d errors" % self.error_count)

    

    另外,我们还可以用如下的方式,对字符串格式化进一步的控制:%[(name)][flags][width].[precision]typecode,其中:

        (name)为命名

        flags可以有+,-,' '或0。+表示右对齐。-表示左对齐。' '为一个空格,表示在正数的左侧填充一个空格,从而与负数对齐。0表示使用0填充。

        width表示显示宽度

        precision表示小数点后精度

    比如:

print("%+10x" % 10)    # +a  print("%04d" % 5)    # 0005  print("%6.3f" % 2.3)    # 2.300

    上面的width, precision为两个整数。我们可以利用*,来动态代入这两个量。比如:

print("%.*f" % (4, 1.2))    # 1.2000

    Python实际上用4来替换*。所以实际的模板为"%.4f"。


    2、str.format()方法

    str.format()方法是在Python 2.6中引入的,它通过 {} 和 : 来代替 % ,功能非常强大。具体的用法见下面的例子:

In [1]: name = 'xianglong'  In [2]: messages = 4    # 通过位置  In [3]: 'Hello {0}, you have {1} messages'.format(name, messages)  Out[3]: 'Hello xianglong, you have 4 messages'    # 通过关键字参数  In [4]: 'Hello {name}, you have {messages} messages'.format(name=name, messages=messages)  Out[4]: 'Hello xianglong, you have 4 messages'    # 通过下标  In [5]: 'Hello {0[0]}, you have {0[1]} messages'.format([name, messages])  Out[5]: 'Hello xianglong, you have 4 messages'    # 格式限定符:填充与对齐  # ^、<、>分别是居中、左对齐、右对齐,后面带宽度  # :号后面带填充的字符,只能是一个字符,不指定的话默认是用空格填充  In [6]: 'Hello {0:>14}, you have {1:>14} messages'.format(name, messages)  Out[6]: 'Hello      xianglong, you have              4 messages'    # 格式限定符:精度与类型f  In [7]: '{:.2f}'.format(321.33345)  Out[7]: '321.33'    # 格式限定符:b、d、o、x分别是二进制、十进制、八进制、十六进制  In [8]: '{:b}'.format(14)  Out[8]: '1110'    In [9]: '{:d}'.format(14)  Out[9]: '14'    In [10]: '{:o}'.format(14)  Out[10]: '16'    In [11]: '{:x}'.format(14)  Out[11]: 'e'    # 格式限定符:千位分隔符  In [12]: '{:,}'.format(1234567890)  Out[12]: '1,234,567,890'

    更多关于Python字符串格式化的介绍,可以参看:PEP 3101 -- Advanced String Formatting


    十五、迭代器(List comprehensions)


    List Comprehensions即迭代器(列表生成式),是Python内置的非常简单却强大的可以用来创建list的生成式。在不使用迭代器的时候,创建一个新列表可以使用for和if来实现:

new_list = []  for item in a_list:      if condition(item):          new_list.append(fn(item))

    使用迭代器的话:

new_list = [fn(item) for item in a_list if condition(item)]

    列表生成式非常简洁的,不过是在某种程度上。你可以在列表生成式中使用多个for循环和多个if语句,但是两个以上的for和if语句会让列表生成式非常复杂,这时候建议直接用for循环。根据Zen of Python,选择更容易读的方式。下面是一些例子:

>>> [n ** 2 for n in range(10)]  [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]    >>> [n ** 2 for n in range(10) if n % 2]  [1, 9, 25, 49, 81]

    

    十六、生成器(Generator & Generator expressions)


    先出一个题:计算1 ~ 100的平方和。最简单的方法就是使用一个for循环:

total = 0  for num in range(1, 101):      total += num * num

    其实,我们可以使用Python内置的sum方法计算:

# 迭代器(列表生成式)  total = sum([num * num for num in range(1, 101)])    # 生成器  total = sum(num * num for num in xrange(1, 101))

    生成器和上面提到的迭代器差不多,可以说:生成器是一种特殊的迭代器;但是它们之间有一个很大的区别:迭代器是贪婪的,而生成器是懒惰的,具体来说:迭代器会一次性的计算出整个结果列表,而生成器只在需要的时候计算一个值。这个特性在列表非常大,或者需要一步一步计算的时候非常有用。

    在上面的例子中,我们只需要平方和,不需要平方的list,所以我们使用生成器xrange。如果我们计算1 ~ 1000000000的平方和,使用迭代器的话会内存溢出,但是生成器却不会:

total = sum(num * num for num in xrange(1, 1000000000))

    在语法上,迭代器会有一个"[]",但是生成器没有;不过有时候,生成器需要"()",所以,最好每次都带上。一些自定义的生成器例子:

# 过滤CSV文件中的空行  def filter_rows(row_iterator):      for row in row_iterator:          if row:              yield row    data_file = open(path, 'rb')  irows = filter_rows(csv.reader(data_file))    # 文件读取:open  datafile = open('datafile')  for line in datafile:      do_something(line)

    

    PS:原文中作者举了一些工作中的例子,这里不再赘述,想了解的可以到原文链接中查看。


    十七、排序(Sorting)


    在Python中对列表排序非常简单,比如:

In [1]: a_list = ['Tommy', 'Jack', 'Smith', 'Paul']  In [2]: a_list.sort()  In [3]: a_list  Out[3]: ['Jack', 'Paul', 'Smith', 'Tommy']

    需要注意的是:list的sort()方法会直接在原list变量上排序,改变原本的list对象,并且该方法不会返回一个list对象。如果你需要不改变原list,并且返回新的list对象的话,可以使用Python的orted方法:

In [1]: a_list = ['Tommy', 'Jack', 'Smith', 'Paul']  In [2]: b_list = sorted(a_list)  In [3]: b_list  Out[3]: ['Jack', 'Paul', 'Smith', 'Tommy']  In [4]: a_list  Out[4]: ['Tom', 'Jack', 'Smith', 'Paul']

    但是,如果你想对一个list进行排序,不过不想使用默认的排序方式,比如你可能需要先根据第二行排序,再根据第四行排序。这时候,我们也可以使用sort()方法,但是得提供一个自定义的排序方法:

In [1]: def custom_cmp(item1, item2):     ...:     return cmp((item1[1], item1[3]), (item2[1], item2[3]))     ...:   In [2]: a_list = ['Tommy', 'Jack', 'Smith', 'Paul']  In [3]: a_list.sort(custom_cmp)  In [4]: a_list  Out[4]: ['Jack', 'Paul', 'Smith', 'Tommy']

    这种方法可以实现,但是在list比较大的情况下效率很低。下面介绍两种其他的方法。

    1、DSU排序方法

    DSU即Decorate-Sort-Undecorate,中文就是"封装-排序-解封"。DSU方法不会创建自定义的排序方法,而是创建一个辅助的排序列表,然后对这个列表进行默认排序。需要说明的是:DSU方法是一种比较老的方法,现在已经基本上不使用了,不过这里还是给出一个简单的例子说明一下:

# Decorate:  to_sort = [(item[1], item[3], item) for item in a_list]    # Sort:  to_sort.sort()    # Undecorate:  a_list = [item[-1] for item in to_sort]

    上述代码第一行创建了一个tuple的list,tuple中的前两项是用来排序的字段,最后一项是原数据;第二行使用sort()方法对辅助的list进行默认的排序;最后一行是从已经排序的辅助list中获取原数据,重新组成list。

    这种方法是使用复杂度和内存空间来减少计算时间,比较简单,也比较快,但是我们得复制原列表的数据。

    2、KEY方法

    自从Python 2.4之后,list.sort()和sorted()都添加了一个key参数用来指定一个函数,这个函数作用于每个list元素,在做cmp之前调用。key参数是一个函数,这个函数有一个参数,返回一个用来排序的关键字。这种排序方法很快,因为key方法在每个输入的record上只执行一次。你可以使用Python内置的函数(len, str.lower)或者自定义函数作为key参数,下面是一个简单的例子:

In [1]: a_list = ['Tommy', 'Jack', 'Smith', 'Paul']  In [2]: def my_key(item):     ...:     return (item[1], item[3])     ...:   In [3]: a_list.sort(key=my_key)  In [4]: a_list  Out[4]: ['Jack', 'Paul', 'Smith', 'Tommy']


    十八、EAFP vs LBYL


    检查数据可以让程序更健壮,用术语来说就是防御性编程。检查数据的时候,有EAFP和LBYL两种不同的编程风格,具体的意思如下:

    LBYL: Look Before You Leap,即事先检查;

    EAFP: It's Easier to Ask Forgiveness than Permission,即不检查,出了问题由异常处理来处理。

    异常处理总是比事先检查容易,因为你很难提前想到所有可能的问题。所以,一般情况下编码时会倾向使用EAFP风格,但它也不是适应所有的情况。两个风格的优缺点如下:

d = {}  words = ['a', 'd', 'a', 'c', 'b', 'z', 'd']  # LBYL  for w in words:      if w not in d:          d[w] = 0      d[w] += 1    # EAFP  for w in words:      try:          d[w] += 1      except KeyError:          d[w] = 1

    对于LBYL,容易打乱思维,本来业务逻辑用一行代码就可以搞定的。却多出来了很多行用于检查的代码。防御性的代码跟业务逻辑混在一块降低了可读性。而EAFP,业务逻辑代码跟防御代码隔离的比较清晰,更容易让开发者专注于业务逻辑。不过,异常处理会影响一点性能。因为在发生异常的时候,需要进行保留现场、回溯traceback等操作。但其实性能相差不大,尤其是异常发生的频率比较低的时候。

    另外,需要注意的是,如果涉及到原子操作,强烈推荐用EAFP风格。比如我某段程序逻辑是根据redis的key是否存在进行操作。如果先if exists(key),然后do something。这样就变成2步操作,在多线程并发的时候,可能key的状态已经被其他线程改变了。而用EAFP风格则可以确保原子性。

    PS:在使用EAFP风格捕获异常时,尽量指明具体的异常,不要直接捕获Exception。否则会捕获到其他未知的异常,如果有问题,你会很难去定位(debug)。


    十九、引用(Importing)


    Python中的引用:

from module import *

    你可能在其他地方见过这种使用通配符*的引用方式,可能你也比较喜欢这种方式。但是,这里要说的是:请不要使用这种引用方式!

    通配符引用的方式属于Python中的阴暗面,这种方式会导致命名空间污染的问题。你可能会在本地命名空间中得到意想不到的东西,而且这种方式引入的变量可能将你在本地定义的变量覆盖,在这种情况下,你很难弄清楚变量来自哪里。所以,通配符引用的方式虽然方便,但可能会导致各种各样奇怪的问题,在Python项目中尽量不要用这种方式。

    在Python中,大家比较认同的import方式有以下几个规则:

    1、通过模块引用变量(Reference names through their module)

    这种方式直接import的是模块,然后通过模块访问其中的变量,Class和方法。使用这种方式可以很清晰的知道变量来自哪里:

import module  module.name

    2、模块名比较长时使用短名字(alias)

import long_module_name as mod  mod.name

    3、直接引用你需要的变量名

from module import name  name


    二十、模块与脚本(Modules & Scripts)


    为了使一个Python文件既可以被引用,又可以直接执行,你可以在Python文件中加上这样的代码:

if __name__ == '__main__':      # script code here

    当被引用时,一个模块(module)的__name__属性会被设置为该文件的文件名(不包括.py后缀)。所以,上面代码片段中if语句中的脚本在被引用的时候不会执行。当把Python文件作为一个脚本执行的时候,__name__属性则被设置为"__main__",这时候if语句中的脚本才会被执行。

    最好不要在Python文件中直接写可执行的语句,应该将这些代码放在方法或类里面,必要的时候放在"if __name__  == '__main__':"中。一个Python文件的结构可以参考下面的:

#!/usr/bin/env python  # -*- coding: utf-8 -*-  """ 文档 module docstring """    # 引用 imports  # 常量 constants  # 异常 exception classes  # 方法 interface functions  # 类 classes  # 内部方法和类 internal functions & classes    def main(...):      ...    if __name__ == '__main__':      status = main()      sys.exit(status)


    二十一、命令行解析(Commend-Line Processing)


    Python是一种脚本语言,有时候我们会直接在命令行运行Python文件,这时候可能需要解析命令行传入的参数,下面是一个例子:cmdline.py

#!/usr/bin/env python    """  Module docstring.  """    import sys  import optparse    def process_command_line(argv):      """      Return a 2-tuple: (settings object, args list).      `argv` is a list of arguments, or `None` for ``sys.argv[1:]``.      """      if argv is None:          argv = sys.argv[1:]        # initialize the parser object:      parser = optparse.OptionParser(          formatter=optparse.TitledHelpFormatter(width=78),          add_help_option=None)        # define options here:      parser.add_option(      # customized description; put --help last          '-h', '--help', action='help',          help='Show this help message and exit.')        settings, args = parser.parse_args(argv)        # check number of arguments, verify values, etc.:      if args:          parser.error('program takes no command-line arguments; '                       '"%s" ignored.' % (args,))        # further process settings & args if necessary        return settings, args    def main(argv=None):      settings, args = process_command_line(argv)      # application code here, like:      # run(settings, args)      return 0        # success    if __name__ == '__main__':      status = main()      sys.exit(status)


    二十二、包(Packages)


    Python中包的设计与引用规则,包的设计例子:

package/      __init__.py      module1.py      subpackage/          __init__.py          module2.py

    建议使用上面的方式来组织你的项目,尽量减小引用路径,明确引用对象,避免引用冲突。引用示例:

import package.module1  from package.subpackage import module2  from package.subpackage.module2 import name

    我们可以通过__future__模块使用Python 3.0的功能:absolute_import。方法如下:

from __future__ import absolute_import

    简单介绍一下相对引入和绝对引入的概念:    

    相对导入:在不指明 package 名的情况下导入自己这个 package 的模块,比如一个 package 下有 a.py 和 b.py 两个文件,在 a.py 里 from . import b 即是相对导入 b.py。

    绝对导入:指明顶层 package 名。比如 import a,Python 会在 sys.path 里寻找所有名为 a 的顶层模块。

    引入absolute_import之后不是支持了绝对引入,而是拒绝相对引入。


    简单比复杂好


    调试程序的难度是写代码的两倍。因此,只要你的代码写的尽可能的清楚,那么你在调试代码时就不需要那么地有技巧。(Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. ) -- Brian Kernighan。所以,尽量保持你的程序足够简单。


    不要重复造轮子


    在你写代码之前,你需要先看一下有没有其他人已经实现了类似的功能。你可以从下面的几个地方寻找:

    1、Python标准库

    2、Python第三方LIB, PYPI(Python Package Index),地址:PYPI

    3、搜索引擎,Google,百度等。。


    参考

    Code Like a Pythonista: Idiomatic Python

    writing idiomatic python 3

    LBYL与EAFP两种防御性编程风格

    Python高级编程

    Python补充05 字符串格式化 (%操作符)

    How to sorting

    探索 Python 的变量、类型和引用


Over!

本文地址: http://xianglong.me/article/how-to-code-like-a-pythonista-idiomatic-python/