判空 和 默认值

# Pythonic
a is None
# Script-Like
a = a or default_value

这两种写法在a的值为None时行为一致:

a = None
# 1
if a is None
    a = default_value # a = default_value
# 2
a = a or default_value # a = default_value

如果参数是’’, 0 , False, [], {} 等值,则行为不同:

a = ''
# 1
if a is None: # False
    a = default_value # a = ''
#
a = a or default_value # a = default_value

这是因为 a = ''a is None 在逻辑运算上的语义等同于 bool(False)

因此在要处理的参数取值可以为'', 0, 0.0, False, [], (), {}, set()等值的时候,使用is None + 赋值default_value最为准确 在不可以取以上值的时候,使用or赋给默认值较为方便。当然,如果想要将代码写在一行,可以像下面一样,不过这样的可读性大不如写成两行:

a = default_value if a is None else a

判空操作对于函数参数有默认None值的时候,判空操作是非常必要的, 有时候要根据参数是否为None,而决定是否为参数赋一个默认值。但是要注意以上两种写法的区别。

三目

# Pythonic
b if a else c
# Script-Like
a and b or c

当使用这两种方式进行三目运算:
行为不受a的取值影响,
当b的取值不同时,会有一定影响,而且这种结果是a和b的值共同决定的。

首先无论是 if a 还是 对 a逻辑的计算结果都是相同的,如果b是逻辑语义非空的值,这时 分为两种情况:
1.如果a的值为真
使用 b if a else c 是直接返回b的,而它不会考虑b的取值到底是什么
使用 a and b or c 则需要根据b的值进行二次判断,
 如果b的值是逻辑语义表现为True的值,则直接返回b了
 如果b的值表现为False, 如 None, '', 0, 0.0, False, [], (), {}, set(), 则a and b 一定判断为False, 而它返回的是c的值
2.如果a的值为假
无论是if a 还是 a逻辑的计算结果都是假,那么两者都是直接返回c,对于if a是走else分支,而对于a and b则进行短路运算。

因此,以上两种情况可以总结为:
a为假,结果一样
a为真,结果在于b的取值

如果你非用and or不可,大可以这样使用:

(a and [b] or [c])[0]

虽然与b if a else c完全一致,但是可读性大大降低,书写也较复杂。

理解 ~ 和 not

~ 其实调用了__invert__这个magic method,Python的任何类型都可以实现这个方法,对于bool类型而言,bool类型并没有重写继承自父类int的__invert__方法, ~实际上是调用了bool类型的父类int类型的__invert__方法。而int的__invert__的实现其实就是对整型数进行字节翻转。

如果a = 10,因为a为正数,则a的原码、反码、补码为1010,它的符号位为0,对它字节翻转之后即-11,这个计算过程如下:

  符号位  
10 0 1010
翻转后:~10 1 0101
求反码 1 0100
求原码 1 1011
-11    

而对于bool类型False、True实际上是以整型的0,1存储的,并且它的长度为1bit, 因此对于False的字节翻转为

  符号位  
False 0 0
翻转后:~False 1 1
求反码 1 0
求原码 1 1
-1    

这与'%d' % ~Falsebin(~False)的输出一致

而对于True的字节翻转过程为:

  符号位  
True 0 1
翻转后:~True 1 0
求反码 1 1
求原码 1 0
-2    

这与'%d' % ~Truebin(~True)的输出一致。

这里已经得到一个结论:不能使用~来对bool类型进行取反,因为
~True = -1 => True
~False = -2 => True
而~True这个结果对于逻辑运算来说是错误的。

题外话:__invert__这个magic method在三方库中也有广泛使用,例如Django中ORM操作中经常使用的Q对象,它对__invert__的实现是对negated这个状态属性值取反,并返回带有这个属性值的新的对象,而在ORM过程生成SQL时会根据这个状态值选择是否使用not关键字。

Python 语言本身支持的的逻辑运算符就是 not and or , 而not的职责是对bool值取反,而它实际上执行起来是先对表达式的值进行bool转换,因此not a实际上就相当于not bool(a),它是对进行bool值取反最稳妥的做法。

not操作在实际项目中的功能远不只是bool判断那么简单,它最常见的使用是对str、list、dict等对象类型进行判空操作, 如:

list_a = []
if not list_a:
    pass # empty handle

