Pythonic编程(不定期更新)

什么样的代码可以称得上是『Pythonic』?这是一个实用的话题,也是本文的讨论主题……总而言之,要充分利用Python语言本身的特性


1. Python之禅

>>> import this
Beautiful is better than ugly.
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.
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.
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!

2. 使用元祖而不是中间变量交换数据

a, b = b, a

只需要一行代码,充分利用了Python『序列解包(unpacking)』这一语言特性;事实上Pytho的解包操作适用于所有的序列

>>> var1, var2 = 'xy'
>>>> var1
'x'
>>> var2
'y'

适用于所有序列,就是说适用于列表、元祖,甚至是字符串

a, b, c, d, e = range(5)

以上是另一个用到解包语法的Pythonic代码

3. Pythonic式的for循环是为了迭代对象而存在的

for i in a_list:
    do_sth_with(i)

看到可迭代对象(序列),下意识就要想到可能要用到Pythonic式的for循环;Python也有while循环,但是并不Pythonic

>>> g = (x for x in range(10))
>>> for i in g:
        print(i, end=" ")
0 1 2 3 4 5 6 7 8 9

一个最基础、也最有用的问题:什么是可迭代对象?答案:实现了__iter__()方法的类之实例,比如上面的g是一个生成器,也是一个可迭代对象;另外在Python文件操作open函数的返回的也是一个可迭代对象

4. 延迟计算,惰性求值

英文解释就是“Lazy Evaluation”

1)条件表达式

#!/usr/bin/env python3
#Filename: lazyevaluation.py

abbr = ['cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.']
for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
    if w in abbr:
    #if w[-1] == '.' and w in abbr:
        pass

上面这段代码只是一个简单的实例(可以假设读取一个非常大的文件),如果把if语句换成注释掉的那句,特别当数据庞大时程序性能(运行时间)提升的不是一点点

为什么呢?因为Python中的条件表达式if x and y,在x == False的情况下不会计算y的值,同理if x or y,在x == True时也就不用计算y的值了

2)使用生成器表达式以节省内存空间

>>>def fib():
        a, b = 1, 2
        while True:
            yield a
            a, b = b, a+b
>>> from itertools import islice
>>> print(list(islince(fib(), 5)))
[1, 1, 2, 3, 5]

5. 巧妙利用动态语言类型实现方法重载

>>> def add(a, b):
        return a + b
>>> add(1, 2)
3
>>> add(1.2, 3.4)
4.6
>>> add('a', 'b')
'ab'
>>> add(1, 2j)
1+2j
>>> add([1, 2], [3, 4])
[1, 2, 3, 4]
>>> add(1, 'a')
Traceback (most recent call last):
    File 'xxx.py' line xx, in 
        print add(1, 'a')
    File 'xxx.py' line xx, in add
        return a + b
TypeError: unsupport operate type(s) for +: 'int' and 'str'

就上面一行代码的函数,却实现了整数(int)、浮点数(float)、字符串(str)、包括复数(complex)、以及列表(list)的运算(+)操作,并且当传入的两个类型不支持该操作时返回一个Error——这样的事在静态语言中是无法实现的

6. 使用isinstance代替type进行类型判断

isinstance函数的原型是

isinstance(object, classname);
>>> isinstance(2, float)
True
>>> isinstance('a', str)
>>> True
>>> isinstance((2, 3), (str, list, tuple))
>>> 
True

说明下,第三行不是元祖解包,而是说当第一个参数只要为后面的元祖中的一个的实例,就返回真

至于为什么不用type函数?因为type函数在判断继承关系上表现不好——既然有更好、更强大的isinstance函数,所以就用isinstance函数

7. 警惕eval安全漏洞

就像一句经典的话:“Eval is evil”

import sys
from math import *
def ExpCalcBot(string):
    try:
        print('Your answer is', eval(user_answer))
    except NameError:
        print("The expresion you enter is not valid")

print('Hi, I am ExpcalBot. Please enter a number or expression. Enter c to complete')
while 1:
    print('Please enter a number or operation. Enter c to complete. :')
    inputstr = input()
    if inputstr == str('e'):
        sys.exit()
    elif repr(inputstr != repr('')):
        ExpcalBot(inputstr)
        inputstr = ''

