本文章持续随缘更新.
最后更新时间: 2019-08-24

如何立即跳出两层嵌套循环

两层循环:

1
2
3
4
for i in listA:
for j in listB:
if i == j:
break # 如何 break 两次呢?

一般跳出两层嵌套循环有以下几种常见的方法:

  1. 将循环放在一个函数中进行, 利用函数返回来跳出循环. 但这样的做法不尽如人意, 因为循环可能不适合重构为一个新的函数, 也许你需要在循环的过程中访问其他局部变量.
  2. 抛出异常, 并在两重循环之外捕获它. 这是把异常当作 goto 语句来用了. 但是这里并没有异常的条件, 只是变向地利用了异常机制.
  3. 使用布尔变量来标记循环的结束, 并在外循环中检查变量值以执行第二次 break 操作. 该方法毫无技术成分, 某些情况下可能是有效的, 但大多数情况下只造成计算资源浪费, 程序效率不高.
    而对于python, 有更为简单的方法, python非常擅长抽象化迭代过程, 如果把python当作一般的语言来使用, 不能发挥循环抽象的优势. 我们甚至可以把两层循环抽象为一层循环, 可以思考一下, 如何用python的语法将两层循环改写为一层循环?

下面给出答案:

1
2
3
4
5
6
7
8
9
def get_pairs(listA, listB):
"""在listA和listB范围中生成索引对"""
for i in listA:
for j in listB:
yield i, j

for i, j in get_index(listA, listB):
if i == j:
break

此处, 我们写了一个生成器用于生成需要的索引对. 现在, 我们的循环就成了对索引对的一重循环, 而不是对索引的两重循环. 两重循环依然存在, 只是被抽象了出去, 移到了 get_pairs() 方法里面.
这使我们的代码更贴近自然语言的描述. 而且, 如果在其他地方也需要这样迭代的话, 还能复用 get_pairs 生成器.

Python函数是传值还是传引用

关于Python函数是传值还是传引用, 总结起来大概有三种说法:

  • 1.传引用, 类似于C语言的指针, 传入参数的引用, 当更改传入值时, 原值也会改变.
    下面举一个例子:
1
2
3
4
5
6
7
8
9
10
11

def changeValue(x):
x = 'b'

origin_value = 'a'
print("origin_value: ", origin_value)
changeValue(origin_value)
print("changed: " + origin_value)
# 输出的值为:
origin_value: a
changed: a

可以清楚地看到在函数内更改值, 原值并没有发生变化, 所以并不是传引用.

  • 2.传值, 和字面意思一样, 只是传入原对象的值的复制, 在函数内更改传入值, 并不会改变原值.
    这时再举一个例子:
1
2
3
4
5
6
7
8
9
10
def changeList(x):
x.append('b')

origin_list = ['a']
print("origin_list: ", origin_list)
changeList(origin_list)
print("changed: ", origin_list)
# 输出结果为:
origin_list: ['a']
changed: ['a', 'b']

可以看到, 在函数内更改列表的值时, 原列表的值也发生了变化, 所以并不是传值.

  • 3.可变对象传引用, 不可变对象传值
    还举一个例子
1
2
3
4
5
6
7
8
9
10
def changeList(x):
x = ['b']

origin_list = ['a']
print("origin_list: ", origin_list)
changeList(origin_list)
print("changed: ", origin_list)
# 输出结果为:
origin_list: ['a']
changed: ['a']

可以看到, 这次传入的参数虽然是可变对象, 但是并没有改变原来的值, 所以这个说法也不正确.

那么python函数传的参数到底是什么?

我们先看一下python在赋值语句中对应的内存变化图:

01

在python中, 当b = a 时, b 和 a 指向同一块内存, 而当再 b = 7 时, 申请了一块新的值为7的内存, 再将b指向该内存, a的值并没有发生改变.

此时再回头看上述的例子, 对于示例1, x = ‘b’, 由于x为字符, 是不可变对象, x = ‘b’会重新申请一块内存, 其值为’b’, 并在函数体中将x指向它. 当调用完函数之后, 函数体中的局部变量在函数外不并不可见, 此时的origin_value值还是3. 而在示例2中, 在org_list中再添加一个元素’b’, 只是在内存中加入新值’b’, 这时x指向的地址, 即对象并没有被更改, 所以函数体外的origin_list的值也发生了改变. 而示例3中是直接申请了一块新的值为 [‘b’] 的内存, 再将函数中的x指向该内存, 所以并没有改变函数外origin_list的值.

