Python 中的一些小提示。
1. 关于引用 (Reference)
1.1. 引用 (Reference) 与复制 (Copy)
1.1.1. 引用:浅复制 (Shallow Copy)
浅复制只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。Python 中的引用就是一种浅复制。
在将一个变量赋值给另一个变量时,若变量必须保存可变数据类型的值(例如列表或字典),Python 就会使用引用:
1 | source = [0, 1, 2, 3, 4, 5] |
此处 target = source
只会将指向 source
中列表的引用赋值到 target
,而不是列表值的本身,这就意味存储在 target
和 source
中的值(此处的两个值就是两个引用)现在指向了同一个列表,因此,此时对其中任意一个变量的值进行操作都会影响到那个被指向的列表,从而同时改变两个变量的值:
1 | [0, 'hello', 2, None, 4, 5] |
注意:此处的操作指对变量的值的操作,不包括对变量本身的完全重新赋值。显然,只有对变量的值(此处的值就是引用)进行的修改才会影响到引用指向的内容,如果直接给这个变量名赋一个全新的值,则它将使用一块新的内存,直接覆盖掉这个引用,而不是对引用指向的内容进行修改:
1
2
3
4
5
6
7 source = [0, 1, 2, 3, 4, 5]
target = source
source = ['a', 'b', 'c', 'd', 'e']
print(source)
print(target)此处对
source
进行了完全重新的赋值(而不是对source
的值进行一些操作),因此,并不会对target
造成影响:
1
2 ['a', 'b', 'c', 'd', 'e']
[0, 1, 2, 3, 4, 5]若希望对变量本身的完全重新赋值不使用新内存,而是在原来的内存空间上修改(即保留引用),则可以使用
list[:]
:
1
2
3
4
5
6
7 source = [0, 1, 2, 3, 4, 5]
target = source
source[:] = ['a', 'b', 'c', 'd', 'e']
print(source)
print(target)在这种情况下,在
source
原内存上的修改也会影响到target
:
1
2 ['a', 'b', 'c', 'd', 'e']
['a', 'b', 'c', 'd', 'e']
1.1.2. 深复制 (Deep Copy)
深复制会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响原对象。
在将一个变量赋值给另一个变量时,若变量需要保存的是不可变数据类型的值(例如字符串、整型或元组),Python 变量就会深复制值本身:
1 | source = 42 |
将 42 赋值给变量 source
,然后复制 source
的值并将其赋值给变量 target
;当稍后将 source
中的值改变成 100 时,并不会影响 target
中的值(这是因为这两个变量是不同的变量,保存了不同的对象),因此输出如下:
1 | 100 |
1.2. Python 复制模块
对于可变数据类型(例如列表或字典),如果不想使用引用(浅复制),而是使用深复制进行赋值,可以使用 copy
库中的函数:
copy.copy()
:对于简单对象(指不存在嵌套结构的对象),该函数可以实现深复制,但是对于一个复杂对象(指存在嵌套结构的列表或字典)的子对象,则只能实现浅复制;copy.deepcopy()
:对于所有对象,都能实现深复制;
1 | import copy |
结果如下:
1 | # target1 与 target2 的值相等,但是不是同一个对象(它们分别是原列表 source 的一个复制) |
实例一:函数传递引用
当函数被调用时:
- 可变数据类型(例如列表或字典)的实际参数以引用的形式传入函数中(即函数中的形式参数是实际参数的引用),针对这些形式参数的操作会反应到函数外的实际参数;
- 不可变数据类型(例如字符串、整型或元组)的实际参数则会以复制的形式传入函数中(即函数中的形式参数是实际参数的复制),针对这些形式参数的操作不会影响函数外的实际参数;
1 | def eggs(para1, para2): |
此处 eggs()
函数分别对形式参数 para1
和 para2
做了一些操作,不同之处在于:
para1
的值来自实际参数former
,这是一个列表,因此传入到para1
的是一个指向列表[1, 2, 'toast']
的引用,所以eggs()
进行的操作会反馈到former
上;para2
的值来自实际参数latter
,这是一个字符串,因此传入到para2
的是字符串'Cheese'
的一个复制,所以eggs()
进行的操作不会影响latter
;
1 | [1, 2, 'toast', 'bacon'] |
实例二:二维数组赋值问题
如果使用如下代码声明一个二维数组:
1 | myArray = [[0] * 5] * 9 |
则可以得到:
1 | [[0, 0, 0, 0, 0], |
如果想要对这个二维数组某个元素进行赋值:
1 | myArray[1][2] = 1 |
会发现整列都会被赋值:
1 | [[0, 0, 1, 0, 0], |
这是因为 [[0] * 5] * 9
表示的是 9 个指向 [0]*5
这个列表的引用,所以当修改其中的某一个值时,整个列表都会被改变。
换一种初始化方式可以解决这个问题:
1 | myArray = [[0] * 5 for _ in range(9)] |
则可以对这个二维数组某个元素进行赋值:
1 | myArray[1][2] = 1 |
结果为:
1 | [[0, 0, 0, 0, 0], |
2. 字符串中引用变量
连字符 +
1 | name = 'Vinn' |
占位字符 %
1 | name = 'Vinn' |
方法 .format()
1 | name = 'Vinn' |
标记字符串
- 字符串前加
u
:表示该字符串以 Unicode 格式进行编码,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出现乱码; - 字符串前加
r
:取消转义字符,表示该字符串中\
不再具有转义能力(\n
、\t
等不再具有特殊含义); - 字符串前加
b
:表示该字符串是 Bytes 类型(网络编程中,服务器和浏览器只认 Bytes 类型数据); - 字符串前加
f
:表示在该字符串内,{}
中的内容被视为 python 表达式(可以在此引用变量);
1 | str1 = u"我是含有中文字符组成的字符串。" # 防止中文乱码 |
3. 帮助函数
dir()
dir()
不带参数时,返回当前工作空间内的所有变量、方法和定义的类型列表;带参数时,返回参数的属性、函数或方法的列表。可用于查看某个模块/库包含的所有函数,某个数据类型/变量包含的所有方法:
1 | # 语法:其中 object 可以是对象、变量、类型等 |
help()
help()
用于查看模块/函数/数据类型的详细说明:
1 | # 语法:object 为具体的模块、数据类型、函数等 |
属性文档
每个对象都会有一个 __doc__
属性,用于描述该对象的作用。该属性的内容来源与:
- 对于文件:一个文件的任意一条可执行的代码之前的内容;
- 对于类:在类定义语句之后,任意一条可执行代码之前的内容;
- 对于函数:在函数定义语句之后,任意一条可执行代码之前的内容;
1 | class Drunk: |
输出为:
1 | This is an example for the document of Class. |
4. 常用函数
Counter()
Counter()
函数用于遍历列表中的所有元素,并记录所有元素的出现个数,返回一个类似字典 Dict 的 Counter 类对象(事实上,Counter 是 Dict 的子类):
1 | from collections import Counter |
输出为:
1 | Counter({6: 1, 'Sunday': 1, 3: 2, 5: 1, 7: 3}) |
注意:
- 在字典 Dict 中查找不存在的键,程序会抛出
KeyError
的异常(理解为不存在的元素不在字典里,因此报错); - 但是由于 Counter 本质是用于统计的计数器,因此,在 Counter 中查找一个不存在的元素,程序不会返回异常,而是返回 0(可以理解为不存在的元素出现了 0 次);
Counter 额外支持字典中没有的三个方法:.elements()
、.most_common(n)
以及 .subtract(iterable-or-mapping)
。
.elements()
该方法基于 Counter 类对象生成对应的原始序列(即按照 Counter 的计数,列出所有元素),返回一个迭代器,可以通过 list ()
或者其它方式得到该迭代器的具体内容:
1 | from collections import Counter |
输出为:
1 | <itertools.chain object at 0x7fc357a6f280> |
注意:在 Counter 中是允许计数为 0 或者负值的,不过通过上面代码可以看出 .elements()
没有将 0 和负值对应的元素值打印出来。
.most_common()
.most_common(n)
是 Counter 最常用的方法,该方法取出出现次数 Top n 的元素,并返回一个由 (元素, 出现次数)
元祖所组成的列表(按出现次数降序排序):
1 | from collections import Counter |
输出为:
1 | [('c', 3), ('b', 2), ('a', 1)] # 不输入 n,默认返回所有元素 |
.subtract()
subtract([iterable_or_mapping])
方法其实就是将两个 Counter 对象中的元素对应的计数相减:(当某个 Counter 中对应的元素不存在时,默认将其计数设置为 0,相减后可能产生负数)
1 | from collections import Counter |
输出为:
1 | Counter({'c': 1, 'a': 0, 'b': -1, 'd': -2}) |
集合运算符
对 Counter 可以直接进行集合运算:
1 | 3, b=1) c = Counter(a= |
map()
该函数会对给定序列中的每个元素调用一次指定函数,并以迭代器的形式返回结果序列(可以通过 list ()
或者其它方式得到该迭代器的具体内容):
1 | # 语法:function 为某个函数;iterable 为一个或多个序列 |
输出为:
1 | <map at 0x7ff11493eee0> |
注意:经常和 lambda 匿名函数配合使用:
1 | # 使用 lambda 匿名函数 |
输出为:
1 | <map at 0x7ff1149589d0> |
enumerate()
该函数会将一个可遍历的数据对象(列表、元祖、字符串)的每个元素与该元素的索引组合成一个元祖,并返回以迭代器的形式返回这些元祖组成的列表(可以通过 list ()
或者其它方式得到该迭代器的具体内容):
1 | # 语法:sequence 为一个序列/迭代器,或其他支持迭代的对象;start 参数表示第一个索引的值 |
输出为:
1 | <enumerate at 0x7ff114907e80> |
注意:经常与 for
循环配合使用:
1 | # 在 for 循环中使用 |
输出为:
1 | 0 Spring |
zip()
该函数用于将多个可迭代对象中对应位置的元素打包成元组,然后以迭代器的形式返回由这些元组组成的列表(可以通过 list ()
或者其它方式得到该迭代器的具体内容):
1 | # 语法:iterable 表示一个或多个迭代器 |
输出为:
1 | <zip at 0x7ff11496e380> |
注意:
- 如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同;
- 利用
*
号操作符,可以将元组解压为列表(相当于zip()
的逆向操作);
1 | short = [1,2,3] |
输出为:
1 | [(1, 4), (2, 5), (3, 6)] |
list.sort()
In python, the .sort()
method sorts a list using Timsort algorithm which has O(n) additional space where n
is the number of the elements.
list 的 .sort()
方法用于对原列表进行排序:
1 | # 语法: |
输出为:
1 | ['me', 'job', 'how', 'work', 'practice'] |
sorted()
sorted()
作为 Python 内置函数之一,其功能是对序列(列表、元组、字典、集合、还包括字符串)进行排序(使用 sorted()
函数并不会在原序列的基础上进行修改,而是会重新生成一个排好序的列表):
1 | # 语法: |
输出为:
1 | [1, 2, 3, 4, 5] |
5. bisect 包
bisect
为 python 内置模块,用于有序序列中的插入与查找:
1 | import bisect |
注意: bisect()
函数常用于直接替代二分查找的代码。
6. nonlocal
当一个函数中嵌套有另一个函数时,有时内部函数需要对外部函数的一些变量进行操作:
如果需要操作的变量是可变数据类型的值(例如列表或字典),则由于浅复制机制,内部函数可以直接使用变量名对该变量进行操作(内部函数中的同名变量是外部函数中原始变量的引用,对其操作会直接影响外部函数中该变量的值);
1
2
3
4
5
6
7
8
9
10
11def outter():
variable = [1,2,3,4,5]
def inner():
variable[2] = 'NewYork'
inner()
print(variable)
# 输出为:
# [1, 2, 'NewYork', 4, 5]如果需要操作的变量是不可变数据类型的值(例如字符串、整型或元组),则由于深复制机制,内部函数不能直接使用变量名对该变量进行操作(内部函数中的同名变量是外部函数中原始变量的复制,两者并不共享内存,可以视为两个不同的变量,因此对该变量的直接操作并不会影响到原始变量的值,甚至可能会报错:
variable referenced before assignment
);1
2
3
4
5
6
7
8
9
10
11def outter():
variable = 42
def inner():
variable += 100
inner()
print(variable)
# 将会报错:
# UnboundLocalError: local variable 'variable' referenced before assignment
为了能够在内部函数直接操作不可变数据类型的值(例如字符串、整型或元组),可以使用 nonlocal
语句:该语句只能在内部函数中使用,经过 nonlocal
声明后的外部变量可以直接由内部函数进行操作
1 | def outter(): |
7. 循环 else 语句
考虑如下结构:
1 | matrix = [[1,-2,5,-7,-8,9], |
此处 else
语句并没有与 if
语句对齐,而是与一个 for
循环语句对齐,这表示:如果该 for
循环正常结束而非被 break
,那么将执行循环下面的 else
语句,否则不执行 else
语句。因此该代码表示:求矩阵中所有不含负数的行的元素之和。
循环 else
相当于其他语言中用来检测是否触发 break
的一个布尔 flag
(无需额外声明)。