上面这段代码的功能是根据用户输入,计算Python表达式的值

让我们把这样的场景放到Web上,假设用户输入

__import__("os").system("dir")

这会显示当前目录下所有文件的列表

__import__("os").system("del * /Q")

于是当前目录下的文件全部被删除了(汗)

8. 使用enumerate函数获取序列的索引和值

enumerate作为一个内置函数,在Python2.3 中被引入,它的存在就是为了解决在循环中获取索引值及其对应值的问题

l = ['a', 'e', 'i', 'o', 'u']
for index, value in enumerate(l):
    print("Index at", i, "with the value of", value)

这和下面的zip函数等价

for i, e in zip(range(len(l)), l):
    print("Index at", i, "with the value of", e)

但是相比之下前者可读性更好,我们推荐使用前者

另外emerate函数具有一定的惰性(lazy)(它的内部实现就是一个yield生成器)

>>> li = [1,2,3]
>>> enumerate(li)
object at 0x104732ab0>
>>> k = enumerate(li)
>>> next(k)
(0, 1)
>>> next(k)
(1, 2)
>>> next(k)
(2, 3)
>>> next(k)
Traceback (most recent call last):
  File "", line 1, in <module>
    next(k)
StopIteration

最后需要指出的是,最好不要对字典用enumerate函数,因为这会默认把字典的键转换为从0、1……开始的下标处理(显然不是我们要的)

9. 考虑兼容性,尽量使用Unicode编码

Python内建的字符串有两种类型,即str和Unicode,它们拥有共同的祖先basestring;其中Unicode是Python2.0引入的一种新的数据类型,所有的Unicode字符都是Unicode类型的实例

>>> strUnicode = u'unicode 字符串'
>>> strUnicode
u'unicode \u5b57\u7b26\u4e32'
>>> print(strUnicode)
>>> type(strUnicode)
'unicode'>
>>> type(strUnicode).__bases__
('basestring'>,)

如你所见,在Python中创建Unicode字符非常简单:只要在字符串前面加上表示Unicode的u即可

Unicode(Universal Multiple-Octet Coded Character Set)也被称为万国码,为每种语言提供了唯一的二进制编码方式,提供从数字代码到不同语言字符集之间的映射,从而可以满足跨平台、跨语言之间的文本处理要求

Unicode编码系统可以分为编码方式和实现方式两个层次
在编码上,分为UCS-2和UCS-4两种方式,目前实际确定的编码方式是UCS-2,使用16位的编码空间
一个字符的Unicode编码是确定的,但是在实际传输过程中,由于系统平台的不同以及出于节省空间的考虑,实现方式有所差异;Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format),简称UTF,包括UTF-7、UTF-16、UTF-32、UTF-8等,其中较为常见的是UTF-8,其特点是对不同范围的字符使用不同长度的编码,其中0x00~0x7F字符的UTF-8编码与ASCII编码完全相同(使用一个字节)

filehandle = open("test.txt", "r")
print(filehandle.read().decode('utf-8').encode('gbk'))
filehandle.close()

假设读入的文件是中文,用UTF-8编码保存,但是Windows系统本地默认编码是CP936,在Windows系统中它被映射为GBK编码,所以当在控制台上直接打印显示UTF-8字符的时候,这两种编码不兼容,以UTF-8形式表示的编码在GBK编码中被解释为其它的符号,由此产生了乱码——看上面,我的解决方法是首先对读入的文件字符用UTF-8进行解码,然后再用GBK进行编码(见上)——utf-8是不同编码转换的中间桥梁,这一点在处理乱码时会很有用

总之,要熟练使用decode和encode函数

10. 使用with自动关闭资源

对于文件资源的关闭,我们有如下写法;相当于通常意义上异常处理的finally语句

with expression [as target]:
    #code here

另外它还支持嵌套

with expr1 as e1:
    with expr2 as e2:
        ...

举个例子

with open('test.txt', 'w') as f:
    f.write('test')

with语句在代码块执行完毕后还原进入代码块时的现场(Python上下文管理器)

11. 善用else子句

1)条件判断中的else子句

2)循环中的else子句