因此, 对于Python函数参数是传值还是传引用这个问题的答案是: 都不是. 正确的叫法应该是传对象(call by object)或者说传对象的引用(call-by-object-reference). 函数参数在传递的过程中将整个对象传入, 对可变对象的修改在函数外部以及内部都可见, 调用者和被调用者之间共享这个对象, 而对于不可变对象, 由于并不能真正被修改, 因此, 修改往往是通过生成一个新对象然后赋值来实现的.

默认参数问题

从上文对python是传值还是传引用问题的探讨, 我们可以引出新的问题. 对于默认的可变对象, 如果我们在函数里 append() 后, 默认的可变对象会不会发生改变?
我们可以写段代码测试一下:

1
2
3
4
5
6
7
8
9
def change(my_list=['a']):
my_list.append('b')
return my_list

print(change())
print(change())
# 输出结果为:
['a', 'b']
['a', 'b', 'b']

代码运行结果显示出默认的my_list在第一次调用后已经发生改变.可以看出Python函数的参数默认值, 是在编译阶段就绑定了对象的.

如何避免这个不必要的麻烦:
当然最好的方式是不要使用可变对象作为函数默认值. 如果非要这么用的话, 将默认值设为None是一个很好的方法:

1
2
3
4
5
6
7
8
def change(my_list=None):
if my_list is None:
my_list = ['a']
my_list.append('b')
return my_list
# 输出结果为:
['a', 'b']
['a', 'b']

这样这个问题就能完美解决了.


Python中下划线的用法

模式 例子 描述
单前导下划线 _var 命名约定, 表示这个变量是私有的. 它通常不由Python解释器强制执行, 仅仅作为程序员的约定俗成
单末尾下划线 var_ 命名约定, 用来避免与Python关键字产生命名冲突
双前导下划线 __var 名称修饰(name mangling), 解释器会更改变量名, 以便在类被扩展的时候不容易产生冲突. 会将变量名更改为: _类名__var
双前导和末尾下划线 __var__ python内部用来表示特殊用途的方法
单下划线 _ 用来表示某个临时的或无关紧要的变量

删除序列相同元素并保持顺序

如果你仅仅就是想消除重复元素, 通常可以简单的构造一个集合, 将序列转换成集合. 然而, 这种方法不能保持元素的顺序, 生成的结果中的元素位置被打乱.
此时我们就可以巧妙地利用生成器来解决这个问题, 例如:

1
2
3
4
5
6
def dedupe(items):    # items 为 hashable 类型的序列
seen = set()
for item in items:
if item not in seen:
yield item # 按顺序返回未重复的元素
seen.add(item) # hash表, 用以保存已经出现过的元素

在使用时只需用该方法, 再将生成器转换为序列即可.

但是当该方法遇到 dict 类型的序列时, 就需做出些许改变:

1
2
3
4
5
6
7
def dedupe(items, key=None):    # items 可以为非 hashable 类型的序列, key为转换序列元素为 hashable 类型的函数
seen = set()
for item in items:
val = item if key is None else key(item) # 若 key 为 None 则不用转换
if val not in seen:
yield item # 按顺序返回未重复的元素
seen.add(val) # hash表, 用以保存已经出现过的元素

可以按下面样例使用:

1
2
3
4
5
6
7
>>>listA = [{'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2},{'x':4, 'y':2}]
>>>list(dedupe(listA, key=lambda d: (d['x'], d['y'])))
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3} ,{'x': 4, 'y': 2}]
>>>list(dedupe(listA, key=lambda d: d['x']))
[{'x': 1, 'y': 2} ,{'x': 4, 'y': 2}]
>>>list(dedupe(listA, key=lambda d: d['y']))
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3}]

Python中 * 号的特殊用法

用作参数