dict_a = {}
if not dict_a:
    pass # empty handle

但是对字符串判空的时候需要注意空串''带空白字符的串' '判断结果是不同的

not '' # True
not ' ' # False

若想对只含有空白字符的串也判断为空串,可以对trim之后的字符串进行判断:

str_a = ' '
not str_a.strip() # True

使用enumerate()遍历

对于集合类型的遍历,有很多种方法,如dict对象的遍历,通常可以遇到以下几种方式遍历:

info = {'name': 'Mike', 'age': 22}
# 方法1
for key in info:
    print(key + "=" + str(info[key]))

# 方法2
for key in info.keys():
    print(key + "=" + str(info[key]))

# 方法3
for key_index, key in enumerate(info):
    print(key + "=" + str(info[key]))

# 方法4
for key, value in info.items():
    print(key + "=" + str(value))

方法1分析:根据for in 语法的特性可知,方法1其实相当于:

for key in iter(info): # iter(info) <==> info.__iter__()
    print(key + '=' + str(info[key]))

通过iter()函数生成了一个dict_keyiterator对象,并不断通过调用这个对象的__next__()方法获取下一个值

方法2中keys()方法生成的是一个dict_keys对象,它是 set-like object ,这个对象保留了所有的 key 的值,而不是像迭代器那样对节约使用内存资源。

方法3中enumerate(info)生成了一个enumerate对象,它本身是一个迭代器,充当了info数据源。

方法4使用了dict类型的items()方法生成的是dict_items类型,它和dict_keys对象对象类似,是一个set-like object,用两个元组来分别保存info所有的key和所有的value,对内存也是不友好的。

通过以上分析可以得到结论,使用enumerate()对字典进行遍历是最Pythonic和性能最优的,这也是官方最推荐的遍历集合类型的数据的方式。

对于list的遍历也是这样:对于只需要值的遍历通常可以使用这种方式:

list_a = ['hello', 'world']

for value in list_a:
    print(value)

for in 语法的特性其实是对list_a.__iter__()遍历,而list_a.__iter__()其实就是iter(list_a),这样:它其实等同于下面的代码:

for value in iter(list_a):
    print(value)

iter(list_a)返回的是一个list_iterator对象,它是一个迭代器,因此在遍历过程中也几乎对内存没有影响。

对于在遍历过程中需要用到索引的情况,通常有以下两种做法:

# 方法1
for index in range(len(list_a)):
    print("%d: %s" % (index, list_a[index]))
# 方法2
for index, value in enumerate(list_a):
    print("%d: %s" % (index, value))

对于方式1,其实是对iter(range(len(list_a)))进行遍历,它是一个range_iterator对象,本身是一个迭代器, 对于list_a[index]取值的方式,其实是对list对象方法__getitem__()的调用,这中使用[]方式取list类型对象索引对应值方式几乎是所有语言都支持的语法糖。 这里的__getitem__()实现并没有研究过,可以是依靠地址偏移取值或者hashtable寻找值等等方式实现。总的来说使用这种方式遍历对内存没有多少影响,但是书写起来不是很优雅, 而第二种方式的优势就凸显出来了,虽然对enumerate对象的遍历没有多少性能提升,但是这种写法是更加Pythonic的,也是官方推荐的做法。

通过以上的分析,我们可以得到结论,对于dict或者是list的遍历,使用enumerate()方式遍历几乎是性能最优的和最Pythonic的做法。

in : for...in 和 if...in

in最常用的做法莫过于在for循环中了,而for…in的用法,其实是对表达式__iter__()方法返回的迭代器进行遍历的过程,这个内容已经在本文前一部分和之前的一篇文章迭代器和生成器都有介绍,这里不再赘述。而对于if…in用法,则是Python推荐的一种对集合类型对象是否包含某个元素的惯用语法,如

list_a = ['hello', 'world']
if 'world' in list_a:
    pass

obj in container_obj实际上是对container_obj.__contains__()方法的调用,因此,理论上讲,下面的两句代码其实是一样的:

if 'world' in list_a:

if list_a.__contains__(obj):

但是magic method的直接调用是从不推荐的,Python几乎为每个magic method 添置了对应的语法糖,而且使用语法糖通常是书写方便、阅读性好的。

多使用List comprehensions