def print_prime(n):
    for i in xrange(2, n):
        for j in xrange(2, i):
            if i % j == 0:
                break
        else:
            print("%d is a prime number " %i)

3)异常处理中的else子句

def save(db, obj):
    try:
    # Save attr1
    db.execute('a sql stml', obj.attr1)
    # Save attr2
    db.execute('another sql stml', obj.attr2)
except:
    db.rollback()
else:
    db.commit()

如果没有Python提供的else子句语法糖,恐怕要引入一个标志变量才能解决问题(此处略)

12. 用join方法而不是+操作符连接字符串

Python的字符串对象是不可变对象

>>> str1, str2, str3 = 'how ', 'are ', 'you'
>>> ''.join([str1, str2, str3])
'how are you'

和使用加法操作符相比,join方法更高效:因为字符串是不可变对象,每次加操作都要在内存中申请一块新的内存(N个字符串会产生N-1个中间结果,所以字符串的加法操作的时间复杂度近似为O(N^2))

而使用join方法连接字符串,会先去计算需要申请的总的内存大小(所以join方法的时间复杂度近似为O(N))

特被在处理大文件时,使用join方法的效率高的不是一点点

13. 使用变长参数

在别的语言中,变长参数通常是写成省略号的形式

int printf(char *fmt, ...)

但是Pythonic的代码由于没有指针这个概念,使用星号表示变长参数

def f(*args):
    #do something

14. Pythonic式的三目运算操作

if value:
    a = 0
else:
    a = 1
a = 0 if value else 1

对比上面两种写法,明显下面的写法更简洁、更优雅,是Pythonic的

15. 熟悉内置函数

if a > 100:
    a = 100
if a < 0:
    a = 0
a = max(min(a, 100), 0)

这个例子是在知乎上看到的,还是挺实用的

16. Python的下划线变量

_xxx: 保护变量,只有类对象和子类对象能够访问到这些变量
__xxx: 私有变量,只有该类能够访问到这些变量
__xxx__: 系统变量

如果不是出于需要,尽量不要用下划线命名Python变量,因为这些变量在Python中往往有特殊含义

17. 高效使用sort()、sorted()

sort()方法和sorted()函数的区别这里不再赘述

sorted(iterable[, cmp[, key[, reverse]]])
s.sort([cmp[, key[, reverse]]])

cmp:为用户定义的任何比较函数,函数的参数为两个可比较的元素(来自iterable或者list),函数根据第一个参数与第二个参数的关系一次返回-1、0、或1;该参数的默认值为None

key:是带一个参数的函数,用来为每个元素提取比较值,默认为为None(即直接比较每个元素)

reverse:表示排序结构是否反转

>>> persons = [{'name':'John', 'age':32}, {'name':'Alex', 'age':50}, {'name':'Bob', 'age':23}]
>>> sorted(persons, key=lambda x: (x['naem'], -x['age']))
[{'age':50, 'name':'Alex'}, {'age':23, 'name':'Bob'}, {'age':32, 'name':John}]

不论是sort()还是sorted(),传入参数key比传入参数cmp效率要高;cmp传入的函数在整个排序过程中会被调用多次,函数开销比较大;而key针对每个元素仅做一次处理——因此使用key比cmp效率要高

# 演示:对字典按照第二个元素排序

>>> d = [{'name':'A', 'age':3}, {'name':'B', 'age':1}, {'name':'C', 'age':2}]
>>> sorted(d, key=lambda x:x['age'])
[{'name': 'B', 'age': 1}, {'name': 'C', 'age': 2}, {'name': 'A', 'age': 3}]

18. 使用collections.Counter对象进行计数

普通人的做法:1. 使用dict,2. 使用defaultdict,3. 使用set和list

Pythonic的做法:使用collections.Counter类(Python2.7引入,是用来统计散列对象的容器类型)