当星号用作参数时, 如 *args, **kwargs , 其中 *args 用来接受任意多个参数并将其放在 args元组 中, 而 **kwargs 用于接收类似于关键参数一样赋值的形式的多个实参放入 kwargs字典中.

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def func(*args, **kwargs):
print(args)
print(kwargs)

func("a", "b")
#输出结果为:
('a', 'b')
{}

func(a="a", b="b", c="c")
#输出结果为:
()
{'a': 'a', 'b': 'b', 'c': 'c'}

func("a", "b", c="c")
#输出结果为:
('a', 'b')
{'c': 'c'}

用作解包

函数在调用多个参数时, 在列表, 元组, 集合, 字典及其他可迭代对象作为实参, 并在前面加 * , 解释器将自动进行解包然后传递给多个单变量参数.

如:

1
2
3
4
5
6
7
8
9
10
11
12
def func(a, b, c):
print(a, b, c)
listA = ["a", "b", "c"]
func(*listA)
#输出结果为:
a b c

dictA = {"a":[1, 4], "b":[2, 5], "c":[3, 6]}
zipped = zip(*dictA.values())
print(list(zipped))
#输出结果为:
[(1, 2, 3), (4, 5, 6)]

将运算符映射到函数

下表显示了抽象操作如何与Python语法中的运算符符号以及 operator 模块中的函数相对应.

运算 语法 函数
加法 a + b add(a, b)
字符串拼接 seq1 + seq2 concat(seq1, seq2)
包含测试 obj in seq contains(seq, obj)
除法 a / b truediv(a, b)
除法取整 a // b floordiv(a, b)
按位与 a & b and_(a, b)
按位异或 a ^ b xor(a, b)
按位取反 ~ a invert(a)
按位或 a | b or_(a, b)
取幂 a ** b pow(a, b)
一致 a is b is_(a, b)
不一致 a is not b is_not(a, b)
索引赋值 obj[k] = v setitem(obj, k, v)
索引删除 del obj[k] delitem(obj, k)
索引取值 obj[k] getitem(obj, k)
左移 a << b lshift(a, b)
取余 a % b mod(a, b)
乘法 a * b mul(a, b)
矩阵乘法 a @ b matmul(a, b)
否定(算术) - a neg(a)
否定(逻辑) not a not_(a)
正数 + a pos(a)
右移 a >> b rshift(a, b)
切片赋值 seq[i:j] = values setitem(seq, slice(i, j), values)
切片删除 del seq[i:j] delitem(seq, slice(i, j))
切片取值 seq[i:j] getitem(seq, slice(i, j))
字符串格式化 s % obj mod(s, obj)
减法 a - b sub(a, b)
真值测试 obj truth(obj)
比较(小于) a < b lt(a, b)
比较(小于等于) a <= b le(a, b)
相等 a == b eq(a, b)
不等 a != b ne(a, b)
比较(大于等于) a >= b ge(a, b)
比较(大于) a > b gt(a, b)

其中, 可以在自定义类中声明 __lt__(a, b) 等函数来使自定义类支持上表中的操作.

格式字符串语法

str.format() 方法和 Formatter 类共享相同的格式字符串语法(虽然对于 Formatter 来说, 其子类可以定义它们自己的格式字符串语法). 具体语法与 格式化字符串字面值 相似, 但也存在区别.

格式字符串包含有以花括号 {} 括起来的”替换字段”. 不在花括号之内的内容被视为字面文本, 会不加修改地复制到输出中. 如果你需要在字面文本中包含花括号字符, 可以通过重复来转义: {{ and }}.

替换字段的语法如下:

