python
环境搭建
见《python环境》篇
基本概念
python解释器
调用解释器
传入参数
解释器读取命令行参数,把脚本名与其他参数转化为字符串列表存到 sys
模块的 argv
变量里。
交互模式
进入解释器时,首先显示欢迎信息、版本信息、版权声明,然后才是提示符:
1 | python3.11 |
解释器运行环境
源文件字符编码
默认情况下,Python 源码文件的编码是 UTF-8。
如果不使用默认编码,则要声明文件的编码,文件的 第一 行要写成特殊注释。句法如下:
1 | # -*- coding: cp1252 -*- |
第一行 的规则也有一种例外情况,源码以 UNIX “shebang” 行 开头。此时,编码声明要写在文件的第二行。例如:
1 | #!/usr/bin/env python3 |
Unix 系统中,为了不与同时安装的 Python 2.x 冲突,Python 3.x 解释器默认安装的执行文件名不是
python
。如果加了第一行,要考虑实际环境的python的默认执行命令是哪一个
命令行与环境
命令行
接口选项
- 解释器接口类似于 UNIX shell,但提供了额外的调用方法:
- 用连接到 tty 设备的标准输入调用时,会提示输入并执行命令,输入 EOF (文件结束符,UNIX 中按 Ctrl-D,Windows 中按 Ctrl-Z, Enter)时终止。
- 用文件名参数或以标准输入文件调用时,读取,并执行该脚本文件。
- 用目录名参数调用时,从该目录读取、执行适当名称的脚本。
- 用
-c command
调用时,执行 command 表示的 Python 语句。command 可以包含用换行符分隔的多条语句。注意,前导空白字符在 Python 语句中非常重要! - 用
-m module-name
调用时,在 Python 模块路径中查找指定的模块,并将其作为脚本执行。
- 非交互模式下,先解析全部输入,再执行。
环境变量
python速览
数字
进入python解释器
解释器像一个简单的计算器:输入表达式,就会给出答案。表达式的语法很直接:运算符 +
、-
、*
、/
的用法和其他大部分语言一样(比如,Pascal 或 C);括号 (()
) 用来分组。例如:
1 | 2 + 2 |
1 | 17 / 3 # classic division returns a float |
1 | 5 ** 2 # 5 squared |
等号(=
)用于给变量赋值。赋值后,下一个交互提示符的位置不显示任何结果:
1 | width = 20 |
Python 全面支持浮点数;混合类型运算数的运算会把整数转换为浮点数:
1 | 4 * 3.75 - 1 |
交互模式下,上次输出的表达式会赋给变量 _
。把 Python 当作计算器时,用该变量实现下一步计算更简单,例如:
1 | tax = 12.5 / 100 |
最好把该变量当作只读类型。不要为它显式赋值,否则会创建一个同名独立局部变量,该变量会用它的魔法行为屏蔽内置变量。
除了 int
和 float
,Python 还支持其他数字类型,例如 Decimal
或 Fraction
。Python 还内置支持 复数,后缀 j
或 J
用于表示虚数(例如 3+5j
)。
字符串
字符串定义与输出
字符串有多种表现形式,用单引号('……'
)或双引号("……"
)标注的结果相同 。反斜杠 \
用于转义:
1 | 'spam eggs' # single quotes |
交互式解释器会为输出的字符串加注引号,特殊字符使用反斜杠转义。虽然,有时输出的字符串看起来与输入的字符串不一样(外加的引号可能会改变),但两个字符串是相同的。如果字符串中有单引号而没有双引号,该字符串外将加注双引号,反之,则加注单引号。print()
函数输出的内容更简洁易读,它会省略两边的引号,并输出转义后的特殊字符:
1 | '"Isn\'t," they said.' |
避免转义
在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string,不要转意backslash ‘\’ 。 例如,\n 在raw string中,是两个字符,\和n, 而不会转意为换行符。由于正则表达式和 \ 会有冲突,因此,当一个字符串使用了正则表达式后,最好在前面加上’r’。
1 | s=r'\tt' |
多行字符串
字符串字面值可以包含多行。 一种实现方式是使用三重引号:"""..."""
或 '''...'''
。 字符串中将自动包括行结束符,但也可以在换行的地方添加一个 \
来避免此情况。 参见以下示例:
1 | print("""\ |
字符串拼接
字符串可以用 +
合并(粘到一起),也可以用 *
重复:
1 | # 3 times 'un', followed by 'ium' |
相邻的两个或多个 字符串字面值 (引号标注的字符)会自动合并:
1 | 'Py' 'thon' |
拼接分隔开的长字符串时,这个功能特别实用:
1 | text = ('Put several strings within parentheses ' |
这项功能只能用于两个字面值,不能用于变量或表达式:
1 | prefix = 'Py' |
合并多个变量,或合并变量与字面值,要用 +
:
1 | prefix + 'thon' |
字符串索引
字符串支持 索引 (下标访问),第一个字符的索引是 0。单字符没有专用的类型,就是长度为一的字符串:
1 | word = 'Python' |
索引还支持负数,用负数索引时,从右边开始计数:
1 | word[-1] # last character |
注意,-0 和 0 一样,因此,负数索引从 -1 开始。
字符串切片
索引可以提取单个字符,切片 则提取子字符串:
左闭右开,不同于下标访问,切片索引从字符中间开始计算
1 | word[0:2] # characters from position 0 (included) to 2 (excluded) |
切片索引的默认值很有用;省略开始索引时,默认值为 0,省略结束索引时,默认为到字符串的结尾:
1 | word[:2] # character from the beginning to position 2 (excluded) |
注意,输出结果包含切片开始,但不包含切片结束。因此,s[:i] + s[i:]
总是等于 s
:
1 | word[:2] + word[2:] |
还可以这样理解切片,索引指向的是字符 之间 ,第一个字符的左侧标为 0,最后一个字符的右侧标为 n ,n 是字符串长度。例如:
1 | +---+---+---+---+---+---+ |
第一行数字是字符串中索引 0…6 的位置,第二行数字是对应的负数索引位置。i 到 j 的切片由 i 和 j 之间所有对应的字符组成。
对于使用非负索引的切片,如果两个索引都不越界,切片长度就是起止索引之差。例如, word[1:3]
的长度是 2。
索引越界会报错:
1 | word[42] # the word only has 6 characters |
但是,切片会自动处理越界索引:
1 | word[4:42] |
Python 字符串不能修改,是 immutable 的。因此,为字符串中某个索引位置赋值会报错:
1 | word[0] = 'J' |
要生成不同的字符串,应新建一个字符串:
1 | 'J' + word[1:] |
内置函数 len()
返回字符串的长度:
1 | s = 'supercalifragilisticexpialidocious' |
列表
Python 支持多种 复合 数据类型,可将不同值组合在一起。最常用的 列表 ,是用方括号标注,逗号分隔的一组值。列表 可以包含不同类型的元素,但一般情况下,各个元素的类型相同:
1 | squares = [1, 4, 9, 16, 25] |
和字符串(及其他内置 sequence 类型)一样,列表也支持索引和切片:
1 | squares[0] # indexing returns the item |
切片操作返回包含请求元素的新列表。以下切片操作会返回列表的 浅拷贝:
1 | squares[:] |
列表还支持合并操作:
1 | squares + [36, 49, 64, 81, 100] |
与 immutable 字符串不同, 列表是 mutable 类型,其内容可以改变:
1 | cubes = [1, 8, 27, 65, 125] # something's wrong here |
append()
方法 可以在列表结尾添加新元素(详见后文):
1 | cubes.append(216) # add the cube of 6 |
为切片赋值可以改变列表大小,甚至清空整个列表:
1 | letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] |
内置函数 len()
也支持列表:
1 | letters = ['a', 'b', 'c', 'd'] |
还可以嵌套列表(创建包含其他列表的列表),例如:
1 | a = ['a', 'b', 'c'] |
以相同索引遍历两个列表
1 | for item1, item2 in zip(list1, list2): |
流程控制
if
if 语句包含零个或多个 elif
子句及可选的 else
子句。关键字 ‘elif
‘ 是 ‘else if’ 的缩写,适用于避免过多的缩进。if
… elif
… elif
… 序列可以当作其他语言中 switch
或 case
语句的替代品。
如果要把一个值与多个常量进行比较,或者检查特定类型或属性,match
语句更实用。函数
1 | x = int(input("Please enter an integer: ")) |
while
for
Python 的 for
语句不迭代算术递增数值(如 Pascal),或是给予用户定义迭代步骤和暂停条件的能力(如 C),而是迭代列表或字符串等任意序列,元素的迭代顺序与在序列中出现的顺序一致。 例如:
1 | # Measure some strings: |
遍历集合时修改集合的内容,会很容易生成错误的结果。因此不能直接进行循环,而是应遍历该集合的副本或创建新的集合:
1 | # Create a sample collection |
range()函数
内置函数 range()
常用于遍历数字序列,该函数可以生成算术级数:
1 | for i in range(5): |
生成的序列不包含给定的终止数值;range(10)
生成 10 个值,这是一个长度为 10 的序列,其中的元素索引都是合法的。range 可以不从 0 开始,还可以按指定幅度递增(递增幅度称为 ‘步进’,支持负数):
1 | ist(range(5, 10)) |
range()
和 len()
组合在一起,可以按索引迭代序列:
1 | a = ['Mary', 'had', 'a', 'little', 'lamb'] |
不过,大多数情况下,enumerate()
函数更便捷,详见 循环的技巧 。
如果只输出 range,会出现意想不到的结果:
1 | range(10) |
range()
返回对象的操作和列表很像,但其实这两种对象不是一回事。迭代时,该对象基于所需序列返回连续项,并没有生成真正的列表,从而节省了空间。
这种对象称为可迭代对象 iterable,函数或程序结构可通过该对象获取连续项,直到所有元素全部迭代完毕。for
语句就是这样的架构,sum()
是一种把可迭代对象作为参数的函数:
1 | sum(range(4)) # 0 + 1 + 2 + 3 |
下文将介绍更多返回可迭代对象或把可迭代对象当作参数的函数。 在 数据结构 这一章节中,我们将讨论有关 list()
的更多细节。
break、continue及else子句
break
语句和 C 中的类似,用于跳出最近的 for
或 while
循环。
循环语句支持 else
子句;for
循环中,可迭代对象中的元素全部循环完毕,或 while
循环的条件为假时,执行该子句;break
语句终止循环时,不执行该子句。 请看下面这个查找素数的循环示例:
1 | for n in range(2, 10): |
(没错,这段代码就是这么写。仔细看:else 子句属于 for 循环,不属于 if 语句。)
与 if
语句相比,循环的 else
子句更像 try
的 else
子句: try
的 else
子句在未触发异常时执行,循环的 else
子句则在未运行 break
时执行。try
语句和异常详见 异常的处理。
continue
语句也借鉴自 C 语言,表示继续执行循环的下一次迭代:
1 | for num in range(2, 10): |
pass
pass
语句不执行任何操作。语法上需要一个语句,但程序不实际执行任何动作时,可以使用该语句。例如:
1 | while True: |
pass
还可以用作函数或条件子句的占位符,让开发者聚焦更抽象的层次。此时,程序直接忽略 pass
:
1 | def initlog(*args): |
match
最简单的形式是将一个目标值与一个或多个字面值进行比较:
1 | def http_error(status): |
注意最后一个代码块:“变量名” _
被作为 通配符 并必定会匹配成功。 如果没有 case 语句匹配成功,则不会执行任何分支。
使用 |
(“ or ”)在一个模式中可以组合多个字面值:
1 | case 401 | 403 | 404: |
模式的形式类似解包赋值,并可被用于绑定变量:
1 | # point is an (x, y) tuple |
请仔细研究此代码! 第一个模式有两个字面值,可以看作是上面所示字面值模式的扩展。但接下来的两个模式结合了一个字面值和一个变量,而变量 绑定 了一个来自目标的值(point
)。第四个模式捕获了两个值,这使得它在概念上类似于解包赋值 (x, y) = point
。
函数
定义函数
下列代码创建一个可以输出限定数值内的斐波那契数列函数:
1 | def fib(n): # write Fibonacci series up to n |
定义 函数使用关键字 def
,后跟函数名与括号内的形参列表。函数语句从下一行开始,并且必须缩进。
函数内的第一条语句是字符串时,该字符串就是文档字符串,也称为 docstring,详见 文档字符串。利用文档字符串可以自动生成在线文档或打印版文档,还可以让开发者在浏览代码时直接查阅文档;Python 开发者最好养成在代码中加入文档字符串的好习惯。
函数在 执行 时使用函数局部变量符号表,所有函数变量赋值都存在局部符号表中;引用变量时,首先,在局部符号表里查找变量,然后,是外层函数局部符号表,再是全局符号表,最后是内置名称符号表。因此,尽管可以引用全局变量和外层函数的变量,但最好不要在函数内直接赋值(除非是 global
语句定义的全局变量,或 nonlocal
语句定义的外层函数变量)。
在调用函数时会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用 按值调用 来传递的(其中的 值 始终是对象的 引用 而不是对象的值)。当一个函数调用另外一个函数时,会为该调用创建一个新的局部符号表。
函数定义在当前符号表中把函数名与函数对象关联在一起。解释器把函数名指向的对象作为用户自定义函数。还可以使用其他名称指向同一个函数对象,并访问访该函数:
1 | fib |
fib
不返回值,因此,其他语言不把它当作函数,而是当作过程。事实上,没有 return
语句的函数也返回值,只不过这个值比较是 None
(是一个内置名称)。一般来说,解释器不会输出单独的返回值 None
,如需查看该值,可以使用 print()
:
1 | fib(0) |
函数定义详解
函数定义支持可变数量的参数。这里列出三种可以组合使用的形式。
默认值参数
为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如:
1 | def ask_ok(prompt, retries=4, reminder='Please try again!'): |
该函数可以用以下方式调用:
- 只给出必选实参:
ask_ok('Do you really want to quit?')
- 给出一个可选实参:
ask_ok('OK to overwrite the file?', 2)
- 给出所有实参:
ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
本例还使用了关键字 in
,用于确认序列中是否包含某个值。
默认值在 定义 作用域里的函数定义中求值,所以:
1 | i = 5 |
上例输出的是 5
。
重要警告: 默认值只计算一次。默认值为列表、字典或类实例等可变对象时,会产生与该规则不同的结果。例如,下面的函数会累积后续调用时传递的参数:
1 | def f(a, L=[]): |
输出结果如下:
1 | [1] |
不想在后续调用之间共享默认值时,应以如下方式编写函数:
1 | def f(a, L=None): |
关键字参数
kwarg=value
形式的 关键字参数 也可以用于调用函数。函数示例如下:
1 | def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'): |
该函数接受一个必选参数(voltage
)和三个可选参数(state
, action
和 type
)。该函数可用下列方式调用:
1 | parrot(1000) # 1 positional argument |
以下调用函数的方式都无效:
1 | parrot() # required argument missing |
函数调用时,关键字参数必须跟在位置参数后面。所有传递的关键字参数都必须匹配一个函数接受的参数(比如,actor
不是函数 parrot
的有效参数),关键字参数的顺序并不重要。这也包括必选参数,(比如,parrot(voltage=1000)
也有效)。不能对同一个参数多次赋值,下面就是一个因此限制而失败的例子:
1 | def function(a): |
最后一个形参为 **name
形式时,接收一个字典(详见 映射类型 — dict),该字典包含与函数中已定义形参对应之外的所有关键字参数。**name
形参可以与 *name
形参(下一小节介绍)组合使用(*name
必须在 **name
前面), *name
形参接收一个 元组,该元组包含形参列表之外的位置参数。例如,可以定义下面这样的函数:
1 | def cheeseshop(kind, *arguments, **keywords): |
该函数可以用如下方式调用:
1 | cheeseshop("Limburger", "It's very runny, sir.", |
输出结果如下:
1 | -- Do you have any Limburger ? |
1 | def cheeseshop(kind, *arguments, **keywords): |
注意,关键字参数在输出结果中的顺序与调用函数时的顺序一致。
特殊参数
默认情况下,参数可以按位置或显式关键字传递给 Python 函数。为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。
函数定义如下:
1 | def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): |
/
和 *
是可选的。这些符号表明形参如何把参数值传递给函数:位置、位置或关键字、关键字。关键字形参也叫作命名形参。
位置或关键字参数
函数定义中未使用 /
和 *
时,参数可以按位置或关键字传递给函数。
仅位置参数
此处再介绍一些细节,特定形参可以标记为 仅限位置。仅限位置 时,形参的顺序很重要,且这些形参不能用关键字传递。仅限位置形参应放在 /
(正斜杠)前。/
用于在逻辑上分割仅限位置形参与其它形参。如果函数定义中没有 /
,则表示没有仅限位置形参。
/
后可以是 位置或关键字 或 仅限关键字 形参。
仅限关键字参数
把形参标记为 仅限关键字,表明必须以关键字参数形式传递该形参,应在参数列表中第一个 仅限关键字 形参前添加 *
。
函数示例
请看下面的函数定义示例,注意 /
和 *
标记:
1 | def standard_arg(arg): |
下面的函数定义中,kwds
把 name
当作键,因此,可能与位置参数 name
产生潜在冲突:
1 | def foo(name, **kwds): |
调用该函数不可能返回 True
,因为关键字 'name'
总与第一个形参绑定。例如:
1 | foo(1, **{'name': 2}) |
加上 /
(仅限位置参数)后,就可以了。此时,函数定义把 name
当作位置参数,'name'
也可以作为关键字参数的键:
1 | def foo(name, /, **kwds): |
换句话说,仅限位置形参的名称可以在 **kwds
中使用,而不产生歧义。
小结
以下用例决定哪些形参可以用于函数定义:
1 | def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): |
说明:
- 使用仅限位置形参,可以让用户无法使用形参名。形参名没有实际意义时,强制调用函数的实参顺序时,或同时接收位置形参和关键字时,这种方式很有用。
- 当形参名有实际意义,且显式名称可以让函数定义更易理解时,阻止用户依赖传递实参的位置时,才使用关键字。
- 对于 API,使用仅限位置形参,可以防止未来修改形参名时造成破坏性的 API 变动。
任意实参列表
调用函数时,使用任意数量的实参是最少见的选项。这些实参包含在元组中(详见 元组和序列 )。在可变数量的实参之前,可能有若干个普通参数:
1 | def write_multiple_items(file, separator, *args): |
variadic 参数用于采集传递给函数的所有剩余参数,因此,它们通常在形参列表的末尾。*args
形参后的任何形式参数只能是仅限关键字参数,即只能用作关键字参数,不能用作位置参数:
1 | def concat(*args, sep="/"): |
解包实参列表
函数调用要求独立的位置参数,但实参在列表或元组里时,要执行相反的操作。例如,内置的 range()
函数要求独立的 start 和 stop 实参。如果这些参数不是独立的,则要在调用函数时,用 *
操作符把实参从列表或元组解包出来:
1 | list(range(3, 6)) # normal call with separate arguments |
同样,字典可以用 **
操作符传递关键字参数:
1 | def parrot(voltage, state='a stiff', action='voom'): |
1 | def show(name, age): |
Lambda 表达式
lambda
关键字用于创建小巧的匿名函数。lambda a, b: a+b
函数返回两个参数的和。Lambda 函数可用于任何需要函数对象的地方。在语法上,匿名函数只能是单个表达式。在语义上,它只是常规函数定义的语法糖。与嵌套函数定义一样,lambda 函数可以引用包含作用域中的变量:
1 | def make_incrementor(n): |
上例用 lambda 表达式返回函数。还可以把匿名函数用作传递的实参:
1 | pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] |
文档字符串
以下是文档字符串内容和格式的约定。
第一行应为对象用途的简短摘要。为保持简洁,不要在这里显式说明对象名或类型,因为可通过其他方式获取这些信息(除非该名称碰巧是描述函数操作的动词)。这一行应以大写字母开头,以句点结尾。
文档字符串为多行时,第二行应为空白行,在视觉上将摘要与其余描述分开。后面的行可包含若干段落,描述对象的调用约定、副作用等。
Python 解析器不会删除 Python 中多行字符串字面值的缩进,因此,文档处理工具应在必要时删除缩进。这项操作遵循以下约定:文档字符串第一行 之后 的第一个非空行决定了整个文档字符串的缩进量(第一行通常与字符串开头的引号相邻,其缩进在字符串中并不明显,因此,不能用第一行的缩进),然后,删除字符串中所有行开头处与此缩进“等价”的空白符。不能有比此缩进更少的行,但如果出现了缩进更少的行,应删除这些行的所有前导空白符。转化制表符后(通常为 8 个空格),应测试空白符的等效性。
下面是多行文档字符串的一个例子:
1 | def my_function(): |
函数注解
函数注解 是可选的用户自定义函数类型的元数据完整信息
标注 以字典的形式存放在函数的 __annotations__
属性中,并且不会影响函数的任何其他部分。 形参标注的定义方式是在形参名后加冒号,后面跟一个表达式,该表达式会被求值为标注的值。 返回值标注的定义方式是加组合符号 ->
,后面跟一个表达式,该标注位于形参列表和表示 def
语句结束的冒号之间。 下面的示例有一个必须的参数,一个可选的关键字参数以及返回值都带有相应的标注:
1 | def f(ham: str, eggs: str = 'eggs') -> str: |
编码风格
现在你将要写更长,更复杂的 Python 代码,是时候讨论一下 代码风格 了。 大多数语言都能以不同的风格被编写(或更准确地说,被格式化);有些比其他的更具有可读性。 能让其他人轻松阅读你的代码总是一个好主意,采用一种好的编码风格对此有很大帮助。
Python 项目大多都遵循 PEP 8 的风格指南;它推行的编码风格易于阅读、赏心悦目。Python 开发者均应抽时间悉心研读;以下是该提案中的核心要点:
缩进,用 4 个空格,不要用制表符。
4 个空格是小缩进(更深嵌套)和大缩进(更易阅读)之间的折中方案。制表符会引起混乱,最好别用。
换行,一行不超过 79 个字符。
这样换行的小屏阅读体验更好,还便于在大屏显示器上并排阅读多个代码文件。
用空行分隔函数和类,及函数内较大的代码块。
最好把注释放到单独一行。
使用文档字符串。
运算符前后、逗号后要用空格,但不要直接在括号内使用:
a = f(1, 2) + g(3, 4)
。类和函数的命名要一致;按惯例,命名类用
UpperCamelCase
,命名函数与方法用lowercase_with_underscores
。命名方法中第一个参数总是用self
(类和方法详见 初探类)。编写用于国际多语环境的代码时,不要用生僻的编码。Python 默认的 UTF-8 或纯 ASCII 可以胜任各种情况。
同理,就算多语阅读、维护代码的可能再小,也不要在标识符中使用非 ASCII 字符。
lambda
表达式
1、在python中,函数是一个被命名的、独立完成特定功能的一段代码,并可能给调用它的程序一个返回值。
①普通函数:有名函数
②匿名函数:为简化程序代码,可定义匿名函数
2、lambda表达式的应用场景:若函数有一个返回值,并且只有一行简单的代码,可使用lambda简化
3、lambda表达式的基本语法
1 | 变量 = lambda 函数参数:表达式(函数代码+return返回值) |
4、编写lambda表达式
(1)定义一个函数,经过一系列操作,返回100(无参数):
1 | def f1(): |
lambda简化:
1 | f2=lambda:100 |
(2)求两数之和(有参数):
1 | def f1(num1,num2): |
lambda简化:
1 | f2 = lambda num1,num2 : num1 + num2 |
6、lambda表达式相关应用
①带默认值的:
1 | f = lambda a,b,c = 100: a + b + c #c为默认值 |
②可变参数args(不定长参数):
1 | f1 = lambda *args : args |
③关键字参数**kwargs
:
1 | f2 = lambda **kwargs : kwargs#返回的是字典 |
④带if判断的lambda表达式(求两数的最大值):
1 | f = lambda a,b:a if a > b else b |
⑤列表数据+字典数据排序
1 | students = [ |
数据结构
列表
列表方法
list.append(x)
- 在列表末尾添加一个元素,相当于
a[len(a):] = [x]
。
list.extend(iterable)
- 用可迭代对象的元素扩展列表。相当于
a[len(a):] = iterable
。
list.insert(i, x)
- 在指定位置插入元素。第一个参数是插入元素的索引,因此,
a.insert(0, x)
在列表开头插入元素,a.insert(len(a), x)
等同于a.append(x)
。
list.remove(x)
- 从列表中删除第一个值为 x 的元素。未找到指定元素时,触发
ValueError
异常。
list.pop([i])
- 删除列表中指定位置的元素,并返回被删除的元素。未指定位置时,
a.pop()
删除并返回列表的最后一个元素。(方法签名中 i 两边的方括号表示该参数是可选的,不是要求输入方括号。这种表示法常见于 Python 参考库)。
list.clear()
- 删除列表里的所有元素,相当于
del a[:]
。
list.index(x[, start[, end]])
返回列表中第一个值为 x 的元素的零基索引。未找到指定元素时,触发
ValueError
异常。可选参数 start 和 end 是切片符号,用于将搜索限制为列表的特定子序列。返回的索引是相对于整个序列的开始计算的,而不是 start 参数。
list.count(x)
- 返回列表中元素 x 出现的次数。
list.sort(*, key=None, reverse=False)
- 就地排序列表中的元素(要了解自定义排序参数,详见
sorted()
)。
list.reverse()
- 翻转列表中的元素。
list.copy()
- 返回列表的浅拷贝。相当于
a[:]
。
样例:
1 | fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana'] |
用列表实现堆栈
使用列表方法实现堆栈非常容易,最后插入的最先取出(“后进先出”)。把元素添加到堆栈的顶端,使用 append()
。从堆栈顶部取出元素,使用 pop()
,不用指定索引。例如:
1 | stack = [3, 4, 5] |
用列表实现队列
列表也可以用作队列,最先加入的元素,最先取出(“先进先出”);然而,列表作为队列的效率很低。因为,在列表末尾添加和删除元素非常快,但在列表开头插入或移除元素却很慢(因为所有其他元素都必须移动一位)。
实现队列最好用 collections.deque
,可以快速从两端添加或删除元素。例如:
1 | from collections import deque |
列表推导式
列表推导式创建列表的方式更简洁。常见的用法为,对序列或可迭代对象中的每个元素应用某种操作,用生成的结果创建新的列表;或用满足特定条件的元素创建子序列。
例如,创建平方值的列表:
1 | squares = [] |
注意,这段代码创建(或覆盖)变量 x
,该变量在循环结束后仍然存在。下述方法可以无副作用地计算平方列表:
1 | squares = list(map(lambda x: x**2, range(10))) |
或等价于:
1 | squares = [x**2 for x in range(10)] |
上面这种写法更简洁、易读。
列表推导式的方括号内包含以下内容:一个表达式,后面为一个 for
子句,然后,是零个或多个 for
或 if
子句。结果是由表达式依据 for
和 if
子句求值计算而得出一个新列表。 举例来说,以下列表推导式将两个列表中不相等的元素组合起来:
1 | [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y] |
等价于:
1 | combs = [] |
表达式是元组(例如上例的 (x, y)
)时,必须加上括号:
1 | vec = [-4, -2, 0, 2, 4] |
列表推导式可以使用复杂的表达式和嵌套函数:
1 | from math import pi |
嵌套的列表推导式
列表推导式中的初始表达式可以是任何表达式,甚至可以是另一个列表推导式。
下面这个 3x4 矩阵,由 3 个长度为 4 的列表组成:
1 | matrix = [ |
下面的列表推导式可以转置行列:
1 | [[row[i] for row in matrix] for i in range(4)] |
等价于:
1 | transposed = [] |
反过来说,也等价于:
1 | transposed = [] |
实际应用中,最好用内置函数替代复杂的流程语句。此时,zip()
函数更好用:
1 | list(zip(*matrix)) |
从列表中取n个元素
有时,我们要从列表中取n个元素,我们通常可以采用以下几种方法:
random.sample()
1
2
3import random
lst=["张三","李四","王五","赵六","麻七","侯八"]
print(random.sample(lst,3))列表推导式
1
2
3import random
lst=["张三","李四","王五","赵六","麻七","侯八"]
print([random.choice(lst) for i in range(3)])iter()迭代函数实现按顺序取
1
2lst=["张三","李四","王五","赵六","麻七","侯八"]
print([(x,y,z) for x,y,z in zip(*[iter(lst)]*3)]) #这里如果按顺序取4个元素就把里面的3换成4
在列表中插入另一个列表中的元素
1 | lst1 = [1,2,3] |
打乱列表原顺序
1 | import random |
del语句
del
语句按索引,而不是值从列表中移除元素。与返回值的 pop()
方法不同, del
语句也可以从列表中移除切片,或清空整个列表(之前是将空列表赋值给切片)。 例如:
1 | a = [-1, 1, 66.25, 333, 333, 1234.5] |
del
也可以用来删除整个变量:
1 | del a |
此后,再引用 a
就会报错(直到为它赋与另一个值)。后文会介绍 del
的其他用法。
元组和序列
列表和字符串有很多共性,例如,索引和切片操作。这两种数据类型是 序列 (参见 序列类型 — list, tuple, range)。随着 Python 语言的发展,其他的序列类型也被加入其中。本节介绍另一种标准序列类型:元组。
元组由多个用逗号隔开的值组成,例如:
1 | t = 12345, 54321, 'hello!' |
输出时,元组都要由圆括号标注,这样才能正确地解释嵌套元组。输入时,圆括号可有可无,不过经常是必须的(如果元组是更大的表达式的一部分)。不允许为元组中的单个元素赋值,当然,可以创建含列表等可变对象的元组。
虽然,元组与列表很像,但使用场景不同,用途也不同。元组是 immutable (不可变的),一般可包含异质元素序列,通过解包(见本节下文)或索引访问(如果是 namedtuples
,可以属性访问)。列表是 mutable (可变的),列表元素一般为同质类型,可迭代访问。
构造 0 个或 1 个元素的元组比较特殊:为了适应这种情况,对句法有一些额外的改变。用一对空圆括号就可以创建空元组;只有一个元素的元组可以通过在这个元素后添加逗号来构建(圆括号里只有一个值的话不够明确)。丑陋,但是有效。例如:
1 | empty = () |
语句 t = 12345, 54321, 'hello!'
是 元组打包 的例子:值 12345
, 54321
和 'hello!'
一起被打包进元组。逆操作也可以:
1 | x, y, z = t |
称之为 序列解包 也是妥妥的,适用于右侧的任何序列。序列解包时,左侧变量与右侧序列元素的数量应相等。注意,多重赋值其实只是元组打包和序列解包的组合。
集合
Python 还支持 集合 这种数据类型。集合是由不重复元素组成的无序容器。基本用法包括成员检测、消除重复元素。集合对象支持合集、交集、差集、对称差分等数学运算。
创建集合用花括号或 set()
函数。注意,创建空集合只能用 set()
,不能用 {}
,{}
创建的是空字典,下一小节介绍数据结构:字典。
以下是一些简单的示例
1 | basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'} |
与 列表推导式 类似,集合也支持推导式:
1 | a = {x for x in 'abracadabra' if x not in 'abc'} |
字典
字典 (参见 映射类型 — dict) 也是一种常用的 Python 內置数据类型。其他语言可能把字典称为 联合内存 或 联合数组。与以连续整数为索引的序列不同,字典以 关键字 为索引,关键字通常是字符串或数字,也可以是其他任意不可变类型。只包含字符串、数字、元组的元组,也可以用作关键字。但如果元组直接或间接地包含了可变对象,就不能用作关键字。列表不能当关键字,因为列表可以用索引、切片、append()
、extend()
等方法修改。
可以把字典理解为 键值对 的集合,但字典的键必须是唯一的。花括号 {}
用于创建空字典。另一种初始化字典的方式是,在花括号里输入逗号分隔的键值对,这也是字典的输出方式。
字典的主要用途是通过关键字存储、提取值。用 del
可以删除键值对。用已存在的关键字存储值,与该关键字关联的旧值会被取代。通过不存在的键提取值,则会报错。
对字典执行 list(d)
操作,返回该字典中所有键的列表,按插入次序排列(如需排序,请使用 sorted(d)
)。检查字典里是否存在某个键,使用关键字 in
。
1 | tel = {'jack': 4098, 'sape': 4139} |
dict()
构造函数可以直接用键值对序列创建字典:
1 | dict([('sape', 4139), ('guido', 4127), ('jack', 4098)]) |
字典推导式可以用任意键值表达式创建字典:
1 | {x: x**2 for x in (2, 4, 6)} |
关键字是比较简单的字符串时,直接用关键字参数指定键值对更便捷:
1 | dict(sape=4139, guido=4127, jack=4098) |
循环的技巧
在字典中循环时,用 items()
方法可同时取出键和对应的值:
1 | knights = {'gallahad': 'the pure', 'robin': 'the brave'} |
在序列中循环时,用 enumerate()
函数可以同时取出位置索引和对应的值:
1 | for i, v in enumerate(['tic', 'tac', 'toe']): |
同时循环两个或多个序列时,用 zip()
函数可以将其内的元素一一匹配:
1 | questions = ['name', 'quest', 'favorite color'] |
逆向循环序列时,先正向定位序列,然后调用 reversed()
函数:
1 | for i in reversed(range(1, 10, 2)): |
按指定顺序循环序列,可以用 sorted()
函数,在不改动原序列的基础上,返回一个重新的序列:
1 | basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] |
使用 set()
去除序列中的重复元素。使用 sorted()
加 set()
则按排序后的顺序,循环遍历序列中的唯一元素:
1 | basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] |
一般来说,在循环中修改列表的内容时,创建新列表比较简单,且安全:
1 | import math |
深入条件控制
while
和 if
条件句不只可以进行比较,还可以使用任意运算符。
比较运算符 in
和 not in
用于执行确定一个值是否存在(或不存在)于某个容器中的成员检测。 运算符 is
和 is not
用于比较两个对象是否是同一个对象。 所有比较运算符的优先级都一样,且低于任何数值运算符。
比较操作支持链式操作。例如,a < b == c
校验 a
是否小于 b
,且 b
是否等于 c
。
比较操作可以用布尔运算符 and
和 or
组合,并且,比较操作(或其他布尔运算)的结果都可以用 not
取反。这些操作符的优先级低于比较操作符;not
的优先级最高, or
的优先级最低,因此,A and not B or C
等价于 (A and (not B)) or C
。与其他运算符操作一样,此处也可以用圆括号表示想要的组合。
布尔运算符 and
和 or
也称为 短路 运算符:其参数从左至右解析,一旦可以确定结果,解析就会停止。例如,如果 A
和 C
为真,B
为假,那么 A and B and C
不会解析 C
。用作普通值而不是布尔值时,短路操作符返回的值通常是最后一个变量。
还可以把比较操作或逻辑表达式的结果赋值给变量,例如:
1 | string1, string2, string3 = '', 'Trondheim', 'Hammer Dance' |
注意,Python 与 C 不同,在表达式内部赋值必须显式使用 海象运算符 :=
。 这避免了 C 程序中常见的问题:要在表达式中写 ==
时,却写成了 =
。
序列和其他类型的比较
序列对象可以与相同序列类型的其他对象比较。这种比较使用 字典式 顺序:首先,比较前两个对应元素,如果不相等,则可确定比较结果;如果相等,则比较之后的两个元素,以此类推,直到其中一个序列结束。如果要比较的两个元素本身是相同类型的序列,则递归地执行字典式顺序比较。如果两个序列中所有的对应元素都相等,则两个序列相等。如果一个序列是另一个的初始子序列,则较短的序列可被视为较小(较少)的序列。 对于字符串来说,字典式顺序使用 Unicode 码位序号排序单个字符。下面列出了一些比较相同类型序列的例子:
1 | (1, 2, 3) < (1, 2, 4) |
注意,对不同类型的对象来说,只要待比较的对象提供了合适的比较方法,就可以使用 <
和 >
进行比较。例如,混合数值类型通过数值进行比较,所以,0 等于 0.0,等等。否则,解释器不会随便给出一个对比结果,而是触发 TypeError
异常。
模块与包
退出 Python 解释器后,再次进入时,之前在 Python 解释器中定义的函数和变量就丢失了。因此,编写较长程序时,建议用文本编辑器代替解释器,执行文件中的输入内容,这就是编写 脚本 。随着程序越来越长,为了方便维护,最好把脚本拆分成多个文件。编写脚本还一个好处,不同程序调用同一个函数时,不用每次把函数复制到各个程序。
为实现这些需求,Python 把各种定义存入一个文件,在脚本或解释器的交互式实例中使用。这个文件就是 模块 ;模块中的定义可以 导入 到其他模块或 主 模块(在顶层和计算器模式下,执行脚本中可访问的变量集)。
模块是包含 Python 定义和语句的文件。其文件名是模块名加后缀名 .py
。在模块内部,通过全局变量 __name__
可以获取模块名(即字符串)。例如,用文本编辑器在当前目录下创建 fibo.py
文件,输入以下内容:
1 | # Fibonacci numbers module |
现在,进入 Python 解释器,用以下命令导入该模块:
1 | import fibo |
This does not add the names of the functions defined in fibo
directly to the current namespace (see Python 作用域和命名空间 for more details); it only adds the module name fibo
there. Using the module name you can access the functions:
1 | fibo.fib(1000) |
如果经常使用某个函数,可以把它赋值给局部变量:
1 | fib = fibo.fib |
模块详解
模块包含可执行语句及函数定义。这些语句用于初始化模块,且仅在 import 语句 第一次 遇到模块名时执行。 (文件作为脚本运行时,也会执行这些语句。)
Each module has its own private namespace, which is used as the global namespace by all functions defined in the module. Thus, the author of a module can use global variables in the module without worrying about accidental clashes with a user’s global variables. On the other hand, if you know what you are doing you can touch a module’s global variables with the same notation used to refer to its functions, modname.itemname
.
Modules can import other modules. It is customary but not required to place all import
statements at the beginning of a module (or script, for that matter). The imported module names, if placed at the top level of a module (outside any functions or classes), are added to the module’s global namespace.
There is a variant of the import
statement that imports names from a module directly into the importing module’s namespace. For example:
1 | from fibo import fib, fib2 |
This does not introduce the module name from which the imports are taken in the local namespace (so in the example, fibo
is not defined).
还有一种变体可以导入模块内定义的所有名称:
1 | from fibo import * |
这种方式会导入所有不以下划线(_
)开头的名称。大多数情况下,不要用这个功能,这种方式向解释器导入了一批未知的名称,可能会覆盖已经定义的名称。
注意,一般情况下,不建议从模块或包内导入 *
, 因为,这项操作经常让代码变得难以理解。不过,为了在交互式编译器中少打几个字,这么用也没问题。
模块名后使用 as
时,直接把 as
后的名称与导入模块绑定。
1 | import fibo as fib |
与 import fibo
一样,这种方式也可以有效地导入模块,唯一的区别是,导入的名称是 fib
。
from
中也可以使用这种方式,效果类似:
1 | from fibo import fib as fibonacci |
备注:为了保证运行效率,每次解释器会话只导入一次模块。如果更改了模块内容,必须重启解释器;仅交互测试一个模块时,也可以使用
importlib.reload()
,例如import importlib; importlib.reload(modulename)
。
以脚本方式执行模块
可以用以下方式运行 Python 模块:
1 | python fibo.py <arguments> |
这项操作将执行模块里的代码,和导入模块一样,但会把 __name__
赋值为 "__main__"
。 也就是把下列代码添加到模块末尾:
1 | if __name__ == "__main__": |
既可以把这个文件当脚本使用,也可以用作导入的模块, 因为,解析命令行的代码只有在模块以 “main” 文件执行时才会运行:
1 | python fibo.py 50 |
导入模块时,不运行这些代码:
1 | import fibo |
这种操作常用于为模块提供便捷用户接口,或用于测试(把模块当作执行测试套件的脚本运行)。
模块搜索路径
当一个名为 spam
的模块被导入时,解释器首先搜索具有该名称的内置模块。这些模块的名字被列在 sys.builtin_module_names
中。如果没有找到,它就在变量 sys.path
给出的目录列表中搜索一个名为 spam.py
的文件, sys.path
从这些位置初始化:
- 输入脚本的目录(或未指定文件时的当前目录)。
PYTHONPATH
(目录列表,与 shell 变量PATH
的语法一样)。- 依赖于安装的默认值(按照惯例包括一个
site-packages
目录,由site
模块处理)。
More details are at The initialization of the sys.path module search path.
备注
在支持 symlink 的文件系统中,输入脚本目录是在追加 symlink 后计算出来的。换句话说,包含 symlink 的目录并 没有 添加至模块搜索路径。
初始化后,Python 程序可以更改 sys.path
。运行脚本的目录在标准库路径之前,置于搜索路径的开头。即,加载的是该目录里的脚本,而不是标准库的同名模块。 除非刻意替换,否则会报错。详见 标准模块。
“已编译的” Python 文件
为了快速加载模块,Python 把模块的编译版缓存在 __pycache__
目录中,文件名为 module.*version*.pyc
,version 对编译文件格式进行编码,一般是 Python 的版本号。例如,CPython 的 3.3 发行版中,spam.py 的编译版本缓存为 __pycache__/spam.cpython-33.pyc
。使用这种命名惯例,可以让不同 Python 发行版及不同版本的已编译模块共存。
Python 对比编译版本与源码的修改日期,查看它是否已过期,是否要重新编译,此过程完全自动化。此外,编译模块与平台无关,因此,可在不同架构系统之间共享相同的支持库。
Python 在两种情况下不检查缓存。其一,从命令行直接载入模块,只重新编译,不存储编译结果;其二,没有源模块,就不会检查缓存。为了支持无源文件(仅编译)发行版本, 编译模块必须在源目录下,并且绝不能有源模块。
专业人士的一些小建议:
- 在 Python 命令中使用
-O
或-OO
开关,可以减小编译模块的大小。-O
去除断言语句,-OO
去除断言语句和 doc 字符串。有些程序可能依赖于这些内容,因此,没有十足的把握,不要使用这两个选项。“优化过的”模块带有opt-
标签,并且文件通常会一小些。将来的发行版或许会改进优化的效果。 - 从
.pyc
文件读取的程序不比从.py
读取的执行速度快,.pyc
文件只是加载速度更快。 compileall
模块可以为一个目录下的所有模块创建 .pyc 文件。- 本过程的细节及决策流程图,详见 PEP 3147。
标准模块
Python 自带一个标准模块的库,它在 Python 库参考(此处以下称为”库参考” )里另外描述。 一些模块是内嵌到编译器里面的, 它们给一些虽并非语言核心但却内嵌的操作提供接口,要么是为了效率,要么是给操作系统基础操作例如系统调入提供接口。 这些模块集是一个配置选项, 并且还依赖于底层的操作系统。 例如,winreg
模块只在 Windows 系统上提供。一个特别值得注意的模块 sys
,它被内嵌到每一个 Python 编译器中。sys.ps1
和 sys.ps2
变量定义了一些字符,它们可以用作主提示符和辅助提示符:
1 | import sys |
只有解释器用于交互模式时,才定义这两个变量。
变量 sys.path
是字符串列表,用于确定解释器的模块搜索路径。该变量以环境变量 PYTHONPATH
提取的默认路径进行初始化,如未设置 PYTHONPATH
,则使用内置的默认路径。可以用标准列表操作修改该变量:
1 | import sys |
dir() 函数
内置函数 dir()
用于查找模块定义的名称。返回结果是经过排序的字符串列表:
1 | import fibo, sys |
没有参数时,dir()
列出当前定义的名称:
1 | a = [1, 2, 3, 4, 5] |
注意,该函数列出所有类型的名称:变量、模块、函数等。
dir()
不会列出内置函数和变量的名称。这些内容的定义在标准模块 builtins
里:
1 | import builtins |
包
包是一种用“点式模块名”构造 Python 模块命名空间的方法。例如,模块名 A.B
表示包 A
中名为 B
的子模块。正如模块可以区分不同模块之间的全局变量名称一样,点式模块名可以区分 NumPy 或 Pillow 等不同多模块包之间的模块名称。
假设要为统一处理声音文件与声音数据设计一个模块集(“包”)。声音文件的格式很多(通常以扩展名来识别,例如:.wav
, .aiff
, .au
),因此,为了不同文件格式之间的转换,需要创建和维护一个不断增长的模块集合。为了实现对声音数据的不同处理(例如,混声、添加回声、均衡器功能、创造人工立体声效果),还要编写无穷无尽的模块流。下面这个分级文件树展示了这个包的架构:
1 | sound/ Top-level package |
导入包时,Python 搜索 sys.path
里的目录,查找包的子目录。
The __init__.py
files are required to make Python treat directories containing the file as packages. This prevents directories with a common name, such as string
, from unintentionally hiding valid modules that occur later on the module search path. In the simplest case, __init__.py
can just be an empty file, but it can also execute initialization code for the package or set the __all__
variable, described later.
还可以从包中导入单个模块,例如:
1 | import sound.effects.echo |
这段代码加载子模块 sound.effects.echo
,但引用时必须使用子模块的全名:
1 | sound.effects.echo.echofilter(input, output, delay=0.7, atten=4) |
另一种导入子模块的方法是 :
1 | from sound.effects import echo |
Import 语句的另一种变体是直接导入所需的函数或变量:
1 | from sound.effects.echo import echofilter |
同样,这样也会加载子模块 echo
,但可以直接使用函数 echofilter()
:
1 | echofilter(input, output, delay=0.7, atten=4) |
注意,使用 from package import item
时,item 可以是包的子模块(或子包),也可以是包中定义的函数、类或变量等其他名称。import
语句首先测试包中是否定义了 item;如果未在包中定义,则假定 item 是模块,并尝试加载。如果找不到 item,则触发 ImportError
异常。
相反,使用 import item.subitem.subsubitem
句法时,除最后一项外,每个 item 都必须是包;最后一项可以是模块或包,但不能是上一项中定义的类、函数或变量。
从包中导入 *
使用 from sound.effects import *
时会发生什么?理想情况下,该语句在文件系统查找并导入包的所有子模块。这项操作花费的时间较长,并且导入子模块可能会产生不必要的副作用,这种副作用只有在显式导入子模块时才会发生。
唯一的解决方案是提供包的显式索引。import
语句使用如下惯例:如果包的 __init__.py
代码定义了列表 __all__
,运行 from package import *
时,它就是用于导入的模块名列表。发布包的新版本时,包的作者应更新此列表。如果包的作者认为没有必要在包中执行导入 * 操作,也可以不提供此列表。例如,sound/effects/__init__.py
文件包含以下代码:
1 | __all__ = ["echo", "surround", "reverse"] |
这将意味着将 from sound.effects import *
导入 sound.effects
包的三个命名的子模块。
如果没有定义 __all__
,from sound.effects import *
语句 不会 把包 sound.effects
中所有子模块都导入到当前命名空间;该语句只确保导入包 sound.effects
(可能还会运行 __init__.py
中的初始化代码),然后,再导入包中定义的名称。这些名称包括 __init__.py
中定义的任何名称(以及显式加载的子模块),还包括之前 import
语句显式加载的包里的子模块。请看以下代码:
1 | import sound.effects.echo |
子包参考
包中含有多个子包时(与示例中的 sound
包一样),可以使用绝对导入引用兄弟包中的子模块。例如,要在模块 sound.filters.vocoder
中使用 sound.effects
包的 echo
模块时,可以用 from sound.effects import echo
导入。
还可以用 import 语句的 from module import name
形式执行相对导入。这些导入语句使用前导句点表示相对导入中的当前包和父包。例如,相对于 surround
模块,可以使用:
1 | from . import echo |
注意,相对导入基于当前模块名。因为主模块名是 "__main__"
,所以 Python 程序的主模块必须始终使用绝对导入。
多目录中的包
包支持一个更特殊的属性 __path__
。在包的 :file:init.py 文件中的代码被执行前,该属性被初始化为包含 :file:init.py 文件所在的目录名在内的列表。可以修改此变量;但这样做会影响在此包中搜索子模块和子包。
这个功能虽然不常用,但可用于扩展包中的模块集。
导入自定义包
python导入自定义包 - 铭烟 - 博客园 (cnblogs.com)
通过sys
模块导入自定义模块的path(处于包中的模块导入不在包中的模块也可以采用这种方法)
先导入
sys
模块然后通过
sys.path.append(path)
函数来导入自定义模块所在的目录导入自定义模块。
1
2
3
4
5
6
7
8
9
10
11
import requests
import sys
utils_path = r"/app/code/project-workshop/code-prac/Spider/spider_utils"
sys.path.append(utils_path)
from proxy import get_proxies
if __name__ == '__main__':
get_proxies()
输入与输出
程序输出有几种显示方式;数据既可以输出供人阅读的形式,也可以写入文件备用。本章探讨一些可用的方式。
我们已学习了两种写入值的方法:表达式语句 和 print()
函数。第三种方法是使用文件对象的 write()
方法;标准输出文件称为 sys.stdout
。详见标准库参考。
对输出格式的控制不只是打印空格分隔的值,还需要更多方式。格式化输出包括以下几种方法。
使用 格式化字符串字面值 ,要在字符串开头的引号/三引号前添加
f
或F
。在这种字符串中,可以在{
和}
字符之间输入引用的变量,或字面值的 Python 表达式。1
2
3
4year = 2016
event = 'Referendum'
f'Results of the {year} {event}'
'Results of the 2016 Referendum'字符串的
str.format()
方法需要更多手动操作。该方法也用{
和}
标记替换变量的位置,虽然这种方法支持详细的格式化指令,但需要提供格式化信息。1
2
3
4
5yes_votes = 42_572_654
no_votes = 43_132_495
percentage = yes_votes / (yes_votes + no_votes)
'{:-9} YES votes {:2.2%}'.format(yes_votes, percentage)
' 42572654 YES votes 49.67%'最后,还可以用字符串切片和合并操作完成字符串处理操作,创建任何排版布局。字符串类型还支持将字符串按给定列宽进行填充,这些方法也很有用。
如果不需要花哨的输出,只想快速显示变量进行调试,可以用 repr()
或 str()
函数把值转化为字符串。
str()
函数返回供人阅读的值,repr()
则生成适于解释器读取的值(如果没有等效的语法,则强制执行 SyntaxError
)。对于没有支持供人阅读展示结果的对象, str()
返回与 repr()
相同的值。一般情况下,数字、列表或字典等结构的值,使用这两个函数输出的表现形式是一样的。字符串有两种不同的表现形式。
示例如下:
1 | s = 'Hello, world.' |
string
模块包含 Template
类,提供了将值替换为字符串的另一种方法。该类使用 $x
占位符,并用字典的值进行替换,但对格式控制的支持比较有限。
格式化字符串字面值
格式化字符串字面值 (简称为 f-字符串)在字符串前加前缀 f
或 F
,通过 {expression}
表达式,把 Python 表达式的值添加到字符串内。
格式说明符是可选的,写在表达式后面,可以更好地控制格式化值的方式。下例将 pi 舍入到小数点后三位:
1 | import math |
在 ':'
后传递整数,为该字段设置最小字符宽度,常用于列对齐:
1 | table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678} |
还有一些修饰符可以在格式化前转换值。 '!a'
应用 ascii()
,'!s'
应用 str()
,'!r'
应用 repr()
:
1 | animals = 'eels' |
The =
specifier can be used to expand an expression to the text of the expression, an equal sign, then the representation of the evaluated expression:
See self-documenting expressions for more infor
1 | bugs = 'roaches' |
mation on the =
specifier. For a reference on these format specifications, see the reference guide for the 格式规格迷你语言.
字符串 format() 方法
str.format()
方法的基本用法如下所示:
1 | print('We are the {} who say "{}!"'.format('knights', 'Ni')) |
花括号及之内的字符(称为格式字段)被替换为传递给 str.format()
方法的对象。花括号中的数字表示传递给 str.format()
方法的对象所在的位置。
1 | print('{0} and {1}'.format('spam', 'eggs')) |
str.format()
方法中使用关键字参数名引用值
1 | print('This {food} is {adjective}.'.format( |
位置参数和关键字参数可以任意组合:
1 | print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred', |
如果不想分拆较长的格式字符串,最好按名称引用变量进行格式化,不要按位置。这项操作可以通过传递字典,并用方括号 '[]'
访问键来完成。
1 | table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678} |
This could also be done by passing the table
dictionary as keyword arguments with the **
notation.
1 | table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678} |
As an example, the following lines produce a tidily aligned set of columns giving integers and their squares and cubes:
1 | for x in range(1, 11): |
str.format()
进行字符串格式化的完整概述详见 格式字符串语法 。
手动格式化字符串
下面是使用手动格式化方式实现的同一个平方和立方的表:
1 | for x in range(1, 11): |
(注意,每列之间的空格是通过使用 print()
添加的:它总在其参数间添加空格。)
字符串对象的 str.rjust()
方法通过在左侧填充空格,对给定宽度字段中的字符串进行右对齐。同类方法还有 str.ljust()
和 str.center()
。这些方法不写入任何内容,只返回一个新字符串,如果输入的字符串太长,它们不会截断字符串,而是原样返回;虽然这种方式会弄乱列布局,但也比另一种方法好,后者在显示值时可能不准确(如果真的想截断字符串,可以使用 x.ljust(n)[:n]
这样的切片操作 。)
另一种方法是 str.zfill()
,该方法在数字字符串左边填充零,且能识别正负号:
1 | '12'.zfill(5) |
旧式字符串格式化方法
% 运算符(求余符)也可用于字符串格式化。给定 'string' % values
,则 string
中的 %
实例会以零个或多个 values
元素替换。此操作被称为字符串插值。例如:
1 | import math |
读写文件
open()
返回一个 file object ,最常使用的是两个位置参数和一个关键字参数:open(filename, mode, encoding=None)
1 | f = open('workfile', 'w', encoding="utf-8") |
第一个实参是文件名字符串。第二个实参是包含描述文件使用方式字符的字符串。mode 的值包括 'r'
,表示文件只能读取;'w'
表示只能写入(现有同名文件会被覆盖);'a'
表示打开文件并追加内容,任何写入的数据会自动添加到文件末尾。'r+'
表示打开文件进行读写。mode 实参是可选的,省略时的默认值为 'r'
。
通常情况下,文件是以 text mode 打开的,也就是说,你从文件中读写字符串,这些字符串是以特定的 encoding 编码的。如果没有指定 encoding ,默认的是与平台有关的(见 open()
)。因为 UTF-8 是现代事实上的标准,除非你知道你需要使用一个不同的编码,否则建议使用 encoding="utf-8"
。在模式后面加上一个 'b'
,可以用 binary mode 打开文件。二进制模式的数据是以 bytes
对象的形式读写的。在二进制模式下打开文件时,你不能指定 encoding 。
在文本模式下读取文件时,默认把平台特定的行结束符(Unix 上为 \n
, Windows 上为 \r\n
)转换为 \n
。在文本模式下写入数据时,默认把 \n
转换回平台特定结束符。这种操作方式在后台修改文件数据对文本文件来说没有问题,但会破坏 JPEG
或 EXE
等二进制文件中的数据。注意,在读写此类文件时,一定要使用二进制模式。
在处理文件对象时,最好使用 with
关键字。优点是,子句体结束后,文件会正确关闭,即便触发异常也可以。而且,使用 with
相比等效的 try
-finally
代码块要简短得多:
1 | with open('workfile', encoding="utf-8") as f: |
如果没有使用 with
关键字,则应调用 f.close()
关闭文件,即可释放文件占用的系统资源。
警告
调用
f.write()
时,未使用with
关键字,或未调用f.close()
,即使程序正常退出,也可能 导致f.write()
的参数没有完全写入磁盘。
通过 with
语句,或调用 f.close()
关闭文件对象后,再次使用该文件对象将会失败。
1 | f.close() |
文件对象的方法
read()
本节下文中的例子假定已创建 f
文件对象。
f.read(size)
可用于读取文件内容,它会读取一些数据,并返回字符串(文本模式),或字节串对象(在二进制模式下)。 size 是可选的数值参数。省略 size 或 size 为负数时,读取并返回整个文件的内容;文件大小是内存的两倍时,会出现问题。size 取其他值时,读取并返回最多 size 个字符(文本模式)或 size 个字节(二进制模式)。如已到达文件末尾,f.read()
返回空字符串(''
)。
1 | f.read() |
readline()
f.readline()
从文件中读取单行数据;字符串末尾保留换行符(\n
),只有在文件不以换行符结尾时,文件的最后一行才会省略换行符。这种方式让返回值清晰明确;只要 f.readline()
返回空字符串,就表示已经到达了文件末尾,空行使用 '\n'
表示,该字符串只包含一个换行符。
1 | f.readline() |
从文件中读取多行时,可以用循环遍历整个文件对象。这种操作能高效利用内存,快速,且代码简单:
1 | for line in f: |
如需以列表形式读取文件中的所有行,可以用 list(f)
或 f.readlines()
。
write()
1 | f.write('This is a test\n') |
写入其他类型的对象前,要先把它们转化为字符串(文本模式)或字节对象(二进制模式):
1 | value = ('the answer', 42) |
f.tell()
返回整数,给出文件对象在文件中的当前位置,表示为二进制模式下时从文件开始的字节数,以及文本模式下的意义不明的数字。
f.seek(offset, whence)
可以改变文件对象的位置。通过向参考点添加 offset 计算位置;参考点由 whence 参数指定。 whence 值为 0 时,表示从文件开头计算,1 表示使用当前文件位置,2 表示使用文件末尾作为参考点。省略 whence 时,其默认值为 0,即使用文件开头作为参考点。
1 | f = open('workfile', 'rb+') |
在文本文件(模式字符串未使用 b
时打开的文件)中,只允许相对于文件开头搜索(使用 seek(0, 2)
搜索到文件末尾是个例外),唯一有效的 offset 值是能从 f.tell()
中返回的,或 0。其他 offset 值都会产生未定义的行为。
文件对象还支持 isatty()
和 truncate()
等方法,但不常用;文件对象的完整指南详见库参考。
使用 json 保存结构化数据
从文件写入或读取字符串很简单,数字则稍显麻烦,因为 read()
方法只返回字符串,这些字符串必须传递给 int()
这样的函数,接受 '123'
这样的字符串,并返回数字值 123。保存嵌套列表、字典等复杂数据类型时,手动解析和序列化的操作非常复杂。
Rather than having users constantly writing and debugging code to save complicated data types to files, Python allows you to use the popular data interchange format called JSON (JavaScript Object Notation). The standard module called json
can take Python data hierarchies, and convert them to string representations; this process is called serializing. Reconstructing the data from the string representation is called deserializing. Between serializing and deserializing, the string representing the object may have been stored in a file or data, or sent over a network connection to some distant machine.
只需一行简单的代码即可查看某个对象的 JSON 字符串表现形式:
1 | import json |
dumps()
函数还有一个变体, dump()
,它只将对象序列化为 text file 。因此,如果 f
是 text file 对象,可以这样做:
1 | json.dump(x, f) |
要再次解码对象,如果 f
是已打开、供读取的 binary file 或 text file 对象:
1 | x = json.load(f) |
JSON文件必须以UTF-8编码。当打开JSON文件作为一个 text file 用于读写时,使用
encoding="utf-8"
。
这种简单的序列化技术可以处理列表和字典,但在 JSON 中序列化任意类的实例,则需要付出额外努力。json
模块的参考包含对此的解释。
参见
pickle
- 封存模块与 JSON 不同,pickle 是一种允许对复杂 Python 对象进行序列化的协议。因此,它为 Python 所特有,不能用于与其他语言编写的应用程序通信。默认情况下它也是不安全的:如果解序化的数据是由手段高明的攻击者精心设计的,这种不受信任来源的 pickle 数据可以执行任意代码。
json.load() 与 json.dump() 操作的是文件流对象,实现了 json 文件的读写操作,而 json.loads() 与 json.dumps() 操作的是 Python 对象或者 JOSN 字符串。
load()
- 需要从json文件中读取数据时
dump()
它可以将 Python 对象(字典、列表等)转换为 json 字符串,并将转换后的数据写入到 json 格式的文件中 ,因此该方法必须操作文件流对象。比如当使用爬虫程序完成数据抓取后,有时需要将数据保存为 json 格式,此时就用到了 json.dump() 方法,语法格式如下:
1
json.dump(object,f,inden=0,ensure_ascii=False)
参数说明如下:
- object:Python 数据对象,比如字典,列表等
- f:文件流对象,即文件句柄。
- indent:格式化存储数据,使 JSON 字符串更易阅读。
- ensure_ascii:是否使用 ascii 编码,当数据中出现中文的时候,需要将其设置为 False。
1
2
3
4
5
6
7
8
9import json
item_list = []
item = {'website': 'C语言中文网', 'url': "c.biancheng.net"}
for k,v in item.items():
item_list.append(v)
with open('info_web.json', 'a') as f:
json.dump(item_list, f, ensure_ascii=False)爬取网页数据,需要写到json文件中时
1
2
3
4
5import json
ditc_info={"name" : "c语言中文网","PV" : "50万","UV" : "20万","create_time" : "2010年"}
with open("web.josn","a") as f:
json.dump(ditc_info,f,ensure_ascii=False)
错误与异常
至此,本教程还未深入介绍错误信息,但如果您输入过本教程前文中的例子,应该已经看到过一些错误信息。目前,(至少)有两种不同错误:句法错误 和 异常
句法错误
句法错误又称解析错误,是学习 Python 时最常见的错误:
1 | while True print('Hello world') |
解析器会复现出现句法错误的代码行,并用小“箭头”指向行里检测到的第一个错误。错误是由箭头 上方 的 token 触发的(至少是在这里检测出的):本例中,在 print()
函数中检测到错误,因为,在它前面缺少冒号(':'
) 。错误信息还输出文件名与行号,在使用脚本文件时,就可以知道去哪里查错。
异常
即使语句或表达式使用了正确的语法,执行时仍可能触发错误。执行时检测到的错误称为 异常,异常不一定导致严重的后果:很快我们就能学会如何处理 Python 的异常。大多数异常不会被程序处理,而是显示下列错误信息:
1 | 10 * (1/0) |
错误信息的最后一行说明程序遇到了什么类型的错误。异常有不同的类型,而类型名称会作为错误信息的一部分中打印出来:上述示例中的异常类型依次是:ZeroDivisionError
, NameError
和 TypeError
。作为异常类型打印的字符串是发生的内置异常的名称。对于所有内置异常都是如此,但对于用户定义的异常则不一定如此(虽然这种规范很有用)。标准的异常类型是内置的标识符(不是保留关键字)。
此行其余部分根据异常类型,结合出错原因,说明错误细节。
错误信息开头用堆栈回溯形式展示发生异常的语境。一般会列出源代码行的堆栈回溯;但不会显示从标准输入读取的行。
内置异常 列出了内置异常及其含义。
异常的处理
可以编写程序处理选定的异常。下例会要求用户一直输入内容,直到输入有效的整数,但允许用户中断程序(使用 Control-C 或操作系统支持的其他操作);注意,用户中断程序会触发 KeyboardInterrupt
异常。
1 | while True: |
try
语句的工作原理如下:
- 首先,执行 try 子句 (
try
和except
关键字之间的(多行)语句)。 - 如果没有触发异常,则跳过 except 子句,
try
语句执行完毕。 - 如果在执行
try
子句时发生了异常,则跳过该子句中剩下的部分。 如果异常的类型与except
关键字后指定的异常相匹配,则会执行 except 子句,然后跳到 try/except 代码块之后继续执行。 - 如果发生的异常与 except 子句 中指定的异常不匹配,则它会被传递到外部的
try
语句中;如果没有找到处理程序,则它是一个 未处理异常 且执行将终止并输出如上所示的消息。
try
语句可以有多个 except 子句 来为不同的异常指定处理程序。 但最多只有一个处理程序会被执行。 处理程序只处理对应的 try 子句 中发生的异常,而不处理同一 try
语句内其他处理程序中的异常。 except 子句 可以用带圆括号的元组来指定多个异常,例如:
1 | except (RuntimeError, TypeError, NameError): |
如果发生的异常与 except
子句中的类是同一个类或是它的基类时,则该类与该异常相兼容(反之则不成立 — 列出派生类的 except 子句 与基类不兼容)。 例如,下面的代码将依次打印 B, C, D:
1 | class B(Exception): |
请注意如果颠倒 except 子句 的顺序(把 except B
放在最前),则会输出 B, B, B — 即触发了第一个匹配的 except 子句。
When an exception occurs, it may have associated values, also known as the exception’s arguments. The presence and types of the arguments depend on the exception type.
The except clause may specify a variable after the exception name. The variable is bound to the exception instance which typically has an args
attribute that stores the arguments. For convenience, builtin exception types define __str__()
to print all the arguments without explicitly accessing .args
.
1 | try: |
The exception’s __str__()
output is printed as the last part (‘detail’) of the message for unhandled exceptions.
BaseException
is the common base class of all exceptions. One of its subclasses, Exception
, is the base class of all the non-fatal exceptions. Exceptions which are not subclasses of Exception
are not typically handled, because they are used to indicate that the program should terminate. They include SystemExit
which is raised by sys.exit()
and KeyboardInterrupt
which is raised when a user wishes to interrupt the program.
Exception
can be used as a wildcard that catches (almost) everything. However, it is good practice to be as specific as possible with the types of exceptions that we intend to handle, and to allow any unexpected exceptions to propagate on.
The most common pattern for handling Exception
is to print or log the exception and then re-raise it (allowing a caller to handle the exception as well):
1 | import sys |
try
… except
语句具有可选的 else 子句,该子句如果存在,它必须放在所有 except 子句 之后。 它适用于 try 子句 没有引发异常但又必须要执行的代码。 例如:
1 | for arg in sys.argv[1:]: |
使用 else
子句比向 try
子句添加额外的代码要好,可以避免意外捕获非 try
… except
语句保护的代码触发的异常。
Exception handlers do not handle only exceptions that occur immediately in the try clause, but also those that occur inside functions that are called (even indirectly) in the try clause. For example:
1 | def this_fails(): |
触发异常
raise
语句支持强制触发指定的异常。例如:
1 | raise NameError('HiThere') |
The sole argument to raise
indicates the exception to be raised. This must be either an exception instance or an exception class (a class that derives from BaseException
, such as Exception
or one of its subclasses). If an exception class is passed, it will be implicitly instantiated by calling its constructor with no arguments:
1 | raise ValueError # shorthand for 'raise ValueError()' |
如果只想判断是否触发了异常,但并不打算处理该异常,则可以使用更简单的 raise
语句重新触发异常:
1 | try: |
异常链
If an unhandled exception occurs inside an except
section, it will have the exception being handled attached to it and included in the error message:
1 | try: |
To indicate that an exception is a direct consequence of another, the raise
statement allows an optional from
clause:
1 | # exc must be exception instance or None. |
转换异常时,这种方式很有用。例如:
1 | def func(): |
It also allows disabling automatic exception chaining using the from None
idiom:
1 | try: |
异常链机制详见 内置异常。
用户自定义异常
程序可以通过创建新的异常类命名自己的异常(Python 类的内容详见 类)。不论是以直接还是间接的方式,异常都应从 Exception
类派生。
异常类可以被定义成能做其他类所能做的任何事,但通常应当保持简单,它往往只提供一些属性,允许相应的异常处理程序提取有关错误的信息。
大多数异常命名都以 “Error” 结尾,类似标准异常的命名。
Many standard modules define their own exceptions to report errors that may occur in functions they define.
定义清理操作
try
语句还有一个可选子句,用于定义在所有情况下都必须要执行的清理操作。例如:
1 | try: |
如果存在 finally
子句,则 finally
子句是 try
语句结束前执行的最后一项任务。不论 try
语句是否触发异常,都会执行 finally
子句。以下内容介绍了几种比较复杂的触发异常情景:
如果执行
try
子句期间触发了某个异常,则某个except
子句应处理该异常。如果该异常没有except
子句处理,在finally
子句执行后会被重新触发。except
或else
子句执行期间也会触发异常。 同样,该异常会在finally
子句执行之后被重新触发。如果执行
try
语句时遇到break
,、continue
或return
语句,则finally
子句在执行break
、continue
或return
语句之前执行。如果
finally
子句中包含return
语句,则返回值来自finally
子句的某个return
语句的返回值,而不是来自try
子句的return
语句的返回值。1
2
3
4
5
6
7
8
9def bool_return():
try:
return True
finally:
return False
res = bool_return()
print(res) # Flase
这是一个比较复杂的例子:
1 | def divide(x, y): |
如上所示,任何情况下都会执行 finally
子句。except
子句不处理两个字符串相除触发的 TypeError
,因此会在 finally
子句执行后被重新触发。
在实际应用程序中,finally
子句对于释放外部资源(例如文件或者网络连接)非常有用,无论是否成功使用资源。
预定义的清理操作
某些对象定义了不需要该对象时要执行的标准清理操作。无论使用该对象的操作是否成功,都会执行清理操作。比如,下例要打开一个文件,并输出文件内容:
1 | for line in open("myfile.txt"): |
这个代码的问题在于,执行完代码后,文件在一段不确定的时间内处于打开状态。在简单脚本中这没有问题,但对于较大的应用程序来说可能会出问题。with
语句支持以及时、正确的清理的方式使用文件对象:
1 | with open("myfile.txt") as f: |
语句执行完毕后,即使在处理行时遇到问题,都会关闭文件 f。和文件一样,支持预定义清理操作的对象会在文档中指出这一点。
Raising and Handling Multiple Unrelated Exceptions
There are situations where it is necessary to report several exceptions that have occurred. This is often the case in concurrency frameworks, when several tasks may have failed in parallel, but there are also other use cases where it is desirable to continue execution and collect multiple errors rather than raise the first exception.
The builtin ExceptionGroup
wraps a list of exception instances so that they can be raised together. It is an exception itself, so it can be caught like any other exception.
1 | def f(): |
By using except*
instead of except
, we can selectively handle only the exceptions in the group that match a certain type. In the following example, which shows a nested exception group, each except*
clause extracts from the group exceptions of a certain type while letting all other exceptions propagate to other clauses and eventually to be reraised.
1 | def f(): |
Note that the exceptions nested in an exception group must be instances, not types. This is because in practice the exceptions would typically be ones that have already been raised and caught by the program, along the following pattern:
1 | excs = [] |
Enriching Exceptions with Notes
When an exception is created in order to be raised, it is usually initialized with information that describes the error that has occurred. There are cases where it is useful to add information after the exception was caught. For this purpose, exceptions have a method add_note(note)
that accepts a string and adds it to the exception’s notes list. The standard traceback rendering includes all notes, in the order they were added, after the exception.
1 | try: |
For example, when collecting exceptions into an exception group, we may want to add context information for the individual errors. In the following each exception in the group has a note indicating when this error has occurred.
1 | def f(): |
类
类把数据与功能绑定在一起。创建新类就是创建新的对象 类型,从而创建该类型的新 实例 。类实例支持维持自身状态的属性,还支持(由类定义的)修改自身状态的方法。
和其他编程语言相比,Python 的类只使用了很少的新语法和语义。Python 的类有点类似于 C++ 和 Modula-3 中类的结合体,而且支持面向对象编程(OOP)的所有标准特性:类的继承机制支持多个基类、派生的类能覆盖基类的方法、类的方法能调用基类中的同名方法。对象可包含任意数量和类型的数据。和模块一样,类也支持 Python 动态特性:在运行时创建,创建后还可以修改。
如果用 C++ 术语来描述的话,类成员(包括数据成员)通常为 public (例外的情况见下文 私有变量),所有成员函数都是 virtual。与在 Modula-3 中一样,没有用于从对象的方法中引用对象成员的简写形式:方法函数在声明时,有一个显式的参数代表本对象,该参数由调用隐式提供。 与在 Smalltalk 中一样,Python 的类也是对象,这为导入和重命名提供了语义支持。与 C++ 和 Modula-3 不同,Python 的内置类型可以用作基类,供用户扩展。 此外,与 C++ 一样,算术运算符、下标等具有特殊语法的内置运算符都可以为类实例而重新定义。
由于缺乏关于类的公认术语,本章中偶尔会使用 Smalltalk 和 C++ 的术语。本章还会使用 Modula-3 的术语,Modula-3 的面向对象语义比 C++ 更接近 Python,但估计听说过这门语言的读者很少。
名称和对象
对象之间相互独立,多个名称(在多个作用域内)可以绑定到同一个对象。 其他语言称之为别名。Python 初学者通常不容易理解这个概念,处理数字、字符串、元组等不可变基本类型时,可以不必理会。 但是,对涉及可变对象,如列表、字典等大多数其他类型的 Python 代码的语义,别名可能会产生意料之外的效果。这样做,通常是为了让程序受益,因为别名在某些方面就像指针。例如,传递对象的代价很小,因为实现只传递一个指针;如果函数修改了作为参数传递的对象,调用者就可以看到更改 — 无需 Pascal 用两个不同参数的传递机制。
作用域和命名空间
在介绍类前,首先要介绍 Python 的作用域规则。类定义对命名空间有一些巧妙的技巧,了解作用域和命名空间的工作机制有利于加强对类的理解。并且,即便对于高级 Python 程序员,这方面的知识也很有用。
接下来,我们先了解一些定义。
namespace (命名空间)是映射到对象的名称。现在,大多数命名空间都使用 Python 字典实现,但除非涉及到优化性能,我们一般不会关注这方面的事情,而且将来也可能会改变这种方式。命名空间的几个常见示例: abs()
函数、内置异常等的内置函数集合;模块中的全局名称;函数调用中的局部名称。对象的属性集合也算是一种命名空间。关于命名空间的一个重要知识点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义 maximize
函数,且不会造成混淆。用户使用函数时必须要在函数名前面附加上模块名。
点号之后的名称是 属性。例如,表达式 z.real
中,real
是对象 z
的属性。严格来说,对模块中名称的引用是属性引用:表达式 modname.funcname
中,modname
是模块对象,funcname
是模块的属性。模块属性和模块中定义的全局名称之间存在直接的映射:它们共享相同的命名空间! 1例外
属性可以是只读或者可写的。如果可写,则可对属性赋值。模块属性是可写时,可以使用 modname.the_answer = 42
。del
语句可以删除可写属性。例如, del modname.the_answer
会删除 modname
对象中的 the_answer
属性。
命名空间是在不同时刻创建的,且拥有不同的生命周期。内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在读取模块定义时创建;通常,模块的命名空间也会持续到解释器退出。从脚本文件读取或交互式读取的,由解释器顶层调用执行的语句是 __main__
模块调用的一部分,也拥有自己的全局命名空间。内置名称实际上也在模块里,即 builtins
。
函数的本地命名空间在调用该函数时创建,并在函数返回或抛出不在函数内部处理的错误时被删除。 (实际上,用“遗忘”来描述实际发生的情况会更好一些。) 当然,每次递归调用都会有自己的本地命名空间。
作用域 是命名空间可直接访问的 Python 程序的文本区域。 “可直接访问” 的意思是,对名称的非限定引用会在命名空间中查找名称。
作用域虽然是静态确定的,但会被动态使用。执行期间的任何时刻,都会有 3 或 4 个命名空间可被直接访问的嵌套作用域:
- 最内层作用域,包含局部名称,并首先在其中进行搜索
- the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contain non-local, but also non-global names
- 倒数第二个作用域,包含当前模块的全局名称
- 最外层的作用域,包含内置名称的命名空间,最后搜索
If a name is declared global, then all references and assignments go directly to the next-to-last scope containing the module’s global names. To rebind variables found outside of the innermost scope, the nonlocal
statement can be used; if not declared nonlocal, those variables are read-only (an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged).
通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。在函数之外,局部作用域引用与全局作用域一致的命名空间:模块的命名空间。 类定义在局部命名空间内再放置另一个命名空间。
划重点,作用域是按字面文本确定的:模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。另一方面,实际的名称搜索是在运行时动态完成的。但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析!(局部变量已经是被静态确定了。)
Python 有一个特殊规定。如果不存在生效的 global
或 nonlocal
语句,则对名称的赋值总是会进入最内层作用域。赋值不会复制数据,只是将名称绑定到对象。删除也是如此:语句 del x
从局部作用域引用的命名空间中移除对 x
的绑定。所有引入新名称的操作都是使用局部作用域:尤其是 import
语句和函数定义会在局部作用域中绑定模块或函数名称。
global
语句用于表明特定变量在全局作用域里,并应在全局作用域中重新绑定;nonlocal
语句表明特定变量在外层作用域中,并应在外层作用域中重新绑定。
作用域和命名空间示例
下例演示了如何引用不同作用域和名称空间,以及 global
和 nonlocal
对变量绑定的影响:
1 | def scope_test(): |
示例代码的输出是:
1 | After local assignment: test spam |
注意,局部 赋值(这是默认状态)不会改变 scope_test 对 spam 的绑定。 nonlocal
赋值会改变 scope_test 对 spam 的绑定,而 global
赋值会改变模块层级的绑定。
而且,global
赋值前没有 spam 的绑定。
初探类
类引入了一点新语法,三种新的对象类型和一些新语义。
类定义语法
最简单的类定义形式如下:
1 | class ClassName: |
与函数定义 (def
语句) 一样,类定义必须先执行才能生效。把类定义放在 if
语句的分支里或函数内部试试。
在实践中,类定义内的语句通常都是函数定义,但也可以是其他语句。这部分内容稍后再讨论。类里的函数定义一般是特殊的参数列表,这是由方法调用的约定规范所指明的 — 同样,稍后再解释。
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 — 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。
当(从结尾处)正常离开类定义时,将创建一个 类对象。 这基本上是一个包围在类定义所创建命名空间内容周围的包装器;我们将在下一节了解有关类对象的更多信息。 原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称 (在这个示例中为 ClassName
)。
Class 对象
类对象支持两种操作:属性引用和实例化。
属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name
。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。 因此,如果类定义是这样的:
1 | class MyClass: |
那么 MyClass.i
和 MyClass.f
就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来更改 MyClass.i
的值。 __doc__
也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"
。
类的 实例化 使用函数表示法。 可以把类对象视为是返回该类的一个新实例的不带参数的函数。 举例来说(假设使用上述的类):
1 | x = MyClass() |
创建类的新 实例 并将此对象分配给局部变量 x
。
实例化操作(“调用”类对象)会创建一个空对象。 许多类喜欢创建带有特定初始状态的自定义实例。 为此类定义可能包含一个名为 __init__()
的特殊方法,就像这样:
1 | def __init__(self): |
When a class defines an __init__()
method, class instantiation automatically invokes __init__()
for the newly created class instance. So in this example, a new, initialized instance can be obtained by:
1 | x = MyClass() |
当然,__init__()
方法还可以有额外参数以实现更高灵活性。 在这种情况下,提供给类实例化运算符的参数将被传递给 __init__()
。 例如,
1 | class Complex: |
实例对象
现在我们能用实例对象做什么? 实例对象所能理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。
数据属性 对应于 Smalltalk 中的“实例变量”,以及 C++ 中的“数据成员”。 数据属性不需要声明;像局部变量一样,它们将在第一次被赋值时产生。 例如,如果 x
是上面创建的 MyClass
的实例,则以下代码段将打印数值 16
,且不保留任何追踪信息:
1 | x.counter = 1 |
另一类实例属性引用称为 方法。 方法是“从属于”对象的函数。 (在 Python 中,方法这个术语并不是类实例所特有的:其他对象也可以有方法。 例如,列表对象具有 append, insert, remove, sort 等方法。 然而,在以下讨论中,我们使用方法一词将专指类实例对象的方法,除非另外显式地说明。)
实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类中所有是函数对象的属性都是定义了其实例的相应方法。 因此在我们的示例中,x.f
是有效的方法引用,因为 MyClass.f
是一个函数,而 x.i
不是方法,因为 MyClass.i
不是函数。 但是 x.f
与 MyClass.f
并不是一回事 — 它是一个 方法对象,不是函数对象。
方法对象
通常,方法在绑定后立即被调用:
1 | x.f() |
在 MyClass
示例中,这将返回字符串 'hello world'
。 但是,立即调用一个方法并不是必须的: x.f
是一个方法对象,它可以被保存起来以后再调用。 例如:
1 | xf = x.f |
将持续打印 hello world
,直到结束。
当一个方法被调用时到底发生了什么? 你可能已经注意到上面调用 x.f()
时并没有带参数,虽然 f()
的函数定义指定了一个参数。 这个参数发生了什么事? 当不带参数地调用一个需要参数的函数时 Python 肯定会引发异常 — 即使参数实际未被使用…
实际上,你可能已经猜到了答案:方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 在我们的示例中,调用 x.f()
其实就相当于 MyClass.f(x)
。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前。
如果你仍然无法理解方法的运作原理,那么查看实现细节可能会弄清楚问题。 当一个实例的非数据属性被引用时,将搜索实例所属的类。 如果被引用的属性名称表示一个有效的类属性中的函数对象,会通过打包(指向)查找到的实例对象和函数对象到一个抽象对象的方式来创建方法对象:这个抽象对象就是方法对象。 当附带参数列表调用方法对象时,将基于实例对象和参数列表构建一个新的参数列表,并使用这个新参数列表调用相应的函数对象。
类和实例变量
一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法:
1 | class Dog: |
正如 名称和对象 中已讨论过的,共享数据可能在涉及 mutable 对象例如列表和字典的时候导致令人惊讶的结果。 例如以下代码中的 tricks 列表不应该被用作类变量,因为所有的 Dog 实例将只共享一个单独的列表:
1 | class Dog: |
正确的类设计应该使用实例变量:
1 | class Dog: |
补充说明
如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:
1 | class Warehouse: |
数据属性可以被方法以及一个对象的普通用户(“客户端”)所引用。 换句话说,类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 — 它是完全基于约定的。 (而在另一方面,用 C 语言编写的 Python 实现则可以完全隐藏实现细节,并在必要时控制对象的访问;此特性可以通过用 C 编写 Python 扩展来使用。)
客户端应当谨慎地使用数据属性 — 客户端可能通过直接操作数据属性的方式破坏由方法所维护的固定变量。 请注意客户端可以向一个实例对象添加他们自己的数据属性而不会影响方法的可用性,只要保证避免名称冲突 — 再次提醒,在此使用命名约定可以省去许多令人头痛的麻烦。
在方法内部引用数据属性(或其他方法!)并没有简便方式。 我发现这实际上提升了方法的可读性:当浏览一个方法代码时,不会存在混淆局部变量和实例变量的机会。
方法的第一个参数常常被命名为 self
。 这也不过就是一个约定: self
这一名称在 Python 中绝对没有特殊含义。 但是要注意,不遵循此约定会使得你的代码对其他 Python 程序员来说缺乏可读性,而且也可以想像一个 类浏览器 程序的编写可能会依赖于这样的约定。
任何一个作为类属性的函数都为该类的实例定义了一个相应方法。 函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的。 例如:
1 | # Function defined outside the class |
现在 f
, g
和 h
都是 C
类的引用函数对象的属性,因而它们就都是 C
的实例的方法 — 其中 h
完全等同于 g
。 但请注意,本示例的做法通常只会令程序的阅读者感到迷惑。
方法可以通过使用 self
参数的方法属性调用其他方法:
1 | class Bag: |
方法可以通过与普通函数相同的方式引用全局名称。 与方法相关联的全局作用域就是包含其定义的模块。 (类永远不会被作为全局作用域。) 虽然我们很少会有充分的理由在方法中使用全局作用域,但全局作用域存在许多合理的使用场景:举个例子,导入到全局作用域的函数和模块可以被方法所使用,在其中定义的函数和类也一样。 通常,包含该方法的类本身是在全局作用域中定义的,而在下一节中我们将会发现为何方法需要引用其所属类的很好的理由。
每个值都是一个对象,因此具有 类 (也称为 类型),并存储为 object.__class__
。
其他
1 |
继承
当然,如果不支持继承,语言特性就不值得称为“类”。派生类定义的语法如下所示:
1 | class DerivedClassName(BaseClassName): |
名称 BaseClassName
必须定义于包含派生类定义的作用域中。 也允许用其他任意表达式代替基类名称所在的位置。 这有时也可能会用得上,例如,当基类定义在另一个模块中的时候:
1 | class DerivedClassName(modname.BaseClassName): |
派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。
派生类的实例化没有任何特殊之处: DerivedClassName()
会创建该类的一个新实例。 方法引用将按以下方式解析:搜索相应的类属性,如有必要将按基类继承链逐步向下查找,如果产生了一个函数对象则方法引用就生效。
派生类可能会重写其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,所以调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。 (对 C++ 程序员的提示:Python 中所有的方法实际上都是 virtual
方法。)
在派生类中的重载方法实际上可能想要扩展而非简单地替换同名的基类方法。 有一种方式可以简单地直接调用基类方法:即调用 BaseClassName.methodname(self, arguments)
。 有时这对客户端来说也是有用的。 (请注意仅当此基类可在全局作用域中以 BaseClassName
的名称被访问时方可使用此方式。)
Python有两个内置函数可被用于继承机制:
- 使用
isinstance()
来检查一个实例的类型:isinstance(obj, int)
仅会在obj.__class__
为int
或某个派生自int
的类时为True
。 - 使用
issubclass()
来检查类的继承关系:issubclass(bool, int)
为True
,因为bool
是int
的子类。 但是,issubclass(float, int)
为False
,因为float
不是int
的子类。
多重继承
Python 也支持一种多重继承。 带有多个基类的类定义语句如下所示:
1 | class DerivedClassName(Base1, Base2, Base3): |
对于多数应用来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的,当层次结构中存在重叠时不会在同一个类中搜索两次。 因此,如果某一属性在 DerivedClassName
中未找到,则会到 Base1
中搜索它,然后(递归地)到 Base1
的基类中搜索,如果在那里未找到,再到 Base2
中搜索,依此类推。
真实情况比这个更复杂一些;方法解析顺序会动态改变以支持对 super()
的协同调用。 这种方式在某些其他多重继承型语言中被称为后续方法调用,它比单继承型语言中的 super 调用更强大。
动态改变顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个父类可通过多条路径被最底层类所访问)。 例如,所有类都是继承自 object
,因此任何多重继承的情况都提供了一条以上的路径可以通向 object
。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化, 保留每个类所指定的从左至右的顺序,只调用每个父类一次,并且保持单调(即一个类可以被子类化而不影响其父类的优先顺序)。 总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 要了解更多细节,请参阅 https://www.python.org/download/releases/2.3/mro/。
私有变量
那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。
由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 __spam
的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam
,其中 classname
为去除了前缀下划线的当前类名称。 这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。
名称改写有助于让子类重载方法而不破坏类内方法调用。例如:
1 | class Mapping: |
上面的示例即使在 MappingSubclass
引入了一个 __update
标识符的情况下也不会出错,因为它会在 Mapping
类中被替换为 _Mapping__update
而在 MappingSubclass
类中被替换为 _MappingSubclass__update
。
请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。
请注意传递给 exec()
或 eval()
的代码不会将发起调用类的类名视作当前类;这类似于 global
语句的效果,因此这种效果仅限于同时经过字节码编译的代码。 同样的限制也适用于 getattr()
, setattr()
和 delattr()
,以及对于 __dict__
的直接引用。
杂项说明
Sometimes it is useful to have a data type similar to the Pascal “record” or C “struct”, bundling together a few named data items. The idiomatic approach is to use dataclasses
for this purpose:
1 | from dataclasses import dataclass |
1 | john = Employee('john', 'computer lab', 1000) |
一段需要特定抽象数据类型的 Python 代码往往可以被传入一个模拟了该数据类型的方法的类作为替代。 例如,如果你有一个基于文件对象来格式化某些数据的函数,你可以定义一个带有 read()
和 readline()
方法从字符串缓存获取数据的类,并将其作为参数传入。
实例方法对象也具有属性: m.__self__
就是带有 m()
方法的实例对象,而 m.__func__
则是该方法所对应的函数对象。
迭代器
到目前为止,您可能已经注意到大多数容器对象都可以使用 for
语句:
1 | for element in [1, 2, 3]: |
这种访问风格清晰、简洁又方便。 迭代器的使用非常普遍并使得 Python 成为一个统一的整体。 在幕后,for
语句会在容器对象上调用 iter()
。 该函数返回一个定义了 __next__()
方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,__next__()
将引发 StopIteration
异常来通知终止 for
循环。 你可以使用 next()
内置函数来调用 __next__()
方法;这个例子显示了它的运作方式:
1 | s = 'abc' |
看过迭代器协议的幕后机制,给你的类添加迭代器行为就很容易了。 定义一个 __iter__()
方法来返回一个带有 __next__()
方法的对象。 如果类已定义了 __next__()
,则 __iter__()
可以简单地返回 self
:
1 | class Reverse: |
1 | rev = Reverse('spam') |
生成器
生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield
语句。 每次在生成器上调用 next()
时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。 一个显示如何非常容易地创建生成器的示例如下:
1 | def reverse(data): |
1 | for char in reverse('golf'): |
可以用生成器来完成的操作同样可以用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()
和 __next__()
方法。
另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。 这使得该函数相比使用 self.index
和 self.data
这种实例变量的方式更易编写且更为清晰。
除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration
。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。
生成器表达式
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
1 | sum(i*i for i in range(10)) # sum of squares |
标准库简介
操作系统接口
os
模块提供了许多与操作系统交互的函数:
1 | import os |
一定要使用 import os
而不是 from os import *
。这将避免内建的 open()
函数被 os.open()
隐式替换掉,因为它们的使用方式大不相同。
内置的 dir()
和 help()
函数可用作交互式辅助工具,用于处理大型模块,如 os
:
1 | import os |
对于日常文件和目录管理任务, shutil
模块提供了更易于使用的更高级别的接口:
1 | import shutil |
编码
常见的编码格式
- utf-8
- url编码
解码
url解码
常用的python url解码代码 :
- 使用urllib库的unquote方法进行url解码:输出结果为:
1
2
3
4import urllib
url = "https://www.example.com/?q=%E6%9D%8E%E5%9B%9B"
decoded_url = urllib.parse.unquote(url)
print(decoded_url)https://www.example.com/?q=李四
- 使用requests库的unquote方法进行url解码:输出结果为:
1
2
3
4import requests
url = "https://www.example.com/?q=%E6%9D%8E%E5%9B%9B"
decoded_url = requests.utils.unquote(url)
print(decoded_url)https://www.example.com/?q=李四
- 使用urllib.parse.unquote_plus方法进行url解码,该方法会将空格解码为加号:
1
2
3
4import urllib.parse
url = "https://www.example.com/?q=%E6%9D%8E%E5%9B%9B+%E5%8A%A0%E5%8A%A0"
decoded_url = urllib.parse.unquote_plus(url)
print(decoded_url)
异步编程
异步等待
1 | import asyncio |
进阶概念
常见问题
- Python常见问题
- 编程常见问题
- 设计和历史常见问题
- 代码库和插件 FAQ
- 扩展/嵌入常见问题
- Python在Windows上的常见问题
- 图形用户界面(GUI)常见问题
- “为什么我的电脑上安装了 Python ?”
if name main
的作用
1 | if __name__ == "__main__": |
一个python文件通常有两种使用方法
- 第一是作为脚本直接执行
- 第二是 import 到其他的 python 脚本中被调用(模块重用)执行。
因此if __name__ == "__main__":
的作用就是控制这两种情况执行代码的过程,在if __name__ == "__main__":
下的代码只有在第一种情况下(即文件作为脚本直接执行)才会被执行,而 import 到其他脚本中是不会被执行的。
举例说明如下:
直接执行
在 test.py 中写入如下代码:
1 | print('this is one') |
直接执行python3 test.py
,结果如下图,可以成功 print 两行字符串。即,if __name__ == "__main__":
语句之前和之后的代码都被执行。
import 执行
然后在同一文件夹新建名称为 import_test.py 的脚本,输入如下代码:
1 | import test |
执行python3 import_test.py
脚本,输出结果如下:
作为模块import执行,只输出了第一行字符串。即,if __name__ == "__main__":
外部的语句被执行,内部的没有被执行。
运行原理
每个python模块(python文件,也就是此处的 test.py 和 import_test.py)都包含内置的变量 __name__
,当该模块被直接当作脚本执行的时候,__name__
等于__main__
;如果该模块 import 到其他模块中,则该模块的 __name__
等于模块(文件)名称(不包含后缀.py)。
进而当模块被直接当作脚本执行时,__name__
== ‘__main__
‘ 结果为真。
为了进一步说明,使用如下代码验证:
1 | # test.py |
直接以脚本运行,结果如下
新建import_test.py
1 | # import_test.py |
运行python3 import_test.py
,结果如下:
上下文管理器
Context Manager指的是python在执行一段代码前后,做的一些预处理和后处理,使得代码块运行处于一个小的环境(surrounding),出了这个小环境之后,资源释放,环境中的各种配置也失效。
例如在打开文件需要关闭,连接数据库后需要关闭连接。很多优雅第三方库也会利用上下文使得对象进入特定的某种状态。
with关键字
with的基本用法如下:
1 | with EXPR as VAR: |
其中发生了一系列过程:
- EXPR语句被执行,得到ContextManager
- 调用ContextManager.__enter__方法
- 如果有as VAR,则ContextManager.__enter__的返回值赋给VAR,否则就不管返回值
- 执行BLOCK,如果有VAR被用到,就和普通变量一样
- 调用ContextManager.__exit__方法
- __exit__有三个参数:type, value, traceback,BLOCK出异常时会得到对应值,正常情况就都为None
- __exit__返回值为True表示BLOCK中出现的异常可以忽略,False表示需要raise
例子
资源操作:
1 | class CustomOpen: |
运行结果:
1 | enter...... |
状态维护
1 | class CustomBrain: |
运行结果:
1 | You're a great man |
使用contextlib简化编写
python内置的标准库contextlib可以使得代码书写更加简洁,本质是一样的。比较有用的是contextlib.contextmanager
这个装饰器,被装饰的函数在yield
的前面相当于__enter__,yield的后面相当于__exit__,yield
本身的返回值赋给as
后的变量
所以第一个示例可以这么写:
1 | from contextlib import contextmanager |
还是优雅了许多~
__init__.py
Python中常见的__init__.py是什么意思?详解Python import的方式和原理
Python包以__init__.py
为标志,用于实现工程模块化,假设包组织结构的实例如下:
1 | package |
用虚拟文件夹的方式理解Python包。所有的包都可视作文件夹,其下包含模块或子包(子文件夹),模块中包含函数、类、变量等属性。当前路径位置可视作一个空白文件夹,关键字from理解为“打开”,关键字import理解为“导入”,必须指出:所有import相关操作都要落实到模块或属性。
一般地,导入有如下方式:
1 | import subpackage1.moduleA |
此方式相当于把一个名为subpackage1的文件夹复制粘贴到当前路径下,文件夹只包含模块moduleA,即使subpackage1中可能还有其他模块,引用moduleA中的func1()需要subpackage1.moduleA.fun1(),即打开subpackage1文件夹,再使用模块moduleA中的属性fun1()。注意,如果仅import subpackage1,相当于只引入了一个空文件夹,此时无法调用fun1(),除非在__init__.py中提前导入了模块。
1 | from subpackage1 import moduleA |
此方式相当于打开一个名为subpackage1的文件夹,再将其中的模块moduleA复制粘贴到当前空白文件夹下,引用moduleA的fun1()需要moduleA.fun1()。这种方式下,还有from subpackage1 import *
的句式可以引入包中的所有模块。
1 | from subpackage.moduleA import fun1() |
此方式相当于打开一个名为subpackage1的文件夹下的模块moduleA,再将其中的fun1()复制粘贴到当前空白文件夹,引用fun1()只需fun1()即可。
除了应用上述导入句式外,还需要注意当前文件的运行路径,如下所示为一个忽略路径因素造成的导入包报错,因为运行目录app\pkg_2\
下没有文件pkg_1且环境变量中也不存在pkg_1。
1 | app |
若需要保持运行目录不变,必须进行环境变量配置,在import pkg_1前先添加父级目录到python解释器的运行环境变量中,在pkg_2的父级目录app下可访问到pkg_1,具体实现上依赖于sys和os包
1 | import sys, os |
或者更换执行脚本的目录
python -m
参数详解
这篇文章主要介绍了Python 中 -m 的典型用法、原理解析与发展演变,需要的朋友可以参考下
在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下:
1 | python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args] |
本文想要聊聊比较特殊的“-m”选项: 关于它的典型用法、原理解析与发展演变的过程。
首先,让我们用“–help”来看看它的解释:
1 | -m mod run library module as a script (terminates option list) |
“mod”是“module”的缩写,即“-m”选项后面的内容是 module(模块),其作用是把模块当成脚本来运行。
“terminates option list”意味着“-m”之后的其它选项不起作用,在这点上它跟“-c”是一样的,都是“终极选项”。官方把它们定义为“接口选项”(Interface options),需要区别于其它的普通选项或通用选项。
-m
选项的五个典型用法
Python 中有很多使用 -m 选项的场景,相信大家可能会用到或者看见过,我在这里想分享 5 个。
在 Python3 中,只需一行命令就能实现一个简单的 HTTP 服务:
1 | python -m http.server 8000 |
执行后,在本机打开“ http://localhost:8000 ”,或者在局域网内的其它机器上打开“ http://本机ip:8000 ”,就能访问到执行目录下的内容,例如下图就是我本机的内容:
与此类似,我们只需要一行命令“python -m pydoc -p xxx
”,就能生成 HTML 格式的官方帮助文档,可以在浏览器中访问。
上面的命令执行了 pydoc 模块,会在 9000 端口启动一个 http 服务,在浏览器中打开,我的结果如下:
它的第三个常见用法是执行 pdb 的调试命令“python -m pdb xxx.py
”,以调试模式来执行“xxx.py”脚本:
第四个同样挺有用的场景是用 timeit 在命令行中测试一小段代码的运行时间。以下的 3 段代码,用不同的方式拼接 “0-1-2-……-99” 数字串。可以直观地看出它们的效率差异:
最后,还有一种常常被人忽略的场景:“python -m pip install xxx”。我们可能会习惯性地使用“pip install xxx”,或者做了版本区分时用“pip3 install xxx”,总之不在前面用“python -m”做指定。但这种写法可能会出问题。
很巧合的是,在本月初(2019.11.01),Python 的核心开发者、第一届指导委员会 五人成员之一的 Brett Cannon 专门写了一篇博客《 Why you should use “python -m pip“ 》,提出应该使用“python -m pip”的方式,并做了详细的解释。
他的主要观点是:在存在多个 Python 版本的环境中,这种写法可以精确地控制三方库的安装位置。例如用“python3.8 -m pip”,可以明确指定给 3.8 版本安装,而不会混淆成其它的版本。
(延伸阅读:关于 Brett 的文章,这有一篇简短的归纳《 原来我一直安装 Python 库的姿势都不对呀! 》)
-m
选项的两种原理解析
看了前面的几种典型用法,你是否开始好奇: “-m”是怎么运作的?它是怎么实现的?
对于“python -m name”,一句话解释: Python 会检索 sys.path
,查找名字为“name”的模块或者包(含命名空间包),并将其内容当成“__main__
”模块来执行。
对于普通模块
以“.py”为后缀的文件就是一个模块,在“-m”之后使用时,只需要使用模块名,不需要写出后缀,但前提是该模块名是有效的,且不能是用 C 语言写成的模块。
在“-m”之后,如果是一个无效的模块名,则会报错“No module named xxx”。
如果是一个带后缀的模块,则首先会导入该模块,然后可能报错:Error while finding module specification for ‘xxx.py’ (AttributeError: module ‘xxx’ has no attribute ‘__path__
‘。
1 | # -*- encoding: utf-8 -*- |
对于一个普通模块,有时候这两种写法表面看起来是等效的:
两种写法都会把定位到的模块脚本当成主程序入口来执行,即在执行时,该脚本的 __name__
都是”__main__
“,跟 import 导入方式是不同的。
但它的前提是:在执行目录中存在着“test.py”,且只有唯一的“test”模块。对于本例,如果换一个目录执行的话,“python test.py”当然会报找不到文件的错误,然而,“python -m test
”却不会报错,因为解释器在遍历 sys.path
时可以找到同名的“test”模块,并且执行
由此差异,我们其实可以总结出“-m”的用法: 已知一个模块的名字,但不知道它的文件路径,那么使用“-m”就意味着交给解释器自行查找,若找到,则当成脚本执行。
以前文的“python -m http.server 8000
”为例,我们也可以找到“server”模块的绝对路径,然后执行,尽管这样会变得很麻烦。
那么,“-m”方式与直接运行脚本相比,在实现上有什么不同呢?
1、直接运行脚本时,相当于给出了脚本的完整路径(不管是绝对路径还是相对路径),解释器根据 文件系统的查找机制, 定位到该脚本,然后执行
使用“-m”方式时,解释器需要在不 import 的情况下,在 所有模块命名空间 中查找,定位到脚本的路径,然后执行。为了实现这个过程,解释器会借助两个模块: pkgutil
和 runpy
,前者用来获取所有的模块列表,后者根据模块名来定位并执行脚本
2、对于包内模块,如果“-m”之后要执行的是一个包,那么解释器经过前面提到的查找过程,先定位到该包,然后会去执行它的“__main__
”子模块,也就是说,在包目录下需要实现一个“__main__
.py”文件。
换句话说,假设有个包的名称是“pname”,那么, “python -m pname”,其实就等效于“python -m pname.__main__
”。
仍以前文创建 HTTP 服务为例,“http”是 Python 内置的一个包,它没有“__main__.py”
文件,所以使用“-m”方式执行时,就会报错:No module named http.__main__
; ‘http’ is a package and cannot be directly executed。
作为对比,我们可以看看前文提到的 pip,它也是一个包,为什么“python -m pip”的方式可以使用呢?当然是因为它有“__main__
.py”文件:
“python -m pip”实际上执行的就是这个“__main__
.py”文件,它主要作为一个调用入口,调用了核心的”pip._internal.main”。
http 包因为没有一个统一的入口模块,所以采用了“python -m 包.模块”的方式,而 pip 包因为有统一的入口模块,所以加了一个“main.py”文件,最后只需要写“python -m 包”,简明直观。
-m
选项的十年演变过程
最早引入 -m 选项的是 Python 2.4 版本(2004年),当时功能还挺受限,只能作用于普通的内置模块(如 pdb 和 profile)。
随后,知名开发者 Nick Coghlan 提出的《PEP 338 – Executing modules as scripts 》把它的功能提升了一个台阶。这个 PEP 在 2004 年提出,最终实现在 2006 年的 2.5 版本。
(插个题外话:Nick Coghlan 是核心开发者中的核心之一,也是第一届指导委员会的五人成员之一。记得当初看材料,他是在 2005 年被选为核心开发者的,这时间与 PEP-338 的时间紧密贴合)
这个 PEP 的几个核心点是:
- 结合了 PEP-302 的新探针机制(new import hooks),提升了解释器查找包内模块的能力
- 结合了其它的导入机制(例如
zipimport
和冻结模块(frozen modules)),拓展了解释器查找模块的范围与精度 - 开发了新的
runpy.run_module(modulename)
来实现本功能,而不用修改 CPython 解释器,如此可方便移植到其它解释器
至此,-m 选项使得 Python 可以在所有的命名空间内定位到命令行中给定的模块。
2009 年,在 Python 3.1 版本中,只需给定包的名称,就能定位和运行它的“__main__
”子模块。2014 年,-m 扩展到支持命名空间包。
至此,经过十年的发展演变,-m 选项变得功能齐全,羽翼丰满。
最后,我们来个 ending 吧:-m 选项可能看似不起眼,但它绝对是最特别的选项之一,它使得在命令行中,使用内置模块、标准包与三方库时变得更轻松便利。有机会就多用一下吧,体会它带来的愉悦体验。
参考材料
https://docs.python.org/3.7/using/cmdline.html#cmdoption-m
https://snarky.ca/why-you-should-use-python-m-pip
https://www.python.org/dev/peps/pep-0338/
调试 Python 程序
pdb 调试 Python 程序
官方参考网站 The Python Debugger : https://docs.python.org/3/library/pdb.html
gdb 调试命令的使用及总结:https://blog.csdn.net/freeking101/article/details/54406982
使用 Pdb 调试 Python:https://segmentfault.com/a/1190000006628456增强的调试器,比如 IPython 的 ipdb 和 pdb++
模块专题
生成数据
Faker
Faker是一个Python库,主要用于生成各种类型的虚假数据,包括但不限于姓名、地址、电子邮件、电话号码、公司名称、身份证号码、信用卡信息等。Faker的目的是帮助数据科学家、开发人员和测试人员快速、高效地创建随机数据,以满足测试和演示的需要。
https://blog.csdn.net/qq_41130705/article/details/125204884
https://zhuanlan.zhihu.com/p/620757661
requests
numpy
NumPy documentation — NumPy v1.24 Manual
教程:
发展历史
NumPy是一个开源的Python科学计算库,能够直接对数组和矩阵进行操作,可以省略很多循环语句,众多的数学函数也会让编写代码的工作轻松许多。
安装方式:
(1)在终端输入:pip install numpy
(2) 第二种:conda install numpy
NumPy(Numerical Python) 是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库。
NumPy 的前身 Numeric 最早是由 Jim Hugunin 与其它协作者共同开发,2005 年,Travis Oliphant 在 Numeric 中结合了另一个同性质的程序库 Numarray 的特色,并加入了其它扩展而开发了 NumPy。NumPy 为开放源代码并且由许多协作者共同维护开发。
NumPy 是一个运行速度非常快的数学库,主要用于数组计算,包含:
- 一个强大的N维数组对象 ndarray
- 广播功能函数
- 整合 C/C++/Fortran 代码的工具
- 线性代数、傅里叶变换、随机数生成等功能
numpy应用
NumPy 通常与 SciPy(Scientific Python)和 Matplotlib(绘图库)一起使用, 这种组合广泛用于替代 MatLab,是一个强大的科学计算环境,有助于我们通过 Python 学习数据科学或者机器学习。
SciPy 是一个开源的 Python 算法库和数学工具包。
SciPy 包含的模块有最优化、线性代数、积分、插值、特殊函数、快速傅里叶变换、信号处理和图像处理、常微分方程求解和其他科学与工程中常用的计算。
Matplotlib 是 Python 编程语言及其数值数学扩展包 NumPy 的可视化操作界面。它为利用通用的图形用户界面工具包,如 Tkinter, wxPython, Qt 或 GTK+ 向应用程序嵌入式绘图提供了应用程序接口(API)。
numpy核心
多维数组
- 掌握到二维数组的处理,基本就够用了
- 能够用numpy去处理,之前用列表时处理的问题(用多维数组去替换列表使用)
代码简洁:减少python代码中的循环
底层实现:厚内核(C) + 薄接口(Python),保证性能
安装numpy
1 | pip3 install numpy scipy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple |
ndarray数组对象介绍
之前用列表保存一组数据,现在用ndarray数组保存一组数据
学习过程中,要比较和列表的差异以加深印象,如增删改查、切片等
用np.ndarray
类的对象,表示n维数组
1 | import numpy as np |
内存中的ndarray对象
元数据(metadata)
存储目标数组的描述信息,如:
dim
、count
、dimensions
、dtype
、data
等实际数据
- 完整的数组数据
- 将实际数据与元数据分开存放,一方面提高了内存空间的使用效率,另一方面减少对实际数据的访问频率,提高性能。
ndarray数组对象的特点
- numpy数组是同质数组,即所有元素的数据类型必须相同
- numpy数组的下标从0开始,最后一个元素的下标为数组长度减1
NumPy 最重要的一个特点是其 N 维数组对象 ndarray,它是一系列同类型数据的集合,以 0 下标为开始进行集合中元素的索引。
ndarray 对象是用于存放同类型元素的多维数组。
ndarray 中的每个元素在内存中都有相同存储大小的区域。
ndarray: N维数组。它是一系列同类型数据的集合,以0下标为开始进行集合中元素的索引。用于存放同类型元素的多维数组。
ndarray 中的每个元素在内存中都有相同存储大小的区域。
ndarray 内部由以下内容组成:
- 一个指向数据(内存或内存映射文件中的一块数据)的指针。
- 数据类型或 dtype,描述在数组中的固定大小值的格子。
- 一个表示数组形状(shape)的元组,表示各维度大小的元组。
- 一个跨度元组(stride),其中的整数指的是为了前进到当前维度下一个元素需要”跨过”的字节数。
ndarray 的内部结构:
跨度可以是负数,这样会使数组在内存中后向移动,切片中 obj[::-1] 或 obj[:,::-1] 就是如此。
创建一个 ndarray 只需调用 NumPy 的 array 函数即可:
1 | numpy.array(object, dtype = None, copy = True, order = None, subok = False, ndmin = 0) |
名称 | 描述 |
---|---|
object | 数组或嵌套的数列 |
dtype | 数组元素的数据类型,可选 |
copy | 对象是否需要复制,可选 |
order | 创建数组的样式,C为行方向,F为列方向,A为任意方向(默认) |
subok | 默认返回一个与基类类型一致的数组 |
ndmin | 指定生成数组的最小维度 |
1 | import numpy as np |
ndarray 对象由计算机内存的连续一维部分组成,并结合索引模式,将每个元素映射到内存块中的一个位置。内存块以行顺序(C样式)或列顺序(FORTRAN或MatLab风格,即前述的F样式)来保存元素。
ndarray创建
np.array
1
2import numpy as np
a = np.array([1,2,3,4])np.arange(起始值(0), 终止值, 步长)
1
2
3
4
5
6import numpy as np
a = np.arange(0, 5, 1)
print(a) # [0 1 2 3 4]
b = np.arange(0, 10, 2)
print(b) # [0 2 4 6 8]np.zeros(数组元素个数, dtype=’类型’)
1
2
3
4
5
6
7
8import numpy as np
a = np.zeros(10)
print(a)
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
b = np.zeros(10, dtype='int32')
print(b, b.dtype)
# [0 0 0 0 0 0 0 0 0 0] int32np.ones(数组元素个数, dtype=’类型’)
1
2
3
4
5
6
7
8
9
10
11
12import numpy as np
a = np.ones(10)
print(a)
# [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
b = np.ones((2, 3), dtype='float32')
print(b, b.shape, b.dtype)
# [[1. 1. 1.]
# [1. 1. 1.]] (2, 3) float32
print(np.ones(5) / 5)
# [0.2 0.2 0.2 0.2 0.2]扩展:
np.zeros_like() & np.ones_like() 生成维度一致的0或1数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import numpy as np
a = np.arange(0, 12, 2)
a.shape = (2, 3)
print(a)
# [[ 0 2 4]
# [ 6 8 10]]
print(np.zeros_like(a))
# [[0 0 0]
# [0 0 0]]
print(np.ones_like(a))
# [[1 1 1]
# [1 1 1]]
ndarray对象属性
数组的维度:np.ndarray.shape
1
2
3
4
5
6
7
8
9
10
11# 维度基础操作
import numpy as np
a = np.arange(1, 9)
print(a, a.shape)
# [1 2 3 4 5 6 7 8] (8,)
a.shape = (2, 4)
print(a, a.shape)
# [[1 2 3 4]
# [5 6 7 8]] (2, 4)元素的类型:np.ndarray.dtype
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 维度基础操作
import numpy as np
a = np.arange(1, 9)
print(a, a.dtype)
# [1 2 3 4 5 6 7 8] int64
# a.dtype = 'float32' # 直接操作的是内存,不是这么改的
# print(a, a.dtype)
# [1.4e-45 0.0e+00 2.8e-45 0.0e+00 4.2e-45 0.0e+00 5.6e-45 0.0e+00 7.0e-45
# 0.0e+00 8.4e-45 0.0e+00 9.8e-45 0.0e+00 1.1e-44 0.0e+00] float32
b = a.astype('float32') # 得到一个新的ndarray数组对象(a本身没变化)
print(b, b.dtype)
# [1. 2. 3. 4. 5. 6. 7. 8.] float32数组元素的个数:np.ndarray.size
1
2
3
4
5
6
7
8
9import numpy as np
a = np.arange(0, 12, 2)
a.shape = (2, 3)
# 观察shape、size和len的区别
print(a.shape) # (2, 3)
print(a.size) # 6
print(len(a)) # 2 可以理解为包含两个一维数组数组元素索引(下标)
数组对象[…, 页号, 行号, 列号]
下标从0开始,到数组len-1结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import numpy as np
a = np.arange(1, 19)
a.shape = (3, 2, 3) # (页 行 列) 可以理解为看书,看书的过程就是在扫描一个三维数组
print(a)
# [[[ 1 2 3]
# [ 4 5 6]]
# [[ 7 8 9]
# [10 11 12]]
# [[13 14 15]
# [16 17 18]]]
print(a[0])
# [[1 2 3]
# [4 5 6]]
print(a[0][1])
# [4 5 6]
print(a[0][1][0]) # 4
print(a[0, 1, 0]) # 4
print(a.shape) # (3, 2, 3) 通过shape属性,获取页数、行数、列数
for i in range(a.shape[0]):
for j in range(a.shape[1]):
for k in range(a.shape[2]):
print(a[i, j, k]) # 打印所有元素,多维数组的扁平迭代器
dtype:numpy内部基本数据类型
类型名 | 类型表示符 |
---|---|
布尔型 | bool_ |
有符号整型 | int(-128~127)/int16/int32/int64 |
无符号整型 | uint8(0~255)/uint16/uint32/uint64 |
浮点型 | float16/float32/float64 |
复数型 | complex64/complex128 |
字串型 | str_,每个字符用32位Unicode编码表示 |
自定义复合类型
1 | # 自定义复复合类型 |
日期数据类型
1 | import numpy as np |
类型字符码
数据类型简写
类型 | 字符码 |
---|---|
np.bool | ? |
np.int8/16/32/64 | i1/i2/i4/i8 |
np.uint8/16/32/64 | u1/u2/u4/u8 |
np.float/16/32/64 | f2/f4/f8 |
np.complex64/128 | c8/ c16 |
np.str_ | U<字符数> |
np.datetime64 | M8[Y] M8[M] M8[D] M8[h] M8[m] M8[s] |
字节序前缀, 用于多字节整数和字符串
</>/[=]
分别表示小端/大端/硬件字节序
类型字符码格式
<字节序前缀><维度><类型><字节数或字符数>
格式 | 释义 |
---|---|
3i4 | 大端字节序,3个元素的的一维数组,每个元素都是整型,每个整型元素占4个字节 |
<(2,3)u8 | 小端字节序,6个元素2行3列的二维数组,每个元素都是无符号整型,每个无符号整型元素占8个字节 |
U7 | 包含7个字符的Unicode字符串,每个字符占4个字节,采用默认字节序 |
shape:ndarray数组对象的维度操作
视图变维
数据共享:reshape()与revel()
1 |
pandas
工具类的api
1 | pip3 install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple |
样例
1 | import pandas as pd |
pandas介绍
python data analysis library
pandas是基于numpy的一种工具,该工具是为了解决数据分析任务而创建的。
pandas纳入了大量库和一些标准的数据模型,提供了高效操作大型结构化数据集(二维表)所需的工具。非结构化数据pandas不搞,如图片、音频等。
pandas核心数据结构
数据结构是计算机存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
数据结构 - Series
Pandas Series 类似表格中的一个列(column),类似于一维数组,只是index可以自己改动。类似于定长的有序字典,有index和value。可以保存任何数据类型。
Series提供了更多的维度,让我们可以更方便的操作一维数组(可以自定义索引标签名称,代码易读)
Series 由索引(index)和列组成,函数如下:
1 | pandas.Series( data, index, dtype, name, copy) |
参数说明:
- data:一组数据(ndarray 类型)。
- index:数据索引标签,如果不指定,默认从 0 开始。
- dtype:数据类型,默认会自己判断。
- name:设置名称。
- copy:拷贝数据,默认为 False。
创建一个简单的 Series 实例:
1 | import pandas as pd |
从上图可知,如果没有指定索引,索引值就从 0 开始,我们可以根据索引值读取数据:
1 | import pandas as pd |
我们可以指定索引值,如下实例:
1 | import pandas as pd |
根据索引值读取数据:
1 | import pandas as pd |
我们也可以使用 key/value 对象,类似字典来创建 Series:
1 | import pandas as pd |
从上图可知,字典的 key 变成了索引值。
如果我们只需要字典中的一部分数据,只需要指定需要数据的索引即可,如下实例:
1 | import pandas as pd |
设置 Series 名称参数:
1 | import pandas as pd |
完整样例及拓展
1 | import pandas as pd |
创建Series
1 | import numpy as np |
访问Series
1 | import pandas as pd |
日期处理
pd.to_datetime()
1 | import pandas as pd |
Series.dt提供了很多日期相关操作,使用时可以查阅相关文档
DateTimeIndex
通过指定周期和频率,使用date_range()函数就可以创建日期序列。默认情况下,范围的频率是天
数据结构 - DataFrame
DataFrame 是一个表格型的数据结构,它含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型值)。DataFrame 既有行索引也有列索引,它可以被看做由 Series 组成的字典(共同用一个索引)。
可以理解为一个二维数组,索引有两个维度,可更改。DataFrame具有以下特点:
- 潜在的列是不同的类型
- 大小可变
- 标记轴(行和列)
- 可以对行和列执行算术运算
DataFrame 构造方法如下:
1 | pandas.DataFrame( data, index, columns, dtype, copy) |
参数说明:
- data:一组数据(ndarray、series, map, lists, dict 等类型)。
- index:索引值,或者可以称为行标签。
- columns:列标签,默认为 RangeIndex (0, 1, 2, …, n) 。
- dtype:数据类型。
- copy:拷贝数据,默认为 False。
Pandas DataFrame 是一个二维的数组结构,类似二维数组。
1 | import pandas as pd |
以下实例使用 ndarrays 创建,ndarray 的长度必须相同, 如果传递了 index,则索引的长度应等于数组的长度。如果没有传递索引,则默认情况下,索引将是range(n),其中n是数组长度。
1 | import pandas as pd |
从以上输出结果可以知道, DataFrame 数据类型类似一个表格,包含 rows(行) 和 columns(列):
还可以使用字典(key/value),其中字典的 key 为列名:
1 | import pandas as pd |
没有对应的部分数据为 NaN。
Pandas 可以使用 loc 属性返回指定行的数据,如果没有设置索引,第一行索引为 0,第二行索引为 1,以此类推:
1 | import pandas as pd |
注意:返回结果其实就是一个 Pandas Series 数据。
也可以返回多行数据,使用 [[ … ]] 格式,**…** 为各行的索引,以逗号隔开:
1 | import pandas as pd |
注意:返回结果其实就是一个 Pandas DataFrame 数据。
我们可以指定索引值,如下实例:
1 | import pandas as pd |
1
2
3
4 calories duration
day1 420 50
day2 380 40
day3 390 45
Pandas 可以使用 loc 属性返回指定索引对应到某一行:
1 | import pandas as pd |
1
2
3 calories 380
duration 40
Name: day2, dtype: int64
创建DataFrame
1 | import pandas as pd |
操作DataFrame
列访问
DataFrame的单列数据为一个Series。根据DataFrame的定义可以知晓,DataFrame是一个带有标签的二维数组,每个标签相当每一列的列名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import pandas as pd
data = {'one': pd.Series([1, 2, 3], index=['a', 'b', 'c']), 'two': pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])}
df = pd.DataFrame(data)
print(df['one'])
# a 1.0
# b 2.0
# c 3.0
# d NaN
# Name: one, dtype: float64
print(df[['one', 'two']])
# one two
# a 1.0 1
# b 2.0 2
# c 3.0 3
# d NaN 4列添加
DataFrame添加一列的方法非常简单,只需要新建一个列索引。并对该索引下的数据进行赋值操作即可
1
2
3
4
5
6
7
8
9
10
11
12import pandas as pd
data = {'Name': ['Tom', 'Jack', 'Steve', 'Ricky'], 'Age': [28, 34, 28, 43]}
df = pd.DataFrame(data, index=['s1', 's2', 's3', 's4'])
# df['Score'] = pd.Series([90, 80, 70, 60], index=['s1', 's2', 's3', 's4'])
df['Score'] = pd.Series([90, 80, 70, 60], index=df.index)
print(df)
# Name Age Score
# s1 Tom 28 90
# s2 Jack 34 80
# s3 Steve 28 70
# s4 Ricky 43 60列删除
删除某列数据需要用到pandas提供的方法pop
注意,该方法影响原数据集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import pandas as pd
data = {'one': pd.Series([1, 2, 3], index=['a', 'b', 'c']), 'two': pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']), 'three': pd.Series([10, 20, 30], index=['a', 'b', 'c'])}
df = pd.DataFrame(data)
print(df)
# one two three
# a 1.0 1 10.0
# b 2.0 2 20.0
# c 3.0 3 30.0
# d NaN 4 NaN
# 删除一列
del(df['one'])
print(df)
# two three
# a 1 10.0
# b 2 20.0
# c 3 30.0
# d 4 NaN
# 调用pop方法删除一列
df.pop('two')
print(df)
# three
# a 10.0
# b 20.0
# c 30.0
# d NaN行访问
如果只是需要访问DataFrame某几行数据,实现方式则采用数组的选取方式(切片访问),使用
:
即可1
2
3
4
5
6
7
8import pandas as pd
data = {'one': pd.Series([1, 2, 3], index=['a', 'b', 'c']), 'two': pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']), 'three': pd.Series([10, 20, 30], index=['a', 'b', 'c'])}
df = pd.DataFrame(data)
print(df[2:4])
# one two three
# c 3.0 3 30.0
# d NaN 4 NaNloc方法是针对DataFrame索引名称的切片方法。loc方法使用方法如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import pandas as pd
data = {'one': pd.Series([1, 2, 3], index=['a', 'b', 'c']), 'two': pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']), 'three': pd.Series([10, 20, 30], index=['a', 'b', 'c'])}
df = pd.DataFrame(data)
print(df)
# one two three
# a 1.0 1 10.0
# b 2.0 2 20.0
# c 3.0 3 30.0
# d NaN 4 NaN
print(df.loc['b'])
# one 2.0
# two 2.0
# three 20.0
# Name: b, dtype: float64
print(df.loc[['a', 'c']])
# one two three
# a 1.0 1 10.0
# c 3.0 3 30.0iloc和loc区别是iloc接受的必须是行索引的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import pandas as pd
data = {'one': pd.Series([1, 2, 3], index=['a', 'b', 'c']), 'two': pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']), 'three': pd.Series([10, 20, 30], index=['a', 'b', 'c'])}
df = pd.DataFrame(data)
print(df)
# one two three
# a 1.0 1 10.0
# b 2.0 2 20.0
# c 3.0 3 30.0
# d NaN 4 NaN
print(df.iloc[2])
# one 3.0
# two 3.0
# three 30.0
# Name: c, dtype: float64
print(df.iloc[[2, 3]]) # 只支持行位置索引
# one two three
# c 3.0 3 30.0
# d NaN 4 NaN
print(df.iloc[1, 1]) # 少写一个方括号,返回的是具体的标量:2在Pandas中,
iloc
是一个用于通过整数位置进行数据选择和切片的方法。它允许你通过指定行和列的位置来访问和操作DataFrame或Series中的数据。iloc
的语法如下:1
2
df.iloc[row_indexer, column_indexer]其中,
row_indexer
和column_indexer
是用于指定行和列位置的整数、整数列表、切片对象或布尔数组。下面是一些常见的用法和示例:
- 选择特定行和列:
1
2
3
4
5
6
7
8# 选择第1行和第2列的数据
value = df.iloc[0, 1]
# 选择前3行和所有列的数据
subset = df.iloc[:3, :]
# 选择第1列的数据
column = df.iloc[:, 0]- 使用整数列表选择多行或多列:
1
2
3
4
5
6
7
8# 选择第1、3和5行的数据
rows = df.iloc[[0, 2, 4], :]
# 选择第0、2和4列的数据
columns = df.iloc[:, [0, 2, 4]]
# 选择第1、2和4行,第0和3列的数据
subset = df.iloc[[1, 2, 4], [0, 3]]- 使用切片对象进行切片选择:
1
2
3
4
5
6
7
8# 选择前5行的数据
subset = df.iloc[:5, :]
# 选择第3到第7行的数据
subset = df.iloc[2:7, :]
# 选择第2到第5列的数据
subset = df.iloc[:, 1:5]- 使用布尔数组进行条件选择:
1
2
3
4
5# 选择满足条件的行
subset = df.iloc[df['column'] > 5, :]
# 选择满足条件的列
subset = df.iloc[:, df.columns.str.contains('keyword')]通过使用
iloc
方法,你可以通过整数位置灵活地选择和操作DataFrame或Series中的数据,无论是选择特定的行和列,还是进行切片选择。行添加
方法一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import pandas as pd
# 创建一个空的DataFrame
df = pd.DataFrame(columns=['Column1', 'Column2', 'Column3'])
# 创建新行的数据
new_row = {'Column1': 1, 'Column2': 2, 'Column3': 3}
# 将新行添加到DataFrame
df = df.append(new_row, ignore_index=True)
# 输出DataFrame
print(df)方法二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import pandas as pd
df = pd.DataFrame([
['zs', 12],
['ls', 4],
], columns=['Name', 'Age'])
df2 = pd.DataFrame([
['ww', 16],
['zl', 8]
], columns=['Name', 'Age'])
# df = df.append(df2) 新版本弃用
df = pd.concat([df, df2], ignore_index=True)
print(df)
# Name Age
# 0 zs 12
# 1 ls 4
# 2 ww 16
# 3 zl 8行删除
使用索引标签从DataFrame中删除或删除行。如果标签重复,则会删除多行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import pandas as pd
df = pd.DataFrame([
['zs', 12],
['ls', 4],
], columns=['Name', 'Age'])
df2 = pd.DataFrame([
['ww', 16],
['zl', 8]
], columns=['Name', 'Age'])
df = pd.concat([df, df2], ignore_index=True)
# 删除index为0的行
df = df.drop(0)
print(df)
# Name Age
# 1 ls 4
# 2 ww 16
# 3 zl 8更改DataFrame中的数据,原理是将这部分数据提取出来,重新赋值为新的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import pandas as pd
df = pd.DataFrame([
['zs', 12],
['ls', 4],
], columns=['Name', 'Age'])
df2 = pd.DataFrame([
['ww', 16],
['zl', 8]
], columns=['Name', 'Age'])
df = pd.concat([df, df2], ignore_index=True)
df['Name'][0] = 'Tom'
print(df)
# Name Age
# 0 Tom 12
# 1 ls 4
# 2 ww 16
# 3 zl 8常用属性或方法
属性或方法 描述 axes 返回行/列 标签(index)列表 dtype 返回对象的数据类型 empty 如果序列为空,则返回True ndim 返回底层数据的维度, 默认定义:1 size 返回基础数据中的元素数 values 将序列作为ndarray返回 head() 返回前n行 tail() 返回最后n行 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import pandas as pd
df = pd.DataFrame([
['zs', 12],
['ls', 4],
], columns=['Name', 'Age'])
df2 = pd.DataFrame([
['ww', 16],
['zl', 8]
], columns=['Name', 'Age'])
df = pd.concat([df, df2], ignore_index=True)
print(df)
# Name Age
# 0 zs 12
# 1 ls 4
# 2 ww 16
# 3 zl 8
print(df.axes) # [RangeIndex(start=0, stop=4, step=1), Index(['Name', 'Age'], dtype='object')]
print(df.empty) # False
print(df.ndim) # 2
print(df.values)
# [['zs' 12]
# ['ls' 4]
# ['ww' 16]
# ['zl' 8]]
pandas核心
描述性统计
数值型的描述性统计,主要包括了计算数值型数据的完整情况、最小值、均值、中位数、最大值、四分位数、极差、标准差、方差、协方差等
在Numpy库中,一些常用的统计学函数也可用与对数据框(DataFrame)进行描述性统计
1 | np.min 最小值 |
1 | import pandas as pd |
pandas提供了统计相关函数
函数 | 含义 |
---|---|
count() | 非空观测数量(计数) |
sum() | 所有值之和 |
mean() | 所有值的平均值 |
median() | 所有值的中位数 |
std() | 值的标准偏差 |
min() | 所有值中的最小值 |
max() | 所有值中的最大值 |
abs() | 绝对值 |
prod() | 数组元素的乘积 |
cumsum() | 累计总和 |
cumprod() | 累计乘积 |
1 | print(df.count()) |
pandas还提供了一个方法叫做descrbibe,能够一次性得出DataFrame所有数值型特征的非空值数目、均值、标准差等
1 | import pandas as pd |
排序
pandas有两种排序,分别是按标签与按实际值排序
1 | import pandas as pd |
按行标签排序
使用sort_index()
方法,通过传递axis
参数和排序顺序,可以对DataFrame
进行排序。
默认情况下,按照升序对行标签进行排序
1 | import pandas as pd |
按某列值排序
像索引排序一样,sort_values
是按值排序的方法。
它接受一个by参数,它将使用要与其排序值的DataFrame的列名称。
1 | import pandas as pd |
分组
在许多情况下,我们将数据分成多个集合,并在每个子集上应用一些函数。
在应用函数中,可以执行以下操作:
- 聚合:计算汇总统计
- 转换:执行一些特定于组的操作
- 过滤:在某些情况下丢弃数据
1 | import pandas as pd |
将数据拆分成组
1 | # 按照年份Year字段分组 |
迭代遍历分组
groupby返回可迭代对象,可以使用for循环遍历(不常用):
1 | grouped = df.groupby('Year') |
获得一个分组细节
1 | grouped = df.groupby('Year') |
分组聚合
聚合函数为每个组返回聚合值。当创建了分组(group by)对象,就可以对每个分组数据执行求和、求标准差等操作
1 | grouped = df.groupby('Year') |
数据表关联操作
pandas具有功能全面的高性能内存中连接操作,与SQL等关系数据库非常相似。
pandas提供了一个单独的merge()函数,作为DataFrame对象之间所有标准数据库连接操作的入口。
1 | import pandas as pd |
使用how参数合并DataFrame:
1 | # 合并两个DataFrame(左连接) |
其他合并方法同数据库相同:
合并方法 | SQL等效 | 描述 |
---|---|---|
left | left outer join | 使用左侧对象的键 |
right | right outer join | 使用右侧对象的键 |
outer | full outer join | 使用键的联合 |
inner | inner join | 使用键的交集 |
如果两个表中,有字段相同,需要指定按照哪个字段去做关联
1 | rs = pd.merge(left, right, on='subject_id', how='right') |
透视表与交叉表
透视表(pivot table)是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具。它根据一个或多个键对数据进行分组聚合,并根据每个分组进行数据汇总。
1 | ``` |
to_string() 用于返回 DataFrame 类型的数据,如果不使用该函数,则输出结果为数据的前面 5 行和末尾 5 行,中间部分以 … 代替。
我们也可以使用 to_csv() 方法将 DataFrame 存储为 csv 文件:
1 | import pandas as pd |
head()
head( n ) 方法用于读取前面的 n 行,如果不填参数 n ,默认返回 5 行。
读取前面 10 行
1 | import pandas as pd |
tail()
tail( n ) 方法用于读取尾部的 n 行,如果不填参数 n ,默认返回 5 行,空行各个字段的值返回 NaN。
1 | import pandas as pd |
info()
info() 方法返回表格的一些基本信息:
1 | import pandas as pd |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <class 'pandas.core.frame.DataFrame'>
RangeIndex: 458 entries, 0 to 457 # 行数,458 行,第一行编号为 0
Data columns (total 9 columns): # 列数,9列
# Column Non-Null Count Dtype # 各列的数据类型
--- ------ -------------- -----
0 Name 457 non-null object
1 Team 457 non-null object
2 Number 457 non-null float64
3 Position 457 non-null object
4 Age 457 non-null float64
5 Height 457 non-null object
6 Weight 457 non-null float64
7 College 373 non-null object # non-null,意思为非空的数据
8 Salary 446 non-null float64
dtypes: float64(4), object(5) # 类型
non-null 为非空数据,我们可以看到上面的信息中,总共 458 行,College 字段的空值最多。
pandas处理JSON文件
JSON(JavaScript Object Notation,JavaScript 对象表示法),是存储和交换文本信息的语法,类似 XML。
JSON 比 XML 更小、更快,更易解析,更多 JSON 内容可以参考 JSON 教程。
Pandas 可以很方便的处理 JSON 数据,本文以 sites.json 为例,内容如下:
1 | [ |
1 | import pandas as pd |
to_string() 用于返回 DataFrame 类型的数据,我们也可以直接处理 JSON 字符串。
1 | import pandas as pd |
1
2
3
4 id name url likes
0 A001 菜鸟教程 www.runoob.com 61
1 A002 Google www.google.com 124
2 A003 淘宝 www.taobao.com 45
JSON 对象与 Python 字典具有相同的格式,所以我们可以直接将 Python 字典转化为 DataFrame 数据:
1 | import pandas as pd |
1
2
3
4 col1 col2
row1 1 x
row2 2 y
row3 3 z
从 URL 中读取 JSON 数据:
1 | import pandas as pd |
1
2
3
4 id name url likes
0 A001 菜鸟教程 www.runoob.com 61
1 A002 Google www.google.com 124
2 A003 淘宝 www.taobao.com 45
内嵌的 JSON 数据
假设有一组内嵌的 JSON 数据文件 nested_list.json :
1 | { |
1 | import pandas as pd |
1
2
3
4 school_name class students
0 ABC primary school Year 1 {'id': 'A001', 'name': 'Tom', 'math': 60, 'phy...
1 ABC primary school Year 1 {'id': 'A002', 'name': 'James', 'math': 89, 'p...
2 ABC primary school Year 1 {'id': 'A003', 'name': 'Jenny', 'math': 79, 'p...
这时我们就需要使用到 json_normalize() 方法将内嵌的数据完整的解析出来:
1 | import pandas as pd |
1
2
3
4 id name math physics chemistry
0 A001 Tom 60 66 61
1 A002 James 89 76 51
2 A003 Jenny 79 90 78
data = json.loads(f.read()) 使用 Python JSON 模块载入数据。
json_normalize() 使用了参数 record_path 并设置为 [‘students’] 用于展开内嵌的 JSON 数据 students。
显示结果还没有包含 school_name 和 class 元素,如果需要展示出来可以使用 meta 参数来显示这些元数据:
1 | import pandas as pd |
1
2
3
4 id name math physics chemistry school_name class
0 A001 Tom 60 66 61 ABC primary school Year 1
1 A002 James 89 76 51 ABC primary school Year 1
2 A003 Jenny 79 90 78 ABC primary school Year 1
接下来,让我们尝试读取更复杂的 JSON 数据,该数据嵌套了列表和字典,数据文件 nested_mix.json 如下:
1 | { |
nested_mix.json 文件转换为 DataFrame:
1 | import pandas as pd |
1
2
3
4 id name math physics chemistry class info.president info.contacts.tel
0 A001 Tom 60 66 61 Year 1 John Kasich 123456789
1 A002 James 89 76 51 Year 1 John Kasich 123456789
2 A003 Jenny 79 90 78 Year 1 John Kasich 123456789
读取内嵌数据中的一组数据
以下是实例文件 nested_deep.json,我们只读取内嵌中的 math 字段:
1 | { |
这里我们需要使用到 glom 模块来处理数据套嵌,glom 模块允许我们使用 . 来访问内嵌对象的属性。
第一次使用我们需要安装 glom:
1 | pip3 install glom -i https://pypi.tuna.tsinghua.edu.cn/simple |
1 | import pandas as pd |
1
2
3
4 0 60
1 89
2 79
Name: students, dtype: int64
pandas处理Excel文件
Excel读取
https://blog.csdn.net/weixin_43092663/article/details/124155354
读写Excel详解
Excel的读取可以采用ExcelFile类和read_excel两种方法,在实际使用中差别不大。其区别可以见e-learn上贴子讨论,观点摘录如下:
除了语法之外没有特别的区别。从技术上讲,ExcelFile是一个类,read_excel是一个函数。在任何一种情况下,实际都是由定义在ExcelFile的_parse_excel解析
ExcelFile.parse循环速度更快(存疑)
ExcelFile类
类的定义和方法参数感兴趣的可以查看源码
1 | ExcelFile.parse(sheet_name=0, header=0, names=None, index_col=None, usecols=None, squeeze=None, converters=None, true_values=None, false_values=None, skiprows=None, nrows=None, na_values=None, parse_dates=False, date_parser=None, thousands=None, comment=None, skipfooter=0, convert_float=None, mangle_dupe_cols=True, **kwds) |
1 | import pandas as pd |
输出结果如下:
read_excel()方法
1 | pandas.read_excel(io, sheet_name=0, header=0, names=None, index_col=None, usecols=None, squeeze=None, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, parse_dates=False, date_parser=None, thousands=None, decimal=’.’, comment=None, skipfooter=0, convert_float=None, mangle_dupe_cols=True, storage_options=None) |
示例如下
1 | ##可以逐个sheet读取,也可以一次读取 |
使用其他参数
1 | st3 = pd.read_excel('./data/table1.xlsx',sheet_name=1,dtype={'体重': int,'分数': float}) |
注意:对某一列同时使用dtype和converters,仅仅converter被有效使用
1 | df = pd.read_excel('./xdiwfjH98xxone (1).xlsx') |
Excel写入
写入Excel
有两种方法可以进行写入,使用to_excel方法和ExcelWriter类,如要写入到一个文件中多个sheet,需要使用ExcelWriter类。
ExcelWriter定义如下:
1 | class pandas.ExcelWriter(path, engine=None, date_format=None, datetime_format=None, mode=‘w’, storage_options=None, if_sheet_exists=None, engine_kwargs=None, **kwargs) |
to_excel方法:
1 | DataFrame.to_excel(excel_writer, sheet_name=‘Sheet1’, na_rep=’’, float_format=None, columns=None, header=True, index=True, index_label=None, startrow=0, startcol=0, engine=None, merge_cells=True, encoding=None, inf_rep=‘inf’, verbose=True, freeze_panes=None, storage_options=None) |
ExcelWriter()可以向同一个excel的不同sheet中写入对应的表格数据
代码如下(示例):
1 | #使用pd.to_excel方法 |
ExcelWriter类写入流程:基于已创建的writer对象,利用to_excel()方法将不同的dataframe及其对应的sheet名称写入该writer对象中,并在全部表格写入完成之后,使用save()方法来执行writer中内容向对应实体excel文件写入数据的过程
同python文件IO一样,有两种写入方式。writer应该作为上下文管理器,否则,需要调用close() 保持和关闭打开的文件句柄
第一种:
1 | writer = pd.ExcelWriter('./data/table3.xlsx') |
第二种:
1 | with pd.ExcelWriter('./data/table4.xlsx') as writer: |
已有Excel增加sheet
mode分为两种写入’w’和追加‘a’,在已有表格中增加sheet使用‘a’模式,指定写入的sheet名称,代码如下(示例):
1 | with pd.ExcelWriter('./data/table4.xlsx',mode='a',engine='openpyxl') as writer: |
覆盖Excel中已有sheet
如果需要重新写入Excel中某个sheet,直接往Excel写入同名sheet不会覆盖,而是创建一个新的表单(sheet1),需要采用以下方法
1 | with pd.ExcelWriter('./data/table4.xlsx',mode='a',engine='openpyxl') as writer: |
已有sheet中追加数据
读取原表格的Workbook和sheets,赋给writer句柄,保证数据内容的一致。获取拟写入表单的数据行数,从下一行开始写入,需要设置header = False
1 | import pandas as pd |
pandas数据清洗
数据清洗是对一些没有用的数据进行处理的过程。
很多数据集存在数据缺失、数据格式错误、错误数据或重复数据的情况,如果要对使数据分析更加准确,就需要对这些没有用的数据进行处理。
在这个教程中,我们将利用 Pandas包来进行数据清洗。
本文使用到的测试数据 property-data.csv 如下:
清洗空值
上表包含了四种空数据:
- n/a
- NA
- —
- na
如果我们要删除包含空字段的行,可以使用 dropna() 方法,语法格式如下:
1 | DataFrame.dropna(axis=0, how='any', thresh=None, subset=None, inplace=False) |
参数说明:
- axis:默认为 0,表示逢空值剔除整行,如果设置参数 axis=1 表示逢空值去掉整列。
- how:默认为 ‘any’ 如果一行(或一列)里任何一个数据有出现 NA 就去掉整行,如果设置 how=’all’ 一行(或列)都是 NA 才去掉这整行。
- thresh:设置需要多少非空值的数据才可以保留下来的。
- subset:设置想要检查的列。如果是多个列,可以使用列名的 list 作为参数。
- inplace:如果设置 True,将计算得到的值直接覆盖之前的值并返回 None,修改的是源数据。
我们可以通过 isnull() 判断各个单元格是否为空。
1 | import pandas as pd |
以上例子中我们看到 Pandas 把 n/a 和 NA 当作空数据,na 不是空数据,不符合我们要求,我们可以指定空数据类型:
1 | import pandas as pd |
接下来的实例演示了删除包含空数据的行。
1 | import pandas as pd |
注意:默认情况下,dropna() 方法返回一个新的 DataFrame,不会修改源数据。
如果你要修改源数据 DataFrame, 可以使用 inplace = True 参数:
1 | import pandas as pd |
我们也可以移除指定列有空值的行:
1 | # 移除 ST_NUM 列中字段值为空的行: |
我们也可以 fillna() 方法来替换一些空字段:
使用 12345 替换空字段:
1 | import pandas as pd |
我们也可以指定某一个列来替换数据:
使用 12345 替换 PID 为空数据:
1 | import pandas as pd |
替换空单元格的常用方法是计算列的均值、中位数值或众数。
Pandas使用 mean()、median() 和 mode() 方法计算列的均值(所有值加起来的平均值)、中位数值(排序后排在中间的数)和众数(出现频率最高的数)。
使用 mean() 方法计算列的均值并替换空单元格:
1 | import pandas as pd |
使用 median() 方法计算列的中位数并替换空单元格:
1 | import pandas as pd |
使用 mode() 方法计算列的众数并替换空单元格:
1 | import pandas as pd |
清洗格式错误数据
数据格式错误的单元格会使数据分析变得困难,甚至不可能。
我们可以通过包含空单元格的行,或者将列中的所有单元格转换为相同格式的数据。
以下实例会格式化日期:
1 | import pandas as pd |
1
2
3
4 Date duration
day1 2020-12-01 50
day2 2020-12-02 40
day3 2020-12-26 45
清洗错误数据
数据错误也是很常见的情况,我们可以对错误的数据进行替换或移除。
以下实例会替换错误年龄的数据:
1 | import pandas as pd |
将 age 大于 120 的设置为 120:
1 | import pandas as pd |
1
2
3 name age
0 Google 50
1 Runoob 40
清洗重复数据
如果我们要清洗重复数据,可以使用 duplicated() 和 drop_duplicates() 方法。
如果对应的数据是重复的,duplicated() 会返回 True,否则返回 False。
1 | import pandas as pd |
1
2
3
4
5 0 False
1 False
2 True
3 False
dtype: bool
删除重复数据,可以直接使用drop_duplicates() 方法。
1 | import pandas as pd |
1
2
3
4 name age
0 Google 50
1 Runoob 40
3 Taobao 23
pandas 常用函数
读取数据
函数 | 说明 |
---|---|
pd.read_csv(filename) | 读取 CSV 文件; |
pd.read_excel(filename) | 读取 Excel 文件; |
pd.read_sql(query, connection_object) | 从 SQL 数据库读取数据; |
pd.read_json(json_string) | 从 JSON 字符串中读取数据; |
pd.read_html(url) | 从 HTML 页面中读取数据。 |
1 | import pandas as pd |
pd.read_excel()
- 文件路径
- 读取引擎:engine,可以不指定,让pandas自己处理。xls:xlrd, xlsx:openpyxl
- 页:sheet_name,字符串或者数字
- 行索引:
- 默认pandas会给数据每一行的前面,加上索引:index_col=None。也可以手动指定:index_col=0。不建议多行索引:index_col=[0, 1]
- 手动指定:pd.set_index(‘Name’, inplace=True)
- 列索引
- 默认header=0,第一行为列索引
查看数据
函数 | 说明 |
---|---|
df.head(n) | 显示前 n 行数据; |
df.tail(n) | 显示后 n 行数据; |
df.info() | 显示数据的信息,包括列名、数据类型、缺失值等; |
df.describe() | 显示数据的基本统计信息,包括均值、方差、最大值、最小值等; |
df.shape | 显示数据的行数和列数。 |
1 | import pandas as pd |
1
2
3
4
5 name likes url
0 Google 25 https://www.google.com
1 Runoob 30 https://www.runoob.com
name likes url
2 Taobao 35 https://www.taobao.com
读取每一行
1 | import pandas as pd |
数据清洗
函数 | 说明 |
---|---|
df.dropna() | 删除包含缺失值的行或列; |
df.fillna(value) | 将缺失值替换为指定的值; |
df.replace(old_value, new_value) | 将指定值替换为新值; |
df.duplicated() | 检查是否有重复的数据; |
df.drop_duplicates() | 删除重复的数据。 |
df.drop_duplicates()
在一个数据集中,找出重复的数据删并将其删除,最终只保存一个唯一存在的数据项,这就是数据去重的整个过程。删除重复数据是数据分析中经常会遇到的一个问题。通过数据去重,不仅可以节省内存空间,提高写入性能,还可以提升数据集的精确度,使得数据集不受重复数据的影响。
drop_duplicates()函数的语法格式如下:
df.drop_duplicates(subset=['A','B','C'],keep='first',inplace=True)
参数说明如下:
subset:表示要进去重的列名,默认为 None。
keep:有三个可选参数,分别是 first、last、False,默认为 first,表示只保留第一次出现的重复项,删除其余重复项,last 表示只保留最后一次出现的重复项,False 则表示删除所有重复项。
inplace:布尔值参数,默认为 False 表示删除重复项后返回一个副本,若为 Ture 则表示直接在原数据上删除重复项。
1 | import pandas as pd |
1 | 1) 默认保留第一次出现的重复项 |
根据指定标签去重
1 | #去除所有重复项,对于B列来说两个0是重复项 |
从上述示例可以看出,删除重复项后,行标签使用的数字是原来的,并没有从 0 重新开始,那么我们应该怎么从 0 重置索引呢?Pandas 提供的 reset_index()
函数会直接使用重置后的索引。如下所示:
1 | #重置索引,从0重新开始 df.reset_index(drop=True) |
指定多列同时去重
1 | #last只保留最后一个重复项 |
上述数据集中,第 0 行、第 3 行对应的列标签数据相同,我们使用参数值“last”保留最后一个重复项,也就是第 3 行数据。
数据选择和切片
函数 | 说明 |
---|---|
df[column_name] | 选择指定的列; |
df.loc[row_index, column_name] | 通过标签选择数据; |
df.iloc[row_index, column_index] | 通过位置选择数据; |
df.ix[row_index, column_name] | 通过标签或位置选择数据; |
df.filter(items=[column_name1, column_name2]) | 选择指定的列; |
df.filter(regex=’regex’) | 选择列名匹配正则表达式的列; |
df.sample(n) | 随机选择 n 行数据。 |
数据排序
函数 | 说明 |
---|---|
df.sort_values(column_name) | 按照指定列的值排序; |
df.sort_values([column_name1, column_name2], ascending=[True, False]) | 按照多个列的值排序; |
df.sort_index() | 按照索引排序。 |
Pands 提供了两种排序方法,分别是按标签排序和按数值排序。
1 | import pandas as pd |
上述示例,行标签和数值元素均未排序,下面分别使用标签排序、数值排序对其进行操作。
df.sort_index()
按行标签排序
使用 sort_index() 方法对行标签排序,指定轴参数(axis)或者排序顺序。或者可以对 DataFrame 进行排序。默认情况下,按照行标签序排序。
1 |
|
按列标签排序
通过给 axis 轴参数传递 0 或 1,可以对列标签进行排序。默认情况下,axis=0 表示按行排序;而 axis=1 则表示按列排序。
1 | sorted_df=unsorted_df.sort_index(axis=1) |
df.sort_values()
按值排序,接受一个by参数,该参数值是要排序数列的 DataFrame 列名。
1 | import pandas as pd |
排序算法
sort_values() 提供了参数kind
用来指定排序算法。这里有三种排序算法:
- mergesort
- heapsort
- quicksort
默认为 quicksort(快速排序) ,其中 Mergesort 归并排序是最稳定的算法。
1 | sorted_df = unsorted_df.sort_values(by='col1' ,kind='mergesort') |
数据分组和聚合
函数 | 说明 |
---|---|
df.groupby(column_name) | 按照指定列进行分组; |
df.aggregate(function_name) | 对分组后的数据进行聚合操作; |
df.pivot_table(values, index, columns, aggfunc) | 生成透视表。 |
新增行
使用loc(len(df))新增行
1 | with open('./project.json', 'r', encoding='utf-8') as fr: |
数据合并
函数 | 说明 |
---|---|
pd.concat([df1, df2]) | 将多个数据框按照行或列进行合并; |
pd.merge(df1, df2, on=column_name) | 按照指定列将两个数据框进行合并。 |
pd.concat
pandas.concat
是一个用于合并(连接)pandas对象的函数,可以将多个DataFrame或Series对象沿指定轴进行连接。它提供了灵活的选项,可根据需求进行行或列的连接。
以下是pandas.concat
函数的基本语法:
1 | pandas.concat(objs, axis=0, join='outer', ignore_index=False) |
参数说明:
objs
: 要连接的DataFrame或Series对象的序列(例如列表、元组、字典)。axis
: 连接的轴方向。默认为0,表示按行连接;设置为1时表示按列连接。join
: 连接的方式。默认为’outer’,表示使用并集连接;设置为’inner’时表示使用交集连接。ignore_index
: 是否忽略原始索引,生成新的索引。默认为False。
下面是几个使用pandas.concat
函数的示例:
- 行连接多个DataFrame:
1 | import pandas as pd |
输出:
1 | A B |
- 列连接多个DataFrame:
1 | import pandas as pd |
输出:
1 | A B C D |
- 忽略原始索引,生成新的索引:
1 | import pandas as pd |
输出:
1 | 0 1 |
以上是pandas.concat
函数的基本用法,它可以在数据处理和数据合并的过程中起到很大的作用。你可以根据实际需求使用不同的参数组合来完成连接操作。
数据选择和过滤
函数 | 说明 |
---|---|
df.loc[row_indexer, column_indexer] | 按标签选择行和列。 |
df.iloc[row_indexer, column_indexer] | 按位置选择行和列。 |
df[df[‘column_name’] > value] | 选择列中满足条件的行。 |
df.query(‘column_name > value’) | 使用字符串表达式选择列中满足条件的行。 |
数据统计和描述
df.describe() | 计算基本统计信息,如均值、标准差、最小值、最大值等。 |
---|---|
df.mean() | 计算每列的平均值。 |
df.median() | 计算每列的中位数。 |
df.mode() | 计算每列的众数。 |
df.count() | 计算每列非缺失值的数量。 |
数据分组并应用函数
在Pandas中,groupby()
方法用于对数据进行分组,并返回一个GroupBy
对象。GroupBy
对象允许你在每个组上应用函数或方法,其中包括使用apply()
方法。
要使用apply()
方法,你需要首先创建一个函数,该函数将应用于每个组。然后,你可以通过apply()
方法将该函数应用于GroupBy
对象。
以下是一个示例,演示如何使用groupby()
和apply()
方法:
1 | import pandas as pd |
输出结果将是每个组的平均分数:
1 | Name |
在这个例子中,我们按照姓名(Name)对数据进行分组,并定义了一个名为calculate_average
的函数,用于计算每个组的平均分数。然后,我们使用apply()
方法将该函数应用于GroupBy
对象,并将结果存储在result
中。最后,我们打印出result
来显示每个组的平均分数。
备注:直接打印分组对象,是看不到实际数据的,可以通过print(grouped.apply(lambda x: x))
来查看,也可以将结果写到excel中,更直观。也可以用grouped.values
方法
计算唯一值数量
在Pandas中,nunique()
是一个用于计算唯一值数量的方法。它用于统计一个Series或DataFrame中不同值的个数,忽略缺失值(NaN)。
nunique()
的语法如下:
- 对于Series对象:
series.nunique(dropna=True)
- 对于DataFrame对象:
df.nunique(axis=0, dropna=True)
其中,参数说明如下:
dropna
(可选):布尔值,表示是否在计算唯一值数量时忽略缺失值。默认值为True
,即忽略缺失值。
下面是一些示例,演示了如何使用nunique()
方法:
- 对于Series对象:
1 | import pandas as pd |
在这个例子中,Series对象s
包含10个元素,其中有5个不同的值。使用nunique()
方法计算唯一值的数量,结果为5。
- 对于DataFrame对象:
1 | import pandas as pd |
在这个例子中,DataFrame对象df
包含3列,每列包含5个元素。使用nunique()
方法计算每列的唯一值数量,结果将是一个Series对象,其中包含每列的唯一值数量。输出结果将是:
1 | A 4 |
结果显示,列”A”和”B”都有4个不同的值,而列”C”只有1个不同的值。
nunique()
方法对于数据的探索和统计非常有用,特别是在数据预处理和数据分析过程中,用于了解数据中的唯一值数量和数据分布情况。
求和
在Pandas中,sum()
是一个用于计算序列或数据框中数值列的总和的方法。它可以应用于Series对象或DataFrame对象。
sum()
的语法如下:
- 对于Series对象:
series.sum()
- 对于DataFrame对象:
df.sum(axis=0, skipna=None)
其中,参数说明如下:
axis
(可选):用于指定计算总和的轴。默认值为0,表示沿着列进行计算,将每列的值相加得到总和。如果axis=1
,则表示沿着行进行计算,将每行的值相加得到总和。skipna
(可选):布尔值,表示是否忽略缺失值(NaN)。默认值为None
,表示根据对象类型和输入数据的缺失值处理策略来确定是否忽略缺失值。当skipna=True
时,将忽略缺失值;当skipna=False
时,缺失值将被视为0。
下面是一些示例,演示了如何使用sum()
方法:
- 对于Series对象:
1 | import pandas as pd |
在这个例子中,Series对象s
包含5个元素,使用sum()
方法计算这些元素的总和,结果为15。
- 对于DataFrame对象:
1 | import pandas as pd |
在这个例子中,DataFrame对象df
包含3列,每列包含4个元素。使用sum()
方法计算每列的总和,结果将是一个Series对象,其中包含每列的总和。输出结果将是:
1 | A 10 |
结果显示,列”A”的总和为10,列”B”的总和为14,列”C”的总和为18。
注意,sum()
方法在将布尔值True
视为1,而布尔值False
视为0,因此计算总和实际上就是计算True
值的数量。如果是计算整体准确率,可以先将数组扁平化。
按行求和
1 | import pandas as pd |
输出将会有两个部分。第一部分是每一行中True
的数量,即每行中除去第一列的True
值的总数。第二部分是每一行中True
的占比,即每行中除去第一列的True
值的总数除以总列数。
注意,sum()
方法的axis
参数设置为1
,表示按行进行求和。df.iloc[:, 1:]
用于选择除去第一列的所有列,即从第二列开始计算。
另外,使用df.shape[1]
可以获取DataFrame的总列数。这里假设第一列是索引列,所以通过df.shape[1]
获取总列数减去1,即不包括索引列。
请注意,输出的结果是每一行的统计结果,其中每一行对应输入数据中的一行。
写入数据
1 | # 写入excel |
去重
读取某一列后,使用drop_duplicates()
方法
其他
数组扁平化:
dataFrame.flatten()
,可以将数组降维对一个dataFrame调用len()方法:
在Pandas中,对DataFrame对象调用
len()
方法将返回DataFrame的行数。len()
函数计算对象的长度,对于DataFrame对象,它返回的是行数。1
2
3
4
5
6
7
8
9
10import pandas as pd
data = {'A': [1, 2, 3],
'B': [4, 5, 6],
'C': [7, 8, 9]}
df = pd.DataFrame(data)
length = len(df)
print(length)在这个例子中,DataFrame
df
包含3行数据。通过对df
调用len()
方法,我们获得DataFrame的行数,输出结果为3。请注意,
len()
方法返回的是DataFrame的行数,并不包括列数。如果你想获取DataFrame的列数,可以使用len(df.columns)
。
行遍历
sys
sys.path
详细解释:Python sys.path详解
1 | # test.py |
进入pkg_test
目录,直接执行python pkg_b/test.py
可以注释掉导包命令,看下sys.path
1 | # from pkg_a import module_a |
是把当前脚本所在的路径,放入了sys.path
,自然是找不到pkg_a这个兄弟的包的,而python -m
允许将模块以脚本的形式运行,并且将当前目录加入到了sys.path
中(父级目录),当然可以根据/app/tag/utils/pkg_test
路径找到对应的模块
导入一个叫 mod1 的模块时,解释器先在当前目录中搜索名为 mod1.py 的文件。如果没有找到的话,接着会到 sys.path 变量中给出的目录列表中查找。 sys.path 变量的初始值来自如下:
- 输入脚本的目录(当前目录)。
- 环境变量 PYTHONPATH 表示的目录列表中搜索(这和 shell 变量 PATH 具有一样的语法,即一系列目录名的列表)。
- Python 默认安装路径中搜索。
实际上,解释器由 sys.path 变量指定的路径目录搜索模块,该变量初始化时默认包含了输入脚本(或者当前目录), PYTHONPATH 和安装目录。这样就允许 Python程序了解如何修改或替换模块搜索目录。
sys.path
写入
1 | print(type(sys.path)) # <class 'list'> |
append
如果在当前目录执行,需要导入兄弟包中的模块
1 | import sys, os |
insert
PYTHONPATH 与 sys.path的关系
1 | (mumu-store-server-env) [~/vhost/nemu-statistics]$ echo $PYTHONPATH |
默认情况下PYTHONPATH是空的, 然后进去看到sys.path是一个列表,包括有所有查找包的目录
下面我们给PYTHON加个目录
1 | (mumu-store-server-env) [~/vhost/nemu-statistics]$ export PYTHONPATH=/home/lidongwei/Desktop |
可以看到’/home/lidongwei/Desktop
‘ 这个目录已经被加到sys.path了, 说明PYTHONPATH会加到sys.path
通用操作系统服务
os – 多种操作系统接口
os.walk
os.walk(top, topdown=True, onerror=None, followlinks=False)
生成目录树中的文件名,方式是按上->下或下->上顺序浏览目录树。对于以 top 为根的目录树中的每个目录(包括 top 本身),它都会生成一个三元组 (dirpath, dirnames, filenames)
。
dirpath is a string, the path to the directory. dirnames is a list of the names of the subdirectories in dirpath (including symlinks to directories, and excluding '.'
and '..'
). filenames is a list of the names of the non-directory files in dirpath. Note that the names in the lists contain no path components. To get a full path (which begins with top) to a file or directory in dirpath, do os.path.join(dirpath, name)
. Whether or not the lists are sorted depends on the file system. If a file is removed from or added to the dirpath directory during generating the lists, whether a name for that file be included is unspecified.
如果可选参数 topdown 为 True
或未指定,则在所有子目录的三元组之前生成父目录的三元组(目录是自上而下生成的)。如果 topdown 为 False
,则在所有子目录的三元组生成之后再生成父目录的三元组(目录是自下而上生成的)。无论 topdown 为何值,在生成目录及其子目录的元组之前,都将检索全部子目录列表。
当 topdown 为 True
时,调用者可以就地修改 dirnames 列表(也许用到了 del
或切片),而 walk()
将仅仅递归到仍保留在 dirnames 中的子目录内。这可用于减少搜索、加入特定的访问顺序,甚至可在继续 walk()
之前告知 walk()
由调用者新建或重命名的目录的信息。当 topdown 为 False
时,修改 dirnames 对 walk 的行为没有影响,因为在自下而上模式中,dirnames 中的目录是在 dirpath 本身之前生成的。
默认将忽略 scandir()
调用中的错误。如果指定了可选参数 onerror,它应该是一个函数。出错时它会被调用,参数是一个 OSError
实例。它可以报告错误然后继续遍历,或者抛出异常然后中止遍历。注意,可以从异常对象的 filename
属性中获取出错的文件名。
walk()
默认不会递归进指向目录的符号链接。可以在支持符号链接的系统上将 followlinks 设置为 True
,以访问符号链接指向的目录。
备注
注意,如果链接指向自身的父目录,则将 followlinks 设置为
True
可能导致无限递归。walk()
不会记录它已经访问过的目录。如果传入的是相对路径,请不要在恢复
walk()
之间更改当前工作目录。walk()
不会更改当前目录,并假定其调用者也不会更改当前目录
返回值是一个可迭代对象
1 | import os |
迭代打印返回值
1 | import os |
os.mkdir
https://blog.csdn.net/weixin_43781229/article/details/112424564
os.mkdir(path, mode=0o777, *, dir_fd=None)
创建一个名为 path 的目录,应用以数字表示的权限模式 mode。
创建一级目录,其参数path 为要创建目录的路径。
如果目录已经存在, FileExistsError 会被提出。如果路径中的父目录不存在,则会引发 FileNotFoundError 。
某些系统会忽略 mode。如果没有忽略它,那么将首先从它中减去当前的 umask 值。如果除最后 9 位(即 mode 八进制的最后 3 位)之外,还设置了其他位,则其他位的含义取决于各个平台。在某些平台上,它们会被忽略,应显式调用 chmod() 进行设置。
本函数支持 基于目录描述符的相对路径。
如果需要创建临时目录,请参阅 tempfile 模块中的 tempfile.mkdtemp() 函数。
引发一个 审计事件 os.mkdir,附带参数 path、mode、dir_fd。
创建目录前,通常需要判断该目录是否存在
1 | if not os.path.isdir(filename + "_dir"): |
os.chdir
os.chdir(path)
用于改变当前工作目录到指定的路径。
1 | #!/usr/bin/python |
结果
1 | 当前工作目录为 /www |
os.listdir
1 | # 获取文件夹内的文件和子文件夹名称 |
文件和目录访问
os.path – 常用路径操作
将脚本所在父级目录添加到运行路径
os.path
中(要求:在子目录中运行,不要在添加后,跑到父级目录运行,这样加的路径也随着执行目录而变化)1
2import sys, os
sys.path.append(os.path.realpath('..'))
os.path.realpath
os.path.realpath(path, *, strict=False)
传入相对路径,不传参或者传一个点
.
,结果是执行脚本时所在的实际路径返回指定文件的规范路径,消除路径中存在的任何符号链接(如果操作系统支持)。
如果一个路径不存在或是遇到了符号链接循环,并且 strict 为
True
,则会引发OSError
。 如果 strict 为False
,则会尽可能地解析路径并添加结果而不检查路径是否存在。备注
这个函数会模拟操作系统生成规范路径的过程,Windows 与 UNIX 的这个过程在处理链接和后续路径组成部分的交互方式上有所差异。
操作系统 API 会根据需要来规范化路径,因此通常不需要调用此函数。
os.path.relpath
os.path.relpath(path, start=os.curdir)
传入绝对路径,返回相当于当前执行目录的一个绝对路径
返回从当前目录或可选的 start 目录至 path 的相对文件路径。 这只是一个路径计算:不会访问文件系统来确认 path 或 start 是否存在或其性质。 在 Windows 上,当 path 和 start 位于不同驱动器时将引发
ValueError
。start 默认为
os.curdir
。可用性: Unix, Windows。
os.path.join
os.path.join(path, *paths)
Join one or more path segments intelligently. The return value is the concatenation of path and all members of *paths, with exactly one directory separator following each non-empty part, except the last. That is, the result will only end in a separator if the last part is either empty or ends in a separator. If a segment is an absolute path (which on Windows requires both a drive and a root), then all previous segments are ignored and joining continues from the absolute path segment.
On Windows, the drive is not reset when a rooted path segment (e.g.,
r'\foo'
) is encountered. If a segment is on a different drive or is an absolute path, all previous segments are ignored and the drive is reset. Note that since there is a current directory for each drive,os.path.join("c:", "foo")
represents a path relative to the current directory on driveC:
(c:foo
), notc:\foo
.连接两个或更多的路径名组件:https://blog.csdn.net/Ybc_csdn/article/details/124508003
- 如果各组件名首字母不包含’/’,则函数会自动加上
- 如果有一个组件是一个绝对路径,则在它之前的所有组件均会被舍弃
- 如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
- 可以拼接路径,也可以拼接路径+文件
demo01
1
2
3
4
5
6
7
8
9
10
11
12
13import os
Path1 = 'home'
Path2 = 'develop'
Path3 = 'code'
Path10 = Path1 + Path2 + Path3
Path20 = os.path.join(Path1,Path2,Path3)
print ('Path10 = ',Path10)
print ('Path20 = ',Path20)
# Path10 = homedevelopcode
# Path20 = home/develop/codedemo02
1
2
3
4
5
6
7
8
9
10
11
12
13
14import os
Path1 = '/home'
Path2 = 'develop'
Path3 = 'code'
Path10 = Path1 + Path2 + Path3
Path20 = os.path.join(Path1,Path2,Path3)
print ('Path10 = ',Path10)
print ('Path20 = ',Path20)
# Path10 = /homedevelopcode
# Path20 = /home/develop/codedemo03
1
2
3
4
5
6
7
8
9
10
11
12
13
14import os
Path1 = 'home'
Path2 = '/develop'
Path3 = 'code'
Path10 = Path1 + Path2 + Path3
Path20 = os.path.join(Path1,Path2,Path3)
print ('Path10 = ',Path10)
print ('Path20 = ',Path20)
# Path10 = home/developcode
# Path20 = /develop/codedemo04
1
2
3
4
5
6
7
8
9
10
11
12
13
14import os
Path1 = 'home'
Path2 = 'develop'
Path3 = '/code'
Path10 = Path1 + Path2 + Path3
Path20 = os.path.join(Path1,Path2,Path3)
print ('Path10 = ',Path10)
print ('Path20 = ',Path20 )
# Path10 = homedevelop/code
# Path20 = /code实际应用
1
2
3
4
5
6video_path = "./data/testvideo1.mp4"
label_path = "./data/labels"
file_name = "testvideo1"
label_file_path = os.path.join(label_path, file_name + "_" + str(frame_counter) + ".txt")
os.path.getsize
os.path.getsize()
os.path.basename
os.path.basename(path)
返回路径 path 的基本名称。这是将 path 传入函数
split()
之后,返回的一对值中的第二个元素。请注意,此函数的结果与Unix basename 程序不同。basename 在'/foo/bar/'
上返回'bar'
,而basename()
函数返回一个空字符串 (''
)。1
2
3print(os.path.basename('/app/tag/utils/test.py')) # test.py
print(os.path.basename('/app/tag/utils')) # utils
print(os.path.basename('/app/tag/utils/')) # '' 空字符串
os.path.isDir
os.path.isdir(path)
如果 path 是 现有的 目录,则返回 True。本方法会跟踪符号链接,因此,对于同一路径,islink() 和 isdir() 都可能为 True。
tempfile – 生成临时文件和目录
https://blog.csdn.net/weixin_37926734/article/details/123563067
Python的
tempfile
模块是用来创建临时文件或者文件夹的跨平台工具。在大型数据处理项目中,有的处理结果是不需要向用户最终展示的,但是它们的应用又是贯穿项目始终的,在这种情况下,我们就需要使用tempfile
模块来解决这种问题。
创建临时目录
TemporaryDirectory
TemporaryDirectory函数使用与mkdtemp()相同的规则安全地创建临时目录。生成的对象可以用作上下文管理器(这里给出示例)。完成上下文或销毁临时目录对象后,新创建的临时目录及其所有内容将从文件系统中删除。其调用格式如下所示:
1 | TemporaryDirectory(suffix=None, prefix=None, dir=None) |
调用该函数后,创建的目录名可以从返回对象的name属性中检索到。当返回的对象作为上下文管理器时,该名称将被分配给with语句中as子句的目标。另外,可以通过调用cleanup()方法显式清理目标。
1 | with tempfile.TemporaryDirectory() as path: |
glob – Unix风格路径名模式扩展
https://blog.csdn.net/weixin_41261833/article/details/108069945
glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
用来查找符合特定规则
的目录和文件,并将搜索的到的结果返回到一个列表中。使用这个模块最主要的原因就是,该模块支持
几个特殊的正则通配符
- glob.glob():返回符合匹配条件的所有文件的路径;
- glob.iglob():返回一个迭代器对象,需要循环遍历获取每个元素,得到的也是符合匹配条件的所有文件的路径;
- glob.escape():escape可以忽略所有的特殊字符,就是星号、问号、中括号,用处不大;
- recursive=False:代表递归调用,与特殊通配符“**”一同使用,默认为False,False表示不递归调用,True表示递归调用;
glob()函数
1 | path1 = r"C:\Users\黄伟\Desktop\publish\os模块\test_shutil_a\[0-9].png" |
iglob()函数
1 | path1 = r"C:\Users\黄伟\Desktop\publish\os模块\test_shutil_a\[0-9].png" |
escape()函数
通过下方两行代码的对比,可以看出escape()函数只是让*
只表示它本来的意思,而不再具有通配符的作用。
1 | glob.glob('t*') |
我们还需要注意一点,os库
、shutil库
、glob库
是互补的,我们要善于发挥各自的优势,充分利用它们的优势,帮助我们快速的操作文件和文件夹。
shutil – 高阶文件操作
https://docs.python.org/zh-cn/3/library/shutil.html#module-shutil
shutil
模块提供了一系列对文件和文件集合的高阶操作。 特别是提供了一些支持文件拷贝和删除的函数。 对于单个文件的操作,请参阅 os
模块。
警告
即便是高阶文件拷贝函数 (
shutil.copy()
,shutil.copy2()
) 也无法拷贝所有的文件元数据。在 POSIX 平台上,这意味着将丢失文件所有者和组以及 ACL 数据。 在 Mac OS 上,资源钩子和其他元数据不被使用。 这意味着将丢失这些资源并且文件类型和创建者代码将不正确。 在 Windows 上,将不会拷贝文件所有者、ACL 和替代数据流。
shutil.copy
复制文件
1 | #复制hello.txt到"C:\myweb\chapter02"目录下 |
复制文件夹
1 | #复制"C:\myweb\chapter01"到"C:\myweb\chapter02"目录下 |
json
常见json格式
1 | [{}, {}] |
读取
将文件对象,读取成python对象,使用load
1 | # 读取json文件 |
将字符串,读取成pytho对象,使用loads
1 | # 读取json文件 |
写入
memory_profiler
reportlab
官方文档:https://docs.reportlab.com/reportlab/userguide/ch1_intro/
模块代码示例:https://vimsky.com/examples/detail/python-module-reportlab.platypus.html
reportlab.platypus.Paragraph() ,22个项目使用
reportlab.platypus.Table() ,13个项目使用
reportlab.platypus.TableStyle() ,13个项目使用
reportlab.platypus.Image() ,13个项目使用
reportlab.platypus.Spacer() ,11个项目使用
reportlab.platypus.PageBreak() ,10个项目使用
reportlab.platypus.Frame() ,5个项目使用
基本用法
https://blog.csdn.net/bocai_xiaodaidai/article/details/102820431
reportlab文档:https://www.reportlab.com/docs/reportlab-userguide.pdf
reportlab实例:https://www.programcreek.com/python/index/1920/reportlab.platypus
reportlab生成pdf文档的基本途径有三种:
1、利用reportlab.pdfgen.canvas模块
pdfgen包是生成PDF文档的最低级别接口。pdfgen程序本质上是将文档“绘制”到页面序列上的指令序列。对象的接口 提供绘画操作的是pdfgen画布。 画布应该被认为是一张白纸,纸上的点用笛卡尔坐标来标识 (X,Y)坐标,默认在页面的左下角有(0,0)原点。此外,默认情况下,第一个坐标x向右,第二个坐标y向上。
2、利用reportlab.platypus.Paragraph模块
Paragraph是最有用最方便的途径之一。就像word一样操作每个段落。它可以格式化任意的文本,并提供使用XML样式的内联字体样式和颜色变化标记。格式化文本的整体形状可以调整为右对齐、左对齐、不规则或居中。XML 标记甚至可以用来插入希腊字符或做下标。
3、使用RML标记语言
类似html语法一样编写RML文档。
可以先看第二种
reportlab.pdfgen.canvas
ReportLab是一个非常强大的库。 稍加努力,您几乎可以想到任何布局。 多年来,我一直使用它来复制许多复杂的页面布局。在本文中,我们将学习如何使用ReportLab的pdfgen软件包。
pdfgen软件包的级别很低。 您将在画布上绘画或“涂画”以创建PDF。 画布是从pdfgen包中导入的。 当您在画布上绘画时,您将需要指定X / Y坐标,以告诉ReportLab从哪里开始绘画。 默认值为(0,0),其原点位于页面的左下角。 许多桌面用户界面工具包,例如wxPython,Tkinter等,也具有此概念。 您也可以使用X / Y坐标将按钮绝对放置在许多工具包中。 这样可以非常精确地放置要添加到页面中的元素。
我需要提及的另一项内容是,当您在PDF中放置某项时,是根据您从原点开始的点数进行定位。 它是点,而不是像素,毫米或英寸。 点! 让我们来看看一个字母大小的页面上有多少个点:
1 | from reportlab.lib.pagesizes import letter |
在这里,我们了解到一个letter宽612点,高792点。 (letter是美国的版面大小约定)让我们找出英寸和毫米分别有多少个点:(这些在reportlab.lib.pagesizes.py
以及同级的units.py
文件中定义)
1 | from reportlab.lib.units import inch |
这些信息将帮助我们将图纸放置在您的画上。 至此,我们已经准备好创建PDF!
canvas对象位于pdfgen包中。 让我们导入它并绘制一些文本:
1 | # hello_reportlab.py |
在此示例中,我们导入canvas对象,然后实例化Canvas对象。 您会注意到,唯一的要求参数是文件名或路径。 接下来,我们在画布对象上调用drawString(),并告诉它开始在原点右边100点向上100点绘制字符串。 之后,我们调用showPage()方法。 showPage()方法将保存画布的当前页面。 实际上不是必需的,但建议使用。 showPage()方法也会结束当前页面。 如果在调用showPage()之后绘制另一个字符串或其他元素,则该对象将被绘制到新页面。 最后,我们调用canvas对象的save()方法,该方法将文档保存到磁盘。 现在我们可以打开它,看看我们的PDF是什么样的:
您可能会注意到,我们的文字位于文档底部附近。 原因是原点(0,0)是文档的左下角。 因此,当我们告诉ReportLab绘制文本时,我们是在告诉它从左侧开始绘制100点,从底部开始绘制100点。 这与在Tkinter或wxPython中创建用户界面(左上角是起源)形成对比。
另请注意,由于我们未指定页面大小,因此默认使用ReportLab配置中的默认大小,通常为A4。 在reportlab.lib.pagesizes中可以找到一些常见的页面大小。
让我们看一下Canvas的构造函数,以了解其对参数的要求(现在已经不止这么多了):
1 | def __init__(self,filename, |
在这里,我们可以看到可以将pagesize作为参数传递。 pagesize实际上是一个以点为单位的宽度和高度的元组。
如果要从默认的左下角更改原点,则可以将bottomup参数设置为0,这会将原点更改为左上角。
pageCompression参数默认为零或关闭。 基本上,它将告诉ReportLab是否压缩每个页面。 启用压缩后,文件生成过程会变慢。 如果您的工作需要尽快生成PDF,则需要将默认值保持为零。 但是,如果不关心速度,并且您想使用更少的磁盘空间,则可以打开页面压缩。 请注意,PDF中的图像将始终被压缩,因此打开页面压缩的主要用例是每页上有大量文本或大量矢量图形时。
ReportLab的《用户指南》没有提及不变参数invariant
的用途,因此我看了一下源代码。 根据消息来源,它会产生具有相同时间戳信息的可重复的相同PDF(用于回归测试)。 我从未见过有人在他们的代码中使用此参数,并且由于消息人士说该参数用于回归测试,所以我认为我们可以放心地忽略它。
下一个参数是详细程度verbosity
,用于记录级别。 Zero(0),ReportLab将允许其他应用程序从标准输出中捕获PDF。 如果将其设置为一(1),则每次创建PDF时都会打印出一条确认消息。 可能添加了其他级别,但是在撰写本文时,这是仅有的两个文档。
https://blog.csdn.net/weixin_48923393/article/details/129173640
创建 PDF 文档
1 | from reportlab.pdfgen import canvas |
添加文本
前两个参数是文本的x和y坐标,第三个参数是文本内容。
1 | pdf.drawString(50, 750, "My Technical Article") |
添加图像
前三个参数是图像路径或名称、x和y坐标,第四个参数是图像的宽度,第五个参数是图像的高度。
1 | from reportlab.lib.pagesizes import letter |
添加表格
1 | from reportlab.lib.pagesizes import letter |
其它内容自行翻阅官方文档
reportlab.platypus.Paragraph
以下仅仅展示了最常用的样式,更多丰富的样式如:目录、页眉页脚、表格、图形等等需后续学习。
示例代碼一:
这份代码没有做封装
1 | import tempfile |
Tips: simsun就是宋体
宋体粗体 Version 3.03 Font Download-TTF
示例代码二:
这份代码作了封装,看上去较为清晰
1 | from reportlab.pdfbase import pdfmetrics # 注册字体 |
内容左右显示
在reportlab中可以使用SimpleDocTemplate创建一个文档,然后向里面添加内容,但是直接添加内容只能将值上下显示,如果要将内容左右显示的话,可以使用reportlab中BalancedColumns,它可以将内容分割成两个或者更多大小相等的列。(实际未使用,不清楚api怎么用,这个样例不全)
分列显示的内容,可以是表格、图表、文字等
只是这样将内容分列,内容上面的显示仍然不是特别的灵活。
1 | from reportlab.platypus.flowables import BalancedColumns |
也可以直接用画表格的形式,设置边框为透明,并设置列宽,若需要换行,添加\n
以下为实际工作中的需求,随机生成合同左右两列的pdf数据
1 | import json |
paddleocr
pdf2image
pydoc
在网页中生成注释文档
一定要cd到文件所在文件夹
输入指令:python3 -m pydoc -h 查看注释文档的生成方法
输入指令:python3 -m pydoc -k 关键字 检索含有关键字的模块
输入指令: python3 -m pydoc -p 1233 python3为版本,1233为指定端口
输入指令: python3 -m pydoc -b 相比于-p,不用写端口,自动寻找闲置端口
输入指令: python3 -m pydoc -w 文件名
文件名不用加.py后缀,生成html文件,可以右键文件在浏览器打开
函数专题
apply()
1 | DataFrame.apply(func: 'AggFuncType', axis: 'Axis' = 0,raw: 'bool' = False,result_type=None,args=(),**kwargs) |
数据获取
见爬虫篇
数据处理
存在key-value的数据,无论是啥格式,第一步优先处理成json格式
txt
docx
xls
xlsx
如果我们只是要 读取
Excel文件里面的数据进行处理,可以使用 xlrd
这个库。
首先我们安装xlrd库,执行下面的命令,必须指定版本号
1 | pip install xlrd==1.2.0 |
注意:xlrd 新版本只支持 xls 格式,所以我们这里指定安装 1.2.0 老版本,可以支持xlsx格式。点击这里,下载 Excel文件 income.xlsx
使用pandas
1 | # 处理模型提取的结果 |
csv
https://www.cnblogs.com/junpengwang/p/10803339.html
pdf2png
将文件夹下多个pdf文件,切分成一张张图片
1 | from pdf2image import convert_from_path |
png
png2pdf
将多张图片合成为pdf
安装库:
1 | pip install -i https://pypi.douban.com/simple --trusted-host pypi.douban.com pillow |
将多张图片合成为pdf
1 | # v1.0 该版本的代码,合成的pdf都会多一个内边距 |