>>> from collections import Counter
>>>> some_data = ['a', '2', 2, 4, 5, '2', 'b', 4, 7, 'a', '5', 'd', 'a', 'z']
>>> print(Counter(some_data))
Couner({'a':3, 4:2, 5:2, '2':2, 2:1, 'b':1, 7:1, 'z':1, 'd':1)

通过__init__()方法创建常量清单

使用__init__()方法创建常量清单

# 定义Suit类,将Suit属性从Card中解耦
class Suit(object):
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol

Club, Diamond, Heart, Spade = Suit('Club', '♣️'), Suit('Diamond', '♦️'), Suit('Heart', '♥️'), Suit('Spade', '♠️')

20. 使用工厂函数创建对象

一般来说,有两种方法实现工厂:

1. 定义一个函数,返回不同类的对象实例

2. 定义一个类,包括创建对象的方法(这是完整的工厂设计模式)

Python的优势在于,它既支持面向对象编程,也支持面向过程编程

可以用函数完成的事情,大可不麻烦的写一个类

class Card(object):
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit
        self.hard, self.soft = self._points()

class NumberCard(Card):
    def _points(self):
        return int(self.rank), int(self.rank)

class AceCard(Card):
    def _points(self):
        return 1, 11

class FaceCard(Card):
    def _points(self):
        return 10, 10

通过工厂函数调用__init__()方法

def card(rank, suit):
    if rank == 1: return AceCard('A', suit)
    elif 2 <= rank < 11: return Numbercard(str(rank), suit)
    elif 11 <= rank <14:
        name = ({11:'J', 12:'Q', 13:'K'}[rank])
        return FaceCard(name, suit)
    else:
        raise Exception("Rank out of range")

在上面的基础上枚举所有花色的牌,完成52张牌的创建

deck = [card(rank, suit)
    for rank in range(1, 14)
        for suit in (Club, Diamond, Heart, Spade)]

21. 使用any()、all()函数

any()函数:判断空元素的存在性;如果该对象的元素都为0、空字符串、None、False,则返回False

all()函数:判断空元素的任意性;如果该对象的元素都非0、空字符串、None、False,则返回True

>>> any('123')
True
>>> any([0,1])
True
>>> any([0,'0',''])
True
>>> any([0,''])
False
>>> any([0,'','false'])
True
>>> any([0,'',bool('false')])
True
>>> any([0,'',False])
False
>>> any(('a','b','c'))
True
>>> any(('a','b',''))
True
>>> any((0,False,''))
False
>>> any([])
False
>>> any(())
False
>>> all(['a', 'b', 'c', 'd'])  #列表list,
True
>>> all(['a', 'b', 'c', 'd'])  #列表list,元素都不为空或0
True
>>> all(['a', 'b', '', 'd'])  #列表list,存在一个为空的元素
False
>>> all([0, 1,2, 3])  #列表list,存在一个为0的元素
False
>>> all(('a', 'b', 'c', 'd'))  #元组tuple,元素都不为空或0
True
>>> all(('a', 'b', '', 'd'))  #元组tuple,存在一个为空的元素
False
>>> all((0, 1,2, 3))  #元组tuple,存在一个为0的元素
False
>>> all([]) # 空列表
True
>>> all(()) # 空元组
True
>>> #注意:空元组、空列表返回值为True,这里要特别注意
>>> all(('', '', '', ''))  #元组tuple,全部为空的元素
False
>>> all('')
True
>>> #如果all(x)参数x对象的所有元素不为0、''、False或者x为空对象,则返回True,否则返回False

本例子来自网络

22. 善用zip函数

如下,我们使用zip函数获取二维矩阵的第n列元素

def get_col(str, n-1):
    lyst1 = [i.split() for i in str1.split('\n')]
    lyst2 = list(zip(*lyst1))
    return lyst2

调用时

str = '''1 2 3 4 5
1 2 3 4 5
1 2 3 4 5'''

print(get_col(str, 3))

# 输出:[3, 3, 3]

另外,压缩函数还可以用来反置字典的键和值

>>> dd = {'a':1, 'b':2, 'c':3}
>>> dict(zip(dd.values(), dd.keys()))
{1: 'a', 2: 'b', 3: 'c'}

23. 列表分组

还是zip函数,谁叫它这么好用呢(没办法……

>>> a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> lyst = list(zip(*(iter(a),)*3))
>>> lyst
[(1, 2, 3), (4, 5, 6), (7, 8, 9)]

你可能感兴趣的:(Python,语言)