类型 语法
replacement_field “{ [field_name] [! conversion] [: format_spec] }”
field_name arg_name (.attribute_name | [element_index])
arg_name [identifier | digit+]
attribute_name identifier
element_index digit+ | index_string
conversion “r” | “s” | “a”
format_spec [[fill]align][sign][#][0][width][grouping_option][.precision][type]
fill <any character>
align “<” | “>” | “=” | “^”
sign “+” | “-“ | “ “
width digit+
grouping_option “_” | “,”
precision digit+
type “b” | “c” | “d” | “e” | “E” | “f” | “F” | “g” | “G” | “n” | “o” | “s” | “x” | “X” | “%”

field_name

用不太正式的术语来描述, 替换字段开头可以用一个 field_name 指定要对值进行格式化并取代替换字符被插入到输出结果的对象. field_name 之后有可选的 conversion 字段, 它是一个感叹号 ‘!’ 加一个 format_spec, 并以一个冒号 ‘:’ 打头. 这些指明了替换值的非默认格式.
field_name 本身以一个数字或关键字 arg_name 打头. 如果为数字, 则它指向一个位置参数, 而如果为关键字, 则它指向一个命名关键字参数. 如果格式字符串中的数字 arg_names 为 0, 1, 2, … 的序列, 它们可以全部省略(而非部分省略), 数字 0, 1, 2, … 将会按顺序自动插入. 由于 arg_name 不使用引号分隔, 因此无法在格式字符串中指定任意的字典键 (例如字符串 ‘10’ 或 ‘:-]’). arg_name 之后可以带上任意数量的索引或属性表达式. ‘.name’ 形式的表达式会使用 getattr() 选择命名属性, 而 ‘[index]’ 形式的表达式会使用 __getitem__() 执行索引查找.

conversion

使用 conversion 字段在格式化之前进行类型强制转换. 通常, 格式化值的工作由值本身的 __format__() 方法来完成. 但是, 在某些情况下最好强制将类型格式化为一个字符串, 覆盖其本身的格式化定义. 通过在调用 __format__() 之前将值转换为字符串, 可以绕过正常的格式化逻辑.
目前支持的转换旗标有三种: ‘!s’ 会对值调用 str(), ‘!r’ 调用 repr() 而 ‘!a’ 则调用 ascii().

format_spec

format_spec 字段包含值应如何呈现的规格描述, 例如字段宽度, 对齐, 填充, 小数精度等细节信息. 每种值类型可以定义自己的”格式化语言”或对 format_spec 的解读方式.
如果指定了一个有效的 align 值, 则可以在该值前面加一个 fill 字符, 它可以为任意字符, 如果省略则默认为空格符. 在 格式化字符串字面值 或在使用 str.format() 方法时是无法使用花括号字面值 (“{“ or “}”) 作为 fill 字符的. 但是, 通过嵌套替换字段插入花括号则是可以的. 这个限制不会影响 format() 函数.

各种对齐选项的含义如下:

选项 意义
‘<’ 强制字段在可用空间内左对齐(这是大多数对象的默认值).
‘>’ 强制字段在可用空间内右对齐(这是数字的默认值).
‘=’ 强制将填充放置在符号(如果有)之后但在数字之前. 这用于以”+000000120”形式打印字段.
此对齐选项仅对数字类型有效. 当’0’紧接在字段宽度之前时, 它成为默认值.
‘^’ 强制字段在可用空间内居中.

请注意, 除非定义了最小字段宽度, 否则字段宽度将始终与填充它的数据大小相同, 因此在这种情况下, 对齐选项没有意义.

sign 选项仅对数字类型有效, 可以是以下之一:

选项 意义
‘+’ 表示标志应该用于正数和负数.
‘-‘ 表示标志应仅用于负数(这是默认行为).
space 表示应在正数上使用前导空格, 在负数上使用减号.

‘#’ 选项可以让”替代形式”被用于转换. 替代形式可针对不同类型分别定义. 此选项仅对整数、浮点、复数和 Decimal 类型有效. 对于整数类型, 当使用二进制、八进制或十六进制输出时, 此选项会为输出值添加相应的 ‘0b’, ‘0o’ 或 ‘0x’ 前缀. 对于浮点数、复数和 Decimal 类型, 替代形式会使得转换结果总是包含小数点符号, 即使其不带小数. 通常只有在带有小数的情况下, 此类转换的结果中才会出现小数点符号. 此外, 对于 ‘g’ 和 ‘G’ 转换, 末尾的零不会从结果中被移除.

‘,’ 选项表示使用逗号作为千位分隔符. 对于感应区域设置的分隔符, 请改用 ‘n’ 整数表示类型.

‘_’ 选项表示对浮点表示类型和整数表示类型 ‘d’ 使用下划线作为千位分隔符. 对于整数表示类型 ‘b’, ‘o’, ‘x’ 和 ‘X’, 将为每 4 个数位插入一个下划线. 对于其他表示类型指定此选项则将导致错误.