List comprehensions(列表推导式) 或者listcomps,本身就是快速生成集合对象的Python特有语法,它通常用用在简化类似下面的固定的构建集合对象的模板代码:

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循环大于2层,尽量不要使用列表推导式,而是选择使用传统的for循环方式构建数据,这样可读性会更好。(Zen of Python: Readability counts(可读性很关键))。

当有时候需要手动构建一个JSON时,需要将对象类型集合转为一个字典类型时,使用列表推导式是非常方便的:

peoples = [{'name': p.name, 'age': p.age} for p in people_list]

Generator Expressions

与列表推导式相对应的就是生成器表达式,生成器表达式也是非常常用的syntax shortcut,对于快速构建一个生成器是非常简洁实用的。如要构建一个可产生1-10int对象的生成器,对于使用yield构建的一般书写为:

def nums():
    for i in range(10):
        yield i + 1

使用生成器表达式语法可以书写为:

(i+1 for i in range(10))

如果一个函数可以接受可变参数,生成器可以快速构建参数值,这个做法是非常常用的。例如:为sum()函数构建10个参数,通常的做法可能是使用元组或者列表

tuple_a = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
sum(tuple_a) # 55

list_a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sum(list_a) # 55

但是使用生成器表达式可以这样做:

sum(i+1 for i in range(10))

不仅书写简单,而且可以运行时读取值,极大节约内存。需要注意一点的是,这里不需再多加一层()

namedtuple

在没有ORM的情况下,使用数据库查询数据并在展示出来,通常我们的做法是:

# name age height
rows = [('Mike', 22, 1.70), ('John', 23, 1.71)]
print('name\tage\theight')
for row in rows:
    print('%s\t%d\t%f' % (row[0], row[1], row[2]))

上面是一条记录只有三个字段的情况,倘若每条记录有10个字段,这样的代码基本没有可读性。另外,当给数据库中的表增加字段或者减少字段时,很有可能要根据需求,更改row[index]的显示顺序,如果这样做不仅增加工作量,而且代码缺失动态调整能力,再遇到类似的需求,依然要做重复的工作。

说到底:使用数字下标取值的做法,将自然逻辑中代表物体两个不同的属性的值,使用标号来区别,这种做法让属性缺乏了可辨识性,而且是不符合认知习惯的。

这个时候要适当地使用一些具有标识含义的”词汇”,namedtuple就是用来在编程中解决这个问题的:

from collections import namedtuple

rows = [('Mike', 22, 1.70), ('John', 23, 1.71)]
Person = namedtuple('Person', 'name, age, height')
print('name\tage\theight')
for row in rows:
    person = Person._make(row)
    print('%s\t%d\t%f' % (person.name, person.age, person.height))

EAFP vs. LBYL

这个话题其实不仅仅限于以Pythonic方式编程,它是编程领域中不可避免的防御式编程问题,关于它的解读,网上的资料不胜枚举,这里只是简单地提一下。 https://docs.python.org/3/glossary.html#term-eafp https://docs.python.org/3/glossary.html#term-lbyl EAFP 即 It’s easier to ask forgiveness than permission. 如果非要翻译成中文 大概就是:请求原谅比获得准许更简单。在现实世界中经常发生这样的事:

一个学生违反了某些学生守则规定,但是他去请求老师的原谅时,如果不是太大的问题,老师是会原谅他的;但是,如果在这件事情发生之前,学生就去找老师说:我要去….,这样老师无论如何都是不允许他这样做的。

在防御式编程领域,EAFP就对应着,先给各种可靠或者非可靠的程序放行,直到发生问题了再去处理,而经典的处理手段就是捕获这个错误(异常)并处理,同时阻止后续代码继续执行。

LBYL 即 Look before you leap 这是一个英文谚语,在中文中有着固定的翻译:三思而后行;摸着石头过河; 在防御式编程领域,LBYL对应对每次要进行的操作的数据进行校验,只对符合规则的数据放行。

通常EAFP方式更好,但不总是这样,尤其是在对动态类型的处理时,如果一个对象是鸭子类型 Generally EAFP is preferred, but not always. Duck typing

If it walks like a duck, and talks like a duck, and looks like a duck: it’s a duck. (Goose? Close enough.)

Exceptions

Use coercion if an object must be a particular type. If x must be a string for your code to work, why not call

str(x) instead of trying something like

isinstance(x, str)