Python技巧与规范整理
本文章持续随缘更新.最后更新时间: 2019-08-24
如何立即跳出两层嵌套循环
两层循环:
1 | for i in listA: |
一般跳出两层嵌套循环有以下几种常见的方法:
- 将循环放在一个函数中进行, 利用函数返回来跳出循环. 但这样的做法不尽如人意, 因为循环可能不适合重构为一个新的函数, 也许你需要在循环的过程中访问其他局部变量.
- 抛出异常, 并在两重循环之外捕获它. 这是把异常当作 goto 语句来用了. 但是这里并没有异常的条件, 只是变向地利用了异常机制.
- 使用布尔变量来标记循环的结束, 并在外循环中检查变量值以执行第二次 break 操作. 该方法毫无技术成分, 某些情况下可能是有效的, 但大多数情况下只造成计算资源浪费, 程序效率不高.
而对于python, 有更为简单的方法, python非常擅长抽象化迭代过程, 如果把python当作一般的语言来使用, 不能发挥循环抽象的优势. 我们甚至可以把两层循环抽象为一层循环, 可以思考一下, 如何用python的语法将两层循环改写为一层循环?
下面给出答案:
1 | def get_pairs(listA, listB): |
此处, 我们写了一个生成器用于生成需要的索引对. 现在, 我们的循环就成了对索引对的一重循环, 而不是对索引的两重循环. 两重循环依然存在, 只是被抽象了出去, 移到了 get_pairs() 方法里面.
这使我们的代码更贴近自然语言的描述. 而且, 如果在其他地方也需要这样迭代的话, 还能复用 get_pairs 生成器.
Python函数是传值还是传引用
关于Python函数是传值还是传引用, 总结起来大概有三种说法:
- 1.传引用, 类似于C语言的指针, 传入参数的引用, 当更改传入值时, 原值也会改变.
下面举一个例子:
1 |
|
可以清楚地看到在函数内更改值, 原值并没有发生变化, 所以并不是传引用.
- 2.传值, 和字面意思一样, 只是传入原对象的值的复制, 在函数内更改传入值, 并不会改变原值.
这时再举一个例子:
1 | def changeList(x): |
可以看到, 在函数内更改列表的值时, 原列表的值也发生了变化, 所以并不是传值.
- 3.可变对象传引用, 不可变对象传值
还举一个例子
1 | def changeList(x): |
可以看到, 这次传入的参数虽然是可变对象, 但是并没有改变原来的值, 所以这个说法也不正确.
那么python函数传的参数到底是什么?
我们先看一下python在赋值语句中对应的内存变化图:
在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 | def change(my_list=['a']): |
代码运行结果显示出默认的my_list在第一次调用后已经发生改变.可以看出Python函数的参数默认值, 是在编译阶段就绑定了对象的.
如何避免这个不必要的麻烦:
当然最好的方式是不要使用可变对象作为函数默认值. 如果非要这么用的话, 将默认值设为None是一个很好的方法:
1 | def change(my_list=None): |
这样这个问题就能完美解决了.
Python中下划线的用法
模式 | 例子 | 描述 |
---|---|---|
单前导下划线 | _var | 命名约定, 表示这个变量是私有的. 它通常不由Python解释器强制执行, 仅仅作为程序员的约定俗成 |
单末尾下划线 | var_ | 命名约定, 用来避免与Python关键字产生命名冲突 |
双前导下划线 | __var | 名称修饰(name mangling), 解释器会更改变量名, 以便在类被扩展的时候不容易产生冲突. 会将变量名更改为: _类名__var |
双前导和末尾下划线 | __var__ | python内部用来表示特殊用途的方法 |
单下划线 | _ | 用来表示某个临时的或无关紧要的变量 |
删除序列相同元素并保持顺序
如果你仅仅就是想消除重复元素, 通常可以简单的构造一个集合, 将序列转换成集合. 然而, 这种方法不能保持元素的顺序, 生成的结果中的元素位置被打乱.
此时我们就可以巧妙地利用生成器来解决这个问题, 例如:
1 | def dedupe(items): # items 为 hashable 类型的序列 |
在使用时只需用该方法, 再将生成器转换为序列即可.
但是当该方法遇到 dict 类型的序列时, 就需做出些许改变:
1 | def dedupe(items, key=None): # items 可以为非 hashable 类型的序列, key为转换序列元素为 hashable 类型的函数 |
可以按下面样例使用:
1 | >>>listA = [{'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2},{'x':4, 'y':2}] |
Python中 * 号的特殊用法
用作参数
当星号用作参数时, 如 *args, **kwargs
, 其中 *args
用来接受任意多个参数并将其放在 args元组 中, 而 **kwargs
用于接收类似于关键参数一样赋值的形式的多个实参放入 kwargs字典中.
例如:
1 | def func(*args, **kwargs): |
用作解包
函数在调用多个参数时, 在列表, 元组, 集合, 字典及其他可迭代对象作为实参, 并在前面加 * , 解释器将自动进行解包然后传递给多个单变量参数.
如:
1 | def func(a, b, c): |
将运算符映射到函数
下表显示了抽象操作如何与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() 输出保持一致. |
*未完待续…