该笔记摘记了《Python 语言及其应用》、《Python 核心编程》等几本书的内容,从中你可以了解到关于 Python 语言的基本使用,同时也会深入讨论一些编码上的细节问题。此外,该笔记也穿插记录了关于 Python 语言的诸多面试问题
2 Python 基本元素:数字、字符串和变量
2.1 变量、名字和对象
Python 里所有数据——布尔值、整数、浮点数、字符串,甚至大型数据结构、函数以及程序——都是以对象(object) 的形式存在的
对象类型决定了它装着的数据是允许被修改的变量还是不可被修改的常量
可以把不可变对象想象成一个透明但封闭的盒子:你可以看到里面装的数据,但是无法改变它
类似地,可变对象就是一个开着口的盒子,你不仅可以看到里面的数据,还可以拿出来修改它,但是你无法改变这个盒子本身,即你无法改变对象的类型
Python 是强类型的(strong typed),你永远无法修改一个已有对象的类型,即使它包含的值是可变的
【注】我们解释一下什么叫做强类型。如果一门语言倾向于不对变量的类型做隐式转换,那我们将其称之为强类型语言。具体可以看一下下面的代码
Python 是强类型语言,并不会隐式地转换 int 或其他类型的变量为 bool 型,但是有时我们又会有下面的问题
其实这里是变量类中的 __bool__
方法让我们产生了错觉,Python 在判断一个变量的布尔值时,如果该变量有 __bool__
方法,那么解释器就会调用该方法判断变量是否为真
另外,bool 类的父类是 int,在Python 中 ==
其实调用的是 __eq__
方法,而 bool 类继承自 int 类,又没有重写 __eq__
方法,所以在使用 ==
的时候回,bool 类的对象自然就会调用父类的 __eq__
方法,自然就会出现上面的情况
Python 中的变量有一个非常重要的性质:它仅仅是一个名字。赋值操作并不会实际复制值,它只是为数据对象取个相关的名字。名字是对对象的引用而不是对象本身。你可以把名字想象成贴在盒子上的标签
而对于如下的操作
a = 7
print(a)
b = 7
print(b)
我们的解释就是在如图 2-3 所示的盒子上我们又贴上了标签 b
2.2 数字
/
用来执行浮点除法;//
用来执行整除
int('99')
可以将仅包含数字和正负号的字符串转换成整数,float('1.0e4')
同理
2.3 字符串
将一系列字符包裹在一对单引号(或双引号)中即可创建字符串,无论使用哪种引号,Python 对字符串的处理方式都是一样的
为什么要引入两种引号呢?这么做的好处是可以创建本省就包含引号的字符串,而不使用转义符
- 使用
str()
将其他数据类型转换为字符串 - 使用
+
拼接字符串 - 使用
*
复制字符串 - 使用
[]
提取字符串 - 使用
len()
获得字符串长度 - 使用
fnid()
查找模式串第一次出现的位置 - 使用
rfind()
查找模式串最后一次出现的位置 - 使用
count()
统计模式串出现的次数 - 使用
[start:en:step]
分片,如果步长为负数,表示从右往左反向进行提取操作 - 使用
split()
进行分割,如果不指定分隔符,那么split()
函数将默认使用空白符 - 使用
join(list)
将包含若干子串的列表分解,并将这些子串合成一个完整的大字符串 - 使用
strip()
移除字符串头尾指定的字符(默认为空格或换行符)或字符序列 - 使用
capitalize()
让字符串首字母变成大写 - 使用
title()
让所有单词的开头字母变成大写 - 使用
upper()
将所有字母都变成大写 - 使用
lower()
将所有字母转换成小写 - 使用
swapcase()
将所有字母的大小写转换 - 使用
replace()
替换子串,如果最后给一个参数省略,则默认值替换第一次出现的位置
3 Python 容器:列表、元组、字典与集合
3.2 列表
- 创建空列表
empty_list=[]
或者empty_list=list()
- 使用
list()
将其他数据类型转换为列表类型 - 同字符串的操作一样,列表也可以使用切片
- 使用
append()
添加在尾部元素 - 使用
extend()
或者+=
合并列表 - 使用
insert()
在指定位置插入元素,如果指定的偏移量超过了尾部,则会插入到列表最后 - 使用
del()
删除指定元素,当列表中一个元素被删除后,位于它后面的元素会自动往前移动填补空出的位置,且列表长度 -1 - 使用
remove()
删除具有指定值的元素,此时我们不关系元素在列表中的位置 - 使用
pop()
获取并删除指定位置的元素,pop()
返回列表尾元素,pop(0)
返回列表头元素 - 使用
index()
查询特定值元素的位置 - 使用
in
判断值是否存在 - 使用
count()
记录特定值出现的次数 - 使用
join()
转换为字符串,注意join()
是字符串方法,即string.join(list)
- 使用
sort()
重新排列元素,默认是升序排列,添加参数reverse=True
可以改变为降序排列
sort()
是列表方法,对原列表排序,改变列表内容
sorted()
是函数,返回拍好序的列表的副本,原列表内容不变 - 使用
len()
获取长度
关于赋值和复制的问题
使用 =
赋值,使用 copy()
复制
如果还记得我们说的,在 Python 中,=
赋值相当于给原对象在贴上一个标签,就不难理解为什么下面的代码中 b 也发生了变化
如果希望得到一个新的对象,改变 a 的值时,b 不会发生改变,那么就需要复制对象,常用的将一个列表值复制到另外一个新的列表中有三种方法:
b = a.copy()
b = list(a)
b = a[:]
计数问题
标准库给出的一个计数器为 Counter()
两个计数器可是使用 +
组合在一起,也可以使用 -
从一个计数器中去掉另一个,此处和集合的运算是相似的
3.3 元组
创建空元组 empty_tuple = ()
创建包含一个或多个元素的元组时,每一个元素后面都需要跟着一个逗号,即使只包含一个元素也不能省略
元组解包
元组相对于列表来说有如下几点好处:
- 元组占用的空间较小
- 你不会意外修改元组的值
- 可以将元组用作指点的键
- 命名元组可以作为对象的替代
- 函数的参数是以元组形式传递的
3.4 字典
- 创建空字典
empty_dist={}
- 使用
dict()
将包含双值子序列的序列转换成字典,每个子序列的第一个元素作为键,第二个元素作为值
- 使用
[key]
添加或修改关键字 - 使用
update()
合并字典,如果待添加的字典与待扩充的字典包含相同的键,新归入字典的值会取代原有的值 - 使用
del mydist[key]
删除具有指定键的元素 - 使用
clear()
删除所有元素 - 使用
in
判断是否存在 - 使用
keys()
获取所有键,在 Python3 中keys()
返回的是dict_keys()
,它是键的迭代形式,需要手动调用list()
转换为列表 - 使用
values()
获得所有值,同样需要手动调用list()
转换为列表 - 使用
items()
获得所有键值对,每一个键值对以元组的形式返回 - 使用
=
赋值,使用copy()
复制
处理缺失的键
直接读取字典中不存在的键的值会抛出异常
使用 dict.get(key, default=None)
获取元素,如果键存在,会得到与之对应的值;反之,会得到 None
使用 dict.setdefault(keyname, value)
类似于 get()
,但当键不存在时它会在字典中添加一项,但是当字典中存在相应键的时候并不会改变键的值
defaultdict()
也有同样的用法,但是在创建字典时,对每个新的键都会指定默认值。它的参数是一个函数,例如下面的代码,把函数 int 作为参数传入,会按照 int( ) 调用,返回整数 0
在处理一些具体问题的时候,可以使用 lambda 函数作为 defaultdict()
的参数
按键排序
有序字典 OrdereddDict()
记忆字典键添加的顺序,然后从一个迭代器按照相同的顺序返回
【 dict 的底层实现】
在 Python 中,字典是通过散列表(或者说哈希表)实现的
字典也叫哈希数组或关联数组,所以其本质是数组(如下图),每个 b u c k e t bucket bucket 有两部分:一个是键对象的引用,一个是值对象的引用。所有 b u c k e t bucket bucket 结构和大小一致,我们可以通过偏移量来读取指定 b u c k e t bucket bucket
注意,键必须是可哈希的,Python 中所有不可变的内置类型都是可哈希的,可变类型(如列表、字典、集合)就是不可哈希的
3.5 集合
集合要求无序、不重复
- 创建空集合
empty_set=set()
- 使用
set()
将已有列表、字符串、元组或字典的内容转换为集合,重复的值被丢弃,注意将字典转换为集合时只有键会被使用 - 使用
in
测试值是否存在 - 使用
&
或者intersection()
来获取集合的交集 - 使用
|
或者union()
来获取集合的并集 - 使用
-
或者difference()
来获取两个集合的差集 - 使用
^
或者symmetric_difference()
来获取两个集合的异或 - 使用
<=
或者issubset()
判断一个集合是否是另一个集合的子集
4 Python 外壳:代码结构
使用 while...else
遍历检查某一数据时,找到满足条件的解使用 break 跳出;循环结束,即没有找到满足的解,将执行 else 部分代码
当然也有 for...else
使用 itertools 迭代代码结构
itertools 包含特殊用途的迭代器函数,在 for...in
循环中调用迭代函数,每次会返回一项,并记住当前调用状态
itertools.chain()
方法可以用来简化这个任务。 它接受一个可迭代对象列表作为输入,并返回一个迭代器,有效的屏蔽掉在多个容器中迭代细节
itertools.cycle()
是一个在它的参数之间循环的无限迭代器
itertools.accumulate()
计算累积的值,默认的话,它计算的是累加和;也可以把一个函数作为 accumulate()
的第二个参数,代替默认的加法,这个参数函数应该接受两个参数,返回单个结果
并行迭代
通过 zip()
多个序列进行并行迭代,zip()
在最短序列用完时即停止。同时,zip()
可以将两项序列,比如元组、列表、字符串,创建成一个字典
4.6 推导式
列表推导式
最简单的形式: [expression for item in iterable]
如果需要加上条件表达式 [expression for item in iterable if condition]
字典推导式
类似地有,{key_expression: value_expression for expression in iterable}
第二个 cell 的内容是因为单词 letter 中出现了两次 e 和 t,按照第一种方法,会重复计算 e 和 t 意义不大
集合推导式
类似地有,{expression for expression in iterable}
生成器推导式
要注意的是元组没有推导式,不是把列表推导式中的方括号换成圆括号就可以定义元组推导式
(expression for expression in iterable)
返回的是一个生成器对象,我们可以对生成器对象直接迭代,或者通过调用 list()
函数转换为列表
一个生成器只能运行一次,其值仅在运行过程中产生,不会被保存下来,所以不能重新使用或者备份一个生成器
4.7 函数
我们需要把 None 和不含任何值的空数据结构区分开,0 值的整型/浮点型、空字符串、空列表、空元组、空字典、空集合都等价于 False,但是不等于 None
位置参数
Python 处理参数的方式比其他语言更加灵活。其中,最熟悉的参数类型是位置参数,传入参数的值是按照顺序依次复制过去的
关键字参数
为了避免位置参数带来的混乱,调用参数时可以指定对应参数的名字。我们也可以把位置参数和关键字参数混合起来,记住位置参数放前面
使用 *args 收集位置参数
当参数被用在函数内部时,星号将一组可变数量的位置参数集合成参数值的元组。如果同时有限定的位置参数,那么 *args
会收集剩下的参数
使用 **kwargs 收集关键字参数
使用两个星号可以将参数收集到一个字典中,参数的名字是字典的键,对应参数的值是字典的值。在函数内部 kwargs
是一个字典
如果把 *args
和 **kwargs
的位置参数混合起来,它们会按照顺序解析
4.8 闭包
在 Python 中,可以在函数中定义另外一个函数,而内部函数即可以看作是一个闭包
闭包是一个可以由另外一个函数动态生成的函数,并且可以改变和存储函数外创建的变量的值
4.9 匿名函数 lambda( )
4.10 生成器
生成器函数和普通函数类似,但是它的返回值使用 yield
语句声明而不是
4.11 装饰器
有时你需要在不改变源代码的情况下修改已经存在的函数,此时就可以用装饰器
装饰器实质上是一个函数,它把一个函数作为输入并且返回另外一个函数
4.12 命名空间和作用域
为了读取全局变量而不是函数中的局部变量,需要在变脸前面显示地添加关键字 global
Python 提供了两个获取命名空间内容的函数:
locals()
返回一个局部命名空间内容的字典globals()
返回一个全局命名空间内容的字典
4.13 使用 try 和 except 处理错误
有时需要除了异常类型以外的其他细节,可以使用 except exceptiontype as name
获取整个异常对象
在自己定义异常类型的时候,需要注意,一个异常是一个类,即类 Exception 的一个子类
5 Python 盒子:模块、包和程序
把多个模块组织成文件层次,即称为包
假设我们在 sources 目录下有两个模块:func1.py 和 func2.py,此时还需要在 sources 目录下添加 一个文件: init.py,这个文件可以是空的,但是 Python 需要这个文件,以便把该目录作为一个包
6 Python 中的基本数据结构
6.1 双端队列
大部分同 C++,注意模块从 collections 引用 from collections import deque
7 对象和类
7.1 定义类
__init__()
是 Python 中一个特殊的函数名,用于根据类的定义创建实例对象;self
参数指向了这个正在被创建的对象本身
关于 self 的讨论
Python 中经常争议的一点就是必须把 self
设置为实例方法的第一个参数,Python 在使用 self
参数来找到正确的对象所包含的特性和方法
就上面的代码,Python 在背后做了以下两件事情:
- 查找 someone 对象所述的类(Person)
- 把 someone 对象作为 self 参数传给 Person 类所包含的 __init__() 方法
7.2 继承
添加新方法
覆盖方法
在子类中,可以覆盖任何父类的方法,包括 __init__()
我们已经知道如何在子类中覆盖父类的方法,但如果想要调用父类的方法怎么办 ?使用 super()
7.3 使用属性对特性进行访问和设置
property()
函数的作用是在新式类中返回属性值,其语法如下所示
class property([fget[, fset[, fdel[, doc]]]])
参数 fget – 获取属性值的函数;fset – 设置属性值的函数;fdel – 删除属性值函数;doc – 属性描述信息
此时,当你尝试访问 Person 类对象的 name 特性时,get_name() 会被自动调用,而不会直接涉及 hidden_name
当然,也可以显示调用 get_name() 方法
另一种方式是将 property 函数用作装饰器可以很方便的创建只读属性
但是此时没有显示的 get_name() 和 set_name() 方法
但是这样做还是有一个问题,如果有人能猜到我们在类的内部用的特性名为 hidden_name,他仍然可以直接通过 someone.hidden_name
进行的读写操作。下一节将看到 Python 中特有的命名私有特性的方式
7.4 使用名称重整保护私有特性
Python 对那些需要可以隐藏在类内部的特性有自己的命名规范:由连续的两个下划线开头
于是,我们把 hidden_name 改为 __name 试试:
这种命名规范本质上并没有把特性变成私有,但 Python 确实将它的名字重整了,让外部的代码无法使用
既然本质上没有把特性变成私有,那么我们还是有办法可以进行直接访问,如下所示
同时要注意,我们并没有得到 calling func get_name
尽管这种保护特性的方式并不完美,但它确实能在一定程度上避免我们有意无意地对特性进行直接访问
7.5 方法的类型
有些数据(特性)和函数(方法)是类本身的一部分,还有一些是由类创建的实例的一部分
在类的定义中,以 self
作为第一参数方法的都是实例方法,它们创建自定义类时最常用。实例方法的首个参数是 self
,当它被调用时,Python 会把调用该方法的对象作为 self
参数传入
与之相对,类方法会作用于整个类,对类作出的任何改变会对它的所有实例对象产生影响。在类定义内部,用前缀修饰符 @classmethod
指定的方法都是类方法
与实例方法类似,类方法的第一个参数是类本身,在 Python 中,这个参数常被写作 cls
,因为全称 class 是保留字,在这里我们无法使用
比如下面的代码统计类有多少个实例对象
注意,我们使用的是 A.count
(类特性),而不是 self.count
(可能是对象的特性),在 kids() 方法中,我们使用的是 cls.count
,它与 A.count
的作用一样
类定义中的方法还存在第三种类型,它既不会影响类也不会影响类的对象。它们出现在类的定义中仅仅是为了方便,否则它们只能孤零零地出现在代码的其他地方,这会影响代码的逻辑性
这种类型的方法称为静态方法,用 @staticmethod
修饰,它既不需要 self 参数也不需要 cls 参数
我们甚至都不需要创建任何 A 类的对象就可以调用这个方法
7.6 鸭子类型
Python 中实现多态的要求十分宽松,这意味着我们可以对不同对象调用同名的操作,甚至不用管这些对象的类型是什么
Python 在这方面走得更远一些,无论对象的种类是什么,只要包含 who()
和 says()
,你便可以调用它
如下,我们重新定义一个类,使它也有 who()
和 says()
这种方式有时被称为鸭子类型
,我们只关注它能实现的功能,而不关注对象本身的具体类型
如果它走路像鸭子,叫起来也像鸭子,那么它就是一只鸭子
7.7 特殊方法
Python 的特殊方法的名称以双下划线开头和结束,记不记得我们已经认识一个特殊方法了,即 __init__
,它根据类的定义以及传入的参数对新创建的对象进行初始化
假如我们定义一个 word 类,添加一个 equals()
方法来比较两个单词是否一致,忽略大小写的差异
我们第一次尝试创建一个普通方法 equals()
效果还可以,但是能不能直接写成 if word1 == word2
呢?这样不会更妙哉
于是,我们进行如下的操作:
完美!!! 就和 C++ 的运算符重载一样
我们再来看两个魔术方法 __str__()
用于关于字符串格式化,__repr__()
用于交互式解释器输出变量
如果在你的类既没有定义 __str__()
也没有定义 __repr__()
,Python 会输出类似下面这样默认的字符串
我们现在把两个魔术方法加进去
至于其他的魔术方法,我们用到一个说一个
7.8 组合
如果你想要创建的子类在大多数情况下的行为都和父类相似的话(之类和父类之间是 is-a 的关系),使用继承是非常不错的选择。建立复杂的继承关系能解决很多问题,但是有些时候使用组合或聚合更加符合显示的逻辑(x 含有 y, 他们之间是 has-a 的关系)
例如,一只鸭子是鸟的一种(is-a),它有 一条尾巴(has-a),尾巴并不是鸭子的一种,它是鸭子的组成部分
7.9 使用类和使用模块
有一些方法可以帮助你决定是把代码封装到类里还是模块里
- 当你需要许多具有相似行为(方法)但不同状态(特性)的实例时,使用对象是最好的选择
- 类支持继承,但模块不支持
- 如果你想要保证实例的唯一性,使用模块是最好的选择。不管模块在程序中被引用多少次,始终只有一个实例被加载
- 如果你有一系列包含多个值的变量,并且它们能作为参数传入不同的函数,那么最好将它们封装到类里面
- 用最简单的方式解决问题。使用字典、列表和元组往往要比使用模块更加简单、简洁且快速。而使用类则更为复杂
命名元组
命名元组是元组的子类,你既可以通过名称(使用 .name
)来访问其中的值,也可以通过位置进行访问(使用 [offset]
)
我们把前面例子中的 Duck 类改成命名元组,nametuple 函数需要传入两个参数来创建命名元组:
- 名称
- 由多个域名组成的字符串,各个域名之间由空格隔开
也可以用字典来构造一个命名元组
注意,上面例子中的 **parts
,它是个关键词变量,它的作用是将 parts 字典中的键和值抽取出来作为参数提供给 Duck()
使用,等价于下面的语句
命名元组是不可变的,但是你可以替换其中某些域的值并返回一个新的命名元组
另外,我们也无法对命名元组添加新的域
作为总结,我们列出一些使用命名元组的好处:
- 它无论看起来还是使用起都和不可变对象非常相似
- 与使用对象相比,使用命名元组在空间和时间上效率更高
- 可以使用点号对特性进行访问,而不需要使用字典风格的方括号
- 可以把它作为字典的键