Python细节小小知识点(一)
重新看了廖雪峰的Python,发现之前学过的后来都没用过导致记忆模糊哦,所以搬运过来了
一、字符串与编码
1.1 编码格式转换
在最新的Python 3版本中
,字符串是以Unicode
编码的,也就是说,Python的字符串支持多语言。对于单个字符的编码,Python提供了ord()
函数来获取字符的整数表示,chr()
函数把编码转换成对应的字符。
1 | ord('A') |
如果知道字符的整数编码,还可以用十六进制写str
,两种写法等价。
1 | '\u4e2d\u6587' |
由于Python的字符串类型是str
,在内存中以Unicode
表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str
变为以字节为单位的bytes
。Python对bytes
类型的数据用带b
前缀的单引号或双引号表示。
1 | x = b'ABC' |
注意区分ABC
和b'ABC'
,前者是str
,后者虽然内容显示得和前者一样,但是bytes
的每个字符都只占用一个字节。以Unicode
表示的str
通过encode()
方法可以编码为指定的bytes
。
1 | 'ABC'.encode('ascii') |
纯英文的str
可以用ASCII
编码为bytes
,内容是一样的,含有中文的str
可以用UTF-8
编码为bytes
。含有中文的str
无法用ASCII
编码,因为中文编码的范围超过了ASCII
编码的范围,Python会报错。
在bytes
中,无法显示为ASCII字符的字节,用\x##
显示。反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes
。要把bytes
变为str
,就需要用decode()
方法。
1 | b'ABC'.decode('ascii') |
如果bytes
中包含无法解码的字节,decode()
方法会报错
1 | b'\xe4\xb8\xad\xff'.decode('utf-8') |
如果bytes
中只有一小部分无效的字节,可以传入errors='ignore'
忽略错误的字节
1 | b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore') |
在操作字符串时,我们经常遇到str
和bytes
的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8
编码对str
和bytes
进行转换。
1.2 申明编码格式
由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8
编码。当Python解释器读取源代码时,为了让它按UTF-8
编码读取,我们通常在文件开头写上这两行:
1 | #!/usr/bin/env python3 |
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8
编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
申明了UTF-8
编码并不意味着你的.py
文件就是UTF-8
编码的,必须并且要确保文本编辑器正在使用UTF-8 without BOM
编码:
1.3 格式化输出
%占位符
在Python中,采用的格式化方式和C语言是一致的,用%
实现,举例如下:
1 | 'Hello, %s' % 'world' |
你可能猜到了,%
运算符就是用来格式化字符串的。在字符串内部,%s
表示用字符串替换,%d
表示用整数替换,有几个%?
占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?
,括号可以省略。
常见的占位符有:
占位符 | 替换内容 |
---|---|
%d | 整数 |
%f | 浮点数 |
%s | 字符串 |
%x | 十六进制整数 |
其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数
1 | print('%2d-%02d' % (3, 1)) #3-01 |
如果你不太确定应该用什么,%s
永远起作用,它会把任何数据类型转换为字符串
1 | 'Age: %s. Gender: %s' % (25, True) |
有些时候,字符串里面的%
是一个普通字符,用%%
来表示一个%
1 | 'growth rate: %d %%' % 7 |
format()
另一种格式化字符串的方法是使用字符串的format()
方法,它会用传入的参数依次替换字符串内的占位符{0}
、{1}
……,不过这种方式写起来比%要麻烦得多:
1 | 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) |
f-string
最后一种格式化字符串的方法是使用以f
开头的字符串,称之为f-string
,它和普通字符串不同之处在于,字符串如果包含{xxx}
,就会以对应的变量替换
1 | 2.5 r = |
上述代码中,{r}
被变量r
的值替换,{s:.2f}
被变量s
的值替换,并且:
后面的.2f
指定了格式化参数(即保留两位小数),因此,{s:.2f}
的替换结果是19.62
。
二、列表、元组、字典
2.1 列表
list是一个可变的有序表,所以,可以往list中追加元素到末尾:
1 | 'Adam') classmates.append( |
也可以把元素插入到指定的位置,比如索引号为1
的位置:
1 | 1, 'Jack') classmates.insert( |
要删除list末尾的元素,用pop()
方法:
1 | classmates.pop() |
要删除指定位置的元素,用pop(i)
方法,其中i
是索引位置:
1 | 1) classmates.pop( |
要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:
1 | 1] = 'Sarah' classmates[ |
list里面的元素的数据类型也可以不同,比如:
1 | 'Apple', 123, True] L = [ |
list元素也可以是另一个list,比如:
1 | 'python', 'java', ['asp', 'php'], 'scheme'] s = [ |
要拿到'php'
可以写s[2][1]
,因此s
可以看成是一个二维数组
,类似的还有三维、四维……数组,不过很少用到。
2.2 元组
要定义一个只有1个元素的tuple,如果你这么定义:
1 | 1) t = ( |
定义的不是tuple,是1
这个数!这是因为括号()
既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1
。
所以,只有1个元素的tuple定义时必须加一个逗号,
,来消除歧义:
1 | 1,) t = ( |
Python在显示只有1个元素的tuple时,也会加一个逗号,
,以免你误解成数学计算意义上的括号。
“可变的”tuple:
1 | 'a', 'b', ['A', 'B']) t = ( |
2.3 字典
把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:
1 | 'Adam'] = 67 d[ |
由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:
1 | 'Jack'] = 90 d[ |
如果key不存在,dict就会报错:
1 | 'Thomas'] d[ |
要避免key不存在的错误,有两种办法,一是通过in
判断key是否存在:
1 | 'Thomas' in d |
二是通过dict提供的get()
方法,如果key不存在,可以返回None
,或者自己指定的value:
1 | 'Thomas') d.get( |
注意:返回None
的时候Python的交互环境不显示结果。
要删除一个key,用pop(key)
方法,对应的value也会从dict中删除:
1 | 'Bob') d.pop( |
请务必注意,dict内部存放的顺序和key放入的顺序是没有关系的。dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象。
这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)
。
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
1 | 1, 2, 3] key = [ |
2.4 集合
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
要创建一个set,需要提供一个list作为输入集合:
1 | set([1, 2, 3]) s = |
注意,传入的参数[1, 2, 3]
是一个list,而显示的{1, 2, 3}
只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。
重复元素在set中自动被过滤:
1 | set([1, 1, 2, 2, 3, 3]) s = |
通过add(key)
方法可以添加元素到set中,可以重复添加,但不会有效果:
1 | 4) s.add( |
通过remove(key)
方法可以删除元素:
1 | 4) s.remove( |
set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
1 | set([1, 2, 3]) s1 = |
set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。
1 | 'c', 'b', 'a'] a = [ |
1 | 'abc' a = |
三、一些内置函数
3.1 isinstance()
数据类型检查可以用内置函数isinstance()
实现:
1 | def my_abs(x): |
3.2 默认参数
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END
再返回:
1 | def add_end(L=[]): |
当你正常调用时,结果似乎不错:
1 | 1, 2, 3]) add_end([ |
当你使用默认参数调用时,一开始结果也是对的:
1 | add_end() |
但是,再次调用add_end()
时,结果就不对了:
1 | add_end() |
很多初学者很疑惑,默认参数是[]
,但是函数似乎每次都“记住了”上次添加了'END'
后的list。
原因解释如下:
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None
这个不变对象来实现:
1 | def add_end(L=None): |
现在,无论调用多少次,都不会有问题:
1 | add_end() |
为什么要设计str
、None
这样的不变对象
呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
3.3 可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
我们以数学题为例子,给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
1 | def calc(numbers): |
但是调用的时候,需要先组装出一个list或tuple:
1 | 1, 2, 3]) calc([ |
如果利用可变参数,调用函数的方式可以简化成这样:
1 | 1, 2, 3) calc( |
所以,我们把函数的参数改为可变参数:
1 | def calc(*numbers): |
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
1 | 1, 2) calc( |
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
1 | 1, 2, 3] nums = [ |
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
1 | 1, 2, 3] nums = [ |
*nums
表示把nums
这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
3.4 关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
1 | def person(name, age, **kw): |
函数person
除了必选参数name
和age
外,还接受关键字参数kw
。在调用该函数时,可以只传入必选参数:
1 | 'Michael', 30) person( |
也可以传入任意个数的关键字参数:
1 | 'Bob', 35, city='Beijing') person( |
关键字参数有什么用?它可以扩展函数的功能。比如,在person
函数里,我们保证能接收到name
和age
这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
1 | 'city': 'Beijing', 'job': 'Engineer'} extra = { |
当然,上面复杂的调用可以用简化的写法:
1 | 'city': 'Beijing', 'job': 'Engineer'} extra = { |
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
3.5 命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw
检查。
仍以person()
函数为例,我们希望检查是否有city
和job
参数:
1 | def person(name, age, **kw): |
但是调用者仍可以传入不受限制的关键字参数:
1 | 'Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456) person( |
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
1 | 'Jack', 24, city='Beijing', job='Engineer') person( |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名
,这和位置参数不同。如果没有传入参数名,调用将报错:
1 | 'Jack', 24, 'Beijing', 'Engineer') person( |
由于调用时缺少参数名city
和job
,Python解释器把前两个参数视为位置参数,后两个参数传给*args
,但缺少命名关键字参数导致报错。
命名关键字参数可以有缺省值,从而简化调用:
1 | def person(name, age, *, city='Beijing', job): |
由于命名关键字参数city
具有默认值,调用时,可不传入city
参数:
1 | 'Jack', 24, job='Engineer') person( |
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
1 | def person(name, age, city, job): |
3.6 参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。