width 是一个定义最小字段宽度的十进制整数. 如果未指定, 则字段宽度将由内容确定.

当未显式给出对齐方式时, 在 width 字段前加一个零 (‘0’) 字段将为数字类型启用感知正负号的零填充. 这相当于设置 fill 字符为 ‘0’ 且 alignment 类型为 ‘=’.

precision 是一个十进制数字, 表示对于以 ‘f’ and ‘F’ 格式化的浮点数值要在小数点后显示多少个数位, 或者对于以 ‘g’ 或 ‘G’ 格式化的浮点数值要在小数点前后共显示多少个数位. 对于非数字类型, 该字段表示最大字段大小 —— 换句话说就是要使用多少个来自字段内容的字符. 对于整数值则不允许使用 precision.

最后, type 确定了数据应如何呈现.

可用的字符串表示类型是:

类型 意义
‘s’ 字符串格式.这是字符串的默认类型, 可以省略.
None 和 ‘s’ 一样.

可用的整数表示类型是:

类型 意义
‘b’ 二进制格式. 输出以 2 为基数的数字.
‘c’ 字符.在打印之前将整数转换为相应的unicode字符.
‘d’ 十进制整数. 输出以 10 为基数的数字.
‘o’ 八进制格式. 输出以 8 为基数的数字.
‘x’ 十六进制格式. 输出以 16 为基数的数字, 使用小写字母表示 9 以上的数码.
‘X’ 十六进制格式. 输出以 16 为基数的数字, 使用大写字母表示 9 以上的数码.
‘n’ 数字. 这与 ‘d’ 相似, 不同之处在于它会使用当前区域设置来插入适当的数字分隔字符.
None 和 ‘d’ 相同.

在上述的表示类型之外, 整数还可以通过下列的浮点表示类型来格式化 (除了 ‘n’ 和 None). 当这样做时, 会在格式化之前使用 float() 将整数转换为浮点数.

浮点数和小数值可用的表示类型有:

类型 意义
‘e’ 指数表示. 以使用字母 ‘e’ 来标示指数的科学计数法打印数字. 默认的精度为 6.
‘E’ 指数表示. 与 ‘e’ 相似, 不同之处在于它使用大写字母 ‘E’ 作为分隔字符.
‘f’ 定点表示. 将数字显示为一个定点数. 默认的精确度为 6.
‘F’ 定点表示. 与 ‘f’ 相似, 但会将 nan 转为 NAN 并将 inf 转为 INF.
‘g’ 常规格式. 对于给定的精度 p >= 1, 这会将数值舍入到 p 位有效数字;
再将结果以定点格式或科学计数法进行格式化, 具体取决于其值的大小.

准确的规则如下: 假设使用表示类型 ‘e’ 和精度 p-1 进行格式化的结果具有指数值 exp.
则如果 -4 <= exp < p, 该数字将使用表示类型 ‘f’ 和精度 p-1-exp 进行格式化.
否则的话, 该数字将使用表示类型 ‘e’ 和精度 p-1 进行格式化.
在两种情况下, 都会从有效数字中移除无意义的末尾零, 如果小数点之后没有数字则小数点也会被移除.
正负无穷, 正负零和 nan 会分别被格式化为 inf, -inf, 0, -0 和 nan, 无论精度如何设定.
精度 0 会被视为等同于精度 1. 默认精度为 6.
‘G’ 常规格式. 类似于 ‘g’, 不同之处在于当数值非常大时会切换为 ‘E’. 无穷与 NaN 也会表示为大写形式.
‘n’ 数字. 这与 ‘g’ 相似, 不同之处在于它会使用当前区域设置来插入适当的数字分隔字符.
‘%’ 百分比. 将数字乘以 100 并显示为定点 (‘f’) 格式, 后面带一个百分号.
None 类似于 ‘g’, 不同之处在于当使用定点表示法时, 小数点后将至少显示一位.
默认精度与表示给定值所需的精度一样. 整体效果为与其他格式修饰符所调整的 str() 输出保持一致.

*未完待续…

参考文献

1. Python 3.7.4 文档