目前为止我的学习材料:

https://www.bilibili.com/video/BV1JZ4y1G7KG

https://docs.python.org/zh-cn/3/

Python教程 - 廖雪峰的官方网站 (liaoxuefeng.com)

前言

对于初学者和完成普通任务,Python语言是非常简单易用的。连Google都在大规模使用Python,你就不用担心学了会没用。

用Python可以做什么?可以做日常任务,比如自动备份你的MP3;可以做网站,很多著名的网站包括YouTube就是Python写的;可以做网络游戏的后台,很多在线游戏的后台都是Python开发的。

Python当然也有不能干的事情,比如写操作系统,这个只能用C语言写;写手机应用,只能用Swift/Objective-C(针对iPhone)和Java(针对Android);写3D游戏,最好用C或C++。

Python适合开发哪些类型的应用呢?

首选是网络应用,包括网站、后台服务等等;

其次是许多日常需要的小工具,包括系统管理员需要的脚本任务等等;

另外就是把其他语言开发的程序再包装起来,方便使用。

python的变量可以重复赋不同类型的值,这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言,赋值语句如下(// 表示注释):

1
2
int a = 123; // a是整数类型变量
a = "ABC"; // 错误:不能把字符串赋给整型变量

和静态语言相比,动态语言更灵活,就是这个原因。

开始使用 PyCharm

创建一个简单的python项目

  1. 创建一个新的环境:

    1
    conda create --name myenv python=3.8

    这会创建一个名为 “myenv” 的新环境,并且指定Python版本为3.8。

  2. 激活一个环境:

    1
    conda activate myenv

当你在PyCharm中选择创建新的虚拟环境时(使用Virtualenv、Pipenv或Conda等工具),PyCharm会为项目创建一个独立的Python环境。这个环境是相互隔离的,不会影响你系统上已经安装的Python版本。

选择第一个选项,PyCharm会重新安装一个python!

所以初学者新建项目时优先考虑第二个选项,选择已有的解释器。如果使用 conda 创建了虚拟环境,就去用户名下的 .conda 文件夹下找到那个环境,选择里面的 python.exe 文件。

NVIDIA cuDNN CUDA 深度神经网络库 (cuDNN) | NVIDIA 开发者

NVIDIA CUDA 深度神经网络库 (cuDNN) 是一个 GPU 加速的深度神经网络基元库,能够以高度优化的方式实现标准例程(如前向和反向卷积、池化层、归一化和激活层)。

全球的深度学习研究人员和框架开发者都依赖 cuDNN 来实现高性能 GPU 加速。借助 cuDNN,研究人员和开发者可以专注于训练神经网络及开发软件应用,而不必花时间进行低层级的 GPU 性能调整。cuDNN 可加速广泛应用的深度学习框架,包括 Caffe2、Chainer、Keras、MATLAB、MxNet、PaddlePaddle、PyTorch 和 TensorFlow。如需获取经 NVIDIA 优化且已在框架中集成 cuDNN 的深度学习框架容器,请访问 NVIDIA GPU CLOUD 了解详情并开始使用。

基本输入和输出

print()会依次打印每个字符串,遇到逗号“,”会输出一个空格

1
2
3
4
5
print('The quick brown fox', 'jumps over', 'the lazy dog')
# The quick brown fox jumps over the lazy dog

print('100 + 200 =', 100 + 200)
# 100 + 200 = 300

python基本数据类型

常来看看python官方文档

python是面向对象的,所以下列这些其实是一个个“类”

int float complex bool String list dict(字典)

不仅有数据成员,还有已经封装好的函数。

所以在python里,有这么两个东西

func 函数

method 方法

常量

🍉所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量:

1
PI = 3.14159265359

但事实上PI仍然是一个变量,Python根本没有任何机制保证PI不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量PI的值,也没人能拦住你。

布尔类型

True False 注意大小写

not运算是非运算,它是一个单目运算符,把True变成FalseFalse变成True

1
2
3
4
5
6
>>> not True
False
>>> not False
True
>>> not 1 > 2
True

整型int

加减乘除和取余

⚠ 在python里,整除是 //(又名地板除) ,一条 / 是浮点除

⚠ 在python里,浮点数也可以求余

对于截断误差的处理:

1
2
3
x = 15.3 % 3
y = 0.3
x == y

结果为 False

1
2
eps = 1e-6
abs(x - y) < eps

结果为True

python里的整数可以非常长

python有个新的乘方运算符 **

1
2
2 ** 11
# 2048

对于很大的数,例如10000000000,很难数清楚0的个数。Python允许在数字中间以_分隔,因此,写成10_000_000_00010000000000是完全一样的。十六进制数也可以写成0xa1b2_c3d4

复数Complex

1
2
3
4
5
6
x = 3 + 2j
y = 4 - 3j
x * y
# (18-1j)
x / y
# (0.24+0.68j)

字符串

python里没有字符类型,只有字符串

1
s = 'abc'

可以使用单引号、双引号、三重引号(单引号或双引号都可)。

使用三重引号时,中间可以换行。

1
2
3
4
5
6
7
8
9
10
s = '''abc
def'''
s
# 'abc\ndef'

len(s)
# 7

s = s +'mn'
# 'abc\ndefmn'

三重引号不常用,一般就在开头写注释。

如果字符串内部既包含'又包含"怎么办?可以用转义字符\来标识,比如:

1
'I\'m \"OK\"!'

表示的字符串内容是:

1
I'm "OK"!

转义字符\可以转义很多字符,比如\n表示换行,\t表示制表符,字符\本身也要转义,所以\\表示的字符就是\,可以在Python的交互式命令行用print()打印字符串看看:

1
2
3
4
5
6
7
8
>>> print('I\'m ok.')
I'm ok.
>>> print('I\'m learning\nPython.')
I'm learning
Python.
>>> print('\\\n\\')
\
\

如果字符串里面有很多字符都需要转义,就需要加很多\,为了简化,Python还允许用r''表示''内部的字符串默认不转义,可以自己试试:

1
2
3
4
>>> print('\\\t\\')
\ \
>>> print(r'\\\t\\')
\\\t\\

如果字符串内部有很多换行,用\n写在一行里不好阅读,为了简化,Python允许用'''...'''的格式表示多行内容,可以自己试试:

1
2
3
4
5
6
>>> print('''line1
... line2
... line3''')
line1
line2
line3

上面是在交互式命令行内输入,注意在输入多行内容时,提示符由>>>变为...,提示你可以接着上一行输入,注意...是提示符,不是代码的一部分

python可以正确数出中文字符数量,因为python使用了UNICODE编码

在python中默认使用UTF-8

字符串之间 + 可以完成拼接,但字符串和整型无法拼接。

可以写成

1
s = s + str(32)

所以有一些转换函数

1
2
3
4
5
6
7
8
9
10
11
int('358')
# 358

float('98')
# 98.0

bool('不管这里写什么')
# True

bool('')
# False

python中类型判断

python提供了判断类型的函数 isinstance( , )

1
2
3
4
isinstance(s, int)
# False
isinstance(s, str)
# True

判断字符串是否”相等“

1
2
3
4
5
6
s1 = "Hi"
s2 = "Hi"
s1 == s2
# True
s1 is s2
# True

x = 1, 会为变量 x 分配存储空间,但并不会把 1 存进去。而是会在内存中构造出 1 ,然后让 x 指向该空间。

x = 1, y = 1 会让 x 和 y 指向同一片空间

1
2
3
4
id(s1)
# 1562225633904
id(s2)
# 1562225633904

id一样,说明它俩引用了同一个变量。

正是由于这种引用的机制,一个变量 x 才可以赋值整型、字符串、复数等等不同的类型。

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

1
2
3
4
5
6
7
8
>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
>>> chr(25991)
'文'

如果知道字符的整数编码,还可以用十六进制这么写str

1
2
>>> '\u4e2d\u6587'
'中文'

两种写法完全是等价的。

bytes的使用

Python对bytes类型的数据用带b前缀的单引号或双引号表示

如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:

1
2
3
4
>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

如果bytes中包含无法解码的字节,decode()方法会报错:

1
2
3
4
>>> b'\xe4\xb8\xad\xff'.decode('utf-8')
Traceback (most recent call last):
...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte

如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:

1
2
>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'

序列类型

python主要有3种序列类型

list , turple , range

list 列表

Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。

1
2
x = [1, 2, 3]
type(x)
list

python里面list长度可变,比如通过 xxx.append()来新增元素。

1
2
x.append(4)
x
[1, 2, 3, 4]
1
x.append(2)
1
x
[1, 2, 3, 4, 2]
1
x[1]
2
1
x[4]
2
1
x[1] is x[4]
True
1
x[0] is x[1]
False
1
1 in x
True

可以把元素插入到指定的位置,比如索引号为1的位置:(其它元素依次后移)

1
2
3
>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']
1
[1 , 2] in x
False

因为python以为你在问 x 里有没有一个元素是[1, 2]

1
2
t = ['abt','mn']
x + t
[1, 2, 3, 4, 2, 'abt', 'mn']

🔨python中,上一条语句的执行结果存在 _ 里

1
_
[1, 2, 3, 4, 2, 'abt', 'mn']
1
t * 2
['abt', 'mn', 'abt', 'mn']

列表乘某个数,长度翻倍

1
2
x = [[]] * 3 
x
[[], [], []]
1
2
x[1].append(6)
x
[[6, 6], [6, 6], [6, 6]]

x[-1]代表 x 的最后一个元素

1
2
x = [1, 2, 3, 4]
x[-1]
4

切片操作,注意不包括最后一个元素

1
x[1:3]
[2, 3]
1
x[1:-1]
[2, 3]

切片操作还可以带第三个元素,表示跳着切(2:每2个数字切一个)

1
x[0:4:2]
[1, 3]

省略第二个元素,表示一直到最后

1
x[1:]
[2, 3, 4]
1
x[1::2]
[2, 4]

求最大值和最小值,注意列表中的元素必须可以互相比较,如果既有数字又有字符串则无法比较。

1
min(x)
1
1
max(x)
4
1
x.append(2)
1
x.index(1)
0
1
x.index(2)
1

删除list末尾的元素,用pop()方法:

1
2
3
4
>>> classmates.pop()
'Adam'
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy']

删除指定位置的元素,用pop(i)方法,其中i是索引位置:

1
2
3
4
>>> classmates.pop(1)
'Jack'
>>> classmates
['Michael', 'Bob', 'Tracy']

tuple 元组

与list不同,元组对象无法改变

1
2
x = [1, 2, 3]
x
[1, 2, 3]
1
2
x = tuple(x)
x
(1, 2, 3)
1
x[1] = 2
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In[52], line 1
----> 1 x[1] = 2


TypeError: 'tuple' object does not support item assignment

但这并不意味着tuple元组永远无法改变。事实上,只有tuple每个元素引用的对象都不可变时这一点才成立。否则

1
2
3
4
5
x1 = [1, 2]
x2 = [2, 3]
x3 = [3, 4]
x = (x1, x2, x3)
x
([1, 2], [2, 3], [3, 4])
1
2
x1 = ['a', 'b']
x
([1, 2], [2, 3], [3, 4])
1
2
x2.append('a')
x
([1, 2], [2, 3, 'a'], [3, 4])
1
2
x3[1] = 'c'
x
([1, 2], [2, 3, 'a'], [3, 'c'])

表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a',就不能改成指向'b',指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!

理解了“指向不变”后,要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。

tuple的陷阱:当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来,比如:

1
2
3
>>> t = (1, 2)
>>> t
(1, 2)

如果要定义一个空的tuple,可以写成()

1
2
3
>>> t = ()
>>> t
()

但是,要定义一个只有1个元素的tuple,如果你这么定义:

1
2
3
>>> t = (1)
>>> t
1

定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1

所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:

1
2
3
>>> t = (1,)
>>> t
(1,)

Python在显示只有1个元素的tuple时,也会加一个逗号,,以免你误解成数学计算意义上的括号。

range类型

通常在循环里使用(用法看文档)

range(10) 表示从 0 到 9 的10个数。

字符串

转义字符

1
2
3
4
print('\\')
# \
print('\'')
# '

可以在字符串之前加一个字母 r ,这样字符串里的 \ 便不再是转义字符。

1
2
3
4
s = r'd:\dir\a.txt'
s
# 'd:\\dir\\a.txt'
# 没有r则会输出'd:\\dir\x07.txt'

格式化字符串

  1. 可以用 + 来拼接
  2. 可以用 % 类似C语言的 printf(“”%d + %d”) ,使用时,用 % 和一个元组进行连接,会用元组里的元素替代 %*
1
2
3
4
5
6
pattern = '%s is %d years old.'
pattern
# '%s is %d years old.'
msg = pattern % ('Peanut', 18)
msg
# 'Peanut is 18 years old.'
1
2
3
4
5
6
7
8
'Hello, %s' % 'world'
# 'Hello, world'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'

# 有些时候,字符串里面的%是一个普通字符怎么办?这个时候就需要转义,用%%来表示一个%:
>>> 'growth rate: %d %%' % 7
'growth rate: 7 %'
  1. 使用 format 函数 “—{}—{}—{}—“.format(——,——,——) 例如
1
2
3
4
5
6
7
8
9
10
msg = '{:d} + {:d} = {:d}'.format(3, 5, 8)
msg
# '3 + 5 = 8'


# format()
# 另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……,不过这种方式写起来比%要麻烦得多:

>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'

使用 format 时还可以调整和元组元素的对应关系,这样就可以用一个小的元组替换更多的占位符。

1
2
3
msg = '{1:d} + {2:d} = {0:d}'.format(3, 5, 8)
msg
# '5 + 8 = 3'
  1. 字符串的 f 前缀

最后一种格式化字符串的方法是使用以f开头的字符串,称之为f-string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换:

1
2
3
4
>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62

上述代码中,{r}被变量r的值替换,{s:.2f}被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}的替换结果是19.62

1
2
3
4
name = 'Peanut'
age = 18
f'{name} is {age} years old.'
# 'Peanut is 18 years old.'

字符串也有乘法,和列表类似。

⚠字符串是只读的。如果进行拼接,本质上是创建一个更长的字符串对象。字符串无法写,其值永远不变。

字符串的内置方法

🍭很多,去看文档

结构控制语句

if 语句

⚠注意,python中输入默认都是字符串类型!

1
2
3
4
5
6
7
8
9
10
11
score = int(input('Please input a number:'))

if score >= 60:
print('及格')
elif score >= 40:
print('真拿你没办法')
else:
print('笨蛋!')

print(f'你这次的成绩是{score}')

⚠浮点数尽量不要用 == 比较

⚠注意区分 == 和 is

⚠python中的逻辑运算符,条件复杂时记得用()

  • and, or, not, is, is not, in, not in

match 语句

当我们用if ... elif ... elif ... else ...判断时,会写很长一串代码,可读性较差。

如果要针对某个变量匹配若干种情况,可以使用match语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
score = 'B'

match score:
case 'A':
print('score is A.')
case 'B':
print('score is B.')
case 'C':
print('score is C.')
case _: # _表示匹配到其他任何情况
print('score is ???.')

# score is B.

使用match语句时,我们依次用case xxx匹配,并且可以在最后(且仅能在最后)加一个case _表示“任意值”,代码较if ... elif ... else ...更易读。

复杂匹配

match语句除了可以匹配简单的单个值外,还可以匹配多个值、匹配一定范围,并且把匹配后的值绑定到变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
age = 15

match age:
case x if x < 10:
print(f'< 10 years old: {x}')
case 10:
print('10 years old.')
case 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18:
print('11~18 years old.')
case 19:
print('19 years old.')
case _:
print('not sure.')

在上面这个示例中,第一个case x if x < 10表示当age < 10成立时匹配,且赋值给变量x,第二个case 10仅匹配单个值,第三个case 11|12|...|18能匹配多个值,用|分隔。

可见,match语句的case匹配非常灵活。

匹配列表

match语句还可以匹配列表,功能非常强大。

我们假设用户输入了一个命令,用args = ['gcc', 'hello.c']存储,下面的代码演示了如何用match匹配来解析这个列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
args = ['gcc', 'hello.c', 'world.c']
# args = ['clean']
# args = ['gcc']

match args:
# 如果仅出现gcc,报错:
case ['gcc']:
print('gcc: missing source file(s).')
# 出现gcc,且至少指定了一个文件:
case ['gcc', file1, *files]:
print('gcc compile: ' + file1 + ', ' + ', '.join(files))
# 仅出现clean:
case ['clean']:
print('clean')
case _:
print('invalid command.')

#gcc compile: hello.c, world.c

第一个case ['gcc']表示列表仅有'gcc'一个字符串,没有指定文件名,报错;

第二个case ['gcc', file1, *files]表示列表第一个字符串是'gcc',第二个字符串绑定到变量file1,后面的任意个字符串绑定到*files(符号*的作用将在函数的参数中讲解),它实际上表示至少指定一个文件;

第三个case ['clean']表示列表仅有'clean'一个字符串;

最后一个case _表示其他所有情况。

while 语句

1
2
3
4
5
6
7
i = 1
s = 0
while i <= 100:
s += i
i += 1

print(s)

for语句

for item in items:

要求 items 是一个可迭代的对象,比如列表和元组。

1
2
3
4
5
6
7
scores = [56, 89, 75, 36, 96]

for score in scores:
if score >= 60:
print('行')
else:
print('不行')

可以和 range() 函数结合

1
2
3
4
5
6
for i in range(len(scores)):
print(scores[i])

# 倒着输出中间三个数
for i in range(3, 0, -1):
print(scores[i])

可以用一个很新的函数 enumerate() ,“枚举”

它会把输入的列表变成很多个元组,如 (0, 56), (1, 89)

🍗在python里,元组可以直接赋值,前提是元素个数一样。

x = (1, 2)

a, b = x

1
2
for idx, score in enumerate(scores):
print(idx, score)

enumerate 还可以带第二个参数

enumerate(scores, 1)

表示生成的元组下标从 1 开始,更适合给普通人阅读。

另外一个函数 zip (英语里有“拉链”的意思),把两个列表一一对应

如果两个列表不一样长,则会以较短的那个为准。

1
2
for name, score in zip(names, scores):
print(name + ': ' + str(score))

字典 Dict

🌻 它是一种数据类型,在数据结构里,我们更习惯称其为 Map(映射)

在处理数据时,我们经常需要处理类似于 Key - Value 的键值对,用 {} 括起来

dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象

这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。

要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:

1
2
3
4
5
>>> key = [1, 2, 3]
>>> d[key] = 'a list'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scores = {'Mike': 85, 'AA': 99}

x = scores['Mike']
print(x)
# 85


# 如果key不存在,dict就会报错:
>>> d['Thomas']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'Thomas'

y = scores.get('PP') #使用get方法,如果 PP 不在字典里也不会报错
# None
# None, 声明的变量 y 没有引用任何的数据成员

z = scores.get('PP', 0) #在找不到某元素时,会返回默认值
print(z)
# 0

添加和删除

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

1
2
3
4
5
6
7
scores['BB'] = 56
print(scores)

# {'Mike': 85, 'AA': 99, 'BB': 56}

del scores['Mike']
print(scores)

为什么字典能够提高查找效率?

采用了 哈希表Hash

把 Int, Float, Bool, String, Tuple 转化成了一个固定长度的整数,作为key

⚠不可变的元素才可以进行hash操作,列表不可以

所以字典元素只能用 key 进行访问,无法用下标访问。因为 hash 表并不知道第几个元素是谁。

1
2
3
4
5
6
print(scores.keys()) #输出所有 key
# dict_keys(['AA', 'BB'])
print(scores.values()) #输出所有 value
# dict_values([99, 56])
print(scores.items()) #输出所有键值对
# dict_items([('AA', 99), ('BB', 56)])

对字典进行遍历

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
29
30
31
32
scores = {
'AA': (35, 96, 54),
'BB': (98, 53, 45),
'CC': (56, 45, 77)
}

for key in scores.keys():
print(key, scores[key])

# AA (35, 96, 54)
# BB (98, 53, 45)
# CC (56, 45, 77)

for key, value in scores.items():
print(key, value)

# AA (35, 96, 54)
# BB (98, 53, 45)
# CC (56, 45, 77)

# 下面这种写法更常用
for name in scores: #和第一种写法相同。在对字典进行遍历时,指挥返回 key, 其它的内容不返回
print(name, scores[name])

for name in scores:
a1, a2, a3 = scores[name]
ave = (a1 + a2 + a3) / 3
msg = f'{name}: {a1}, {a2}, {a3}, ave:{ave}'
print(msg)
# AA: 35, 96, 54, ave:61.666666666666664
# BB: 98, 53, 45, ave:65.33333333333333
# CC: 56, 45, 77, ave:59.333333333333336

list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:

1
2
3
4
5
6
7
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
a
c
b

因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。

默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()

由于字符串也是可迭代对象,因此,也可以作用于for循环:

1
2
3
4
5
6
>>> for ch in 'ABC':
... print(ch)
...
A
B
C

所以,当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。

那么,如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:

1
2
3
4
5
6
7
>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

1
2
3
4
5
6
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C

上面的for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:

1
2
3
4
5
6
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9

集合 Set

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

要创建一个set,需要提供一个list作为输入集合:

1
2
3
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。

重复元素在set中自动被过滤:

1
2
3
>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:

1
2
3
4
5
6
>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

通过remove(key)方法可以删除元素:

1
2
3
>>> s.remove(4)
>>> s
{1, 2, 3}

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

1
2
3
4
5
6
>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。

函数

🥭函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

1
2
3
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

如果没有return语句,函数执行完毕后也会返回结果,只是结果为Nonereturn None可以简写为return

函数的定义

def 函数名 (参数列表):

​ 函数体

1
2
3
4
5
6
def foobar():
print("Hello World.")


foobar()
# Hello World.

⚠注意区分值传递和引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def foobar(x):
x[1] = 23


x = [1, 2, 6]
print(x)
foobar(x)
print(x)
# [1, 2, 6]
# [1, 23, 6]

def foobar(x):
x = 23


x = 1
print(x)
foobar(x)
print(x)
# 1
# 1

带返回值

1
2
3
4
5
6
7
8
def foobar(x):
return x**2 + 3 * x +6


x = 1
y = foobar(x)
print(y)
# 10

传递多个参数时,可以指明对应关系并忽略次序

1
2
3
4
5
6
def foobar(x, y):
return x - y


z = foobar(y=3, x=5)
print(z)

定义的函数可以带默认值,注意如果前面的参数有默认值,则它后面的参数必须要有

1
2
3
4
5
6
7
def foobar(x=3, y=5):
return x - y


z = foobar(5)
print(z)
# 0

空函数

如果想定义一个什么事也不做的空函数,可以用pass语句:

1
2
def nop():
pass

pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。

pass还可以用在其他语句里,比如:

1
2
if age >= 18:
pass

缺少了pass,代码运行就会有语法错误。

参数数量可变的情况

采用“打包”的方法。注意先打包成元组,后打包成字典,所以要注意传参的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def foobar(x, y, *arg):
for x in arg:
print(x)


foobar(35, 56, 'Hello', True, 96.3)
# Hello
# True
# 96.3


#如果参数中还含有 **=** 的情况
def foobar(x, y, *arg, **kwarg):
print(arg)
print(kwarg)


foobar(35, 56, 'Hello', True, 96.3, arg1=66, arg2=53.2)
# ('Hello', True, 96.3)
# {'arg1': 66, 'arg2': 53.2}

给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:

1
2
3
4
5
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:

1
2
3
4
>>> calc(1, 2)
5
>>> calc()
0

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:

1
2
3
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14

这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

1
2
3
>>> nums = [1, 2, 3]
>>> calc(*nums)
14

🥟*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

🍽用列表作为默认参数的下场:

1
2
3
4
5
6
7
8
9
10
def add_end(L=[]):
L.append('END')
return L

>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

原因解释如下:

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

小结

Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。

默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

要注意定义可变参数和关键字参数的语法:

*args是可变参数,args接收的是一个tuple;

**kw是关键字参数,kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法:

可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})

使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。

定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

返回多个值

函数可以返回多个值吗?答案是肯定的。

比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的坐标:

1
2
3
4
5
6
import math

def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny

import math语句表示导入math包,并允许后续代码引用math包里的sincos等函数。

然后,我们就可以同时获得返回值:

1
2
3
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0

但其实这只是一种假象,Python函数返回的仍然是单一值:

1
2
3
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

一等函数公民(First-Class Function)

在编程语言中,如果函数(或方法)被视为“一等函数公民”,那么它意味着函数可以像其他数据类型(比如整数、字符串、对象等)一样被传递、赋值给变量、作为参数传递给其他函数,以及从其他函数返回。具有这个特性的编程语言被称为“支持一等函数公民(或一等函数)的语言”。

在支持一等函数公民的编程语言中,函数可以被当作数据来操作,这样的语言通常能够更灵活地进行编程。例如,在JavaScript、Python、Ruby、Scala等编程语言中,函数被视为一等函数公民,这意味着你可以将函数作为参数传递给其他函数,将函数赋值给变量,甚至可以在运行时动态地创建函数。这种特性使得在这些语言中能够更容易地实现高阶函数、闭包、函数式编程等编程范式。

1
2
3
4
5
6
7
8
9
def f(x):
return 3*x**2 + 6 * x - 2


y = f
print(type(y))
print(y(1))
# <class 'function'>
# 7

近似计算 x 点导数:

1
2
3
4
5
6
7
def diff(x, func):
delta = 1e-6
return (func(x + delta) - func(x)) / delta


print(diff(1, f))
# 12.000002998391324

🍚 一等公民还可以被加入到集合中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def f2(x):
return x**2


def f3(x):
return x**3


def f4(x):
return x**4


funs = [f2, f3, f4]
for f in funs:
print(f(5))

# 25
# 125
# 625

🍤 一等公民函数还可以作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def fun1(x):
def fun2(y):
return x +y;
return fun2


a = fun1(3)
print(type(a))
print(a(4))
print(type(a(4)))
# <class 'function'>
# 7
# <class 'int'>
print(fun1(3)(6))
# 9

单行函数

又名 lambda 表达式(λ-expression)

🍳 有时候我们写代码需要一个函数,但是该函数太简单,可以使用 lambda 表达式

lambda关键字 + 参数表 + ‘:’ + 表达式内容,例如

1
2
3
4
5
6
7
8
def diff(x, func):
delta = 1e-6
return (func(x + delta) - func(x)) / delta


d = diff(2, lambda x: 2*x**2 + 3)
print(d)
# 8.000002001296025

异常处理

编写程序时会遇到三类错误

  • 语法错误,编译器会告诉你写得不对。
  • 语义错误,写迷糊了,你的代码并没有按你想的那样执行。可以通过调试器 debugger 来观察出错原因。
  • 运行时错误,代码执行时才会出现的、无法预期的错误。比如尝试打开的文件并不存在,比如用户并没有按你要求的那样输入一个浮点数反而输入了一个字符串。大概有两种方法:
    • 返回值。比如打开一个文件,成功打开则返回指向该文件的指针,文件不存在则返回 NULL ,后续可以通过判断是否为 NULL 来编写程序,避免错误。但是有时很难操作,比如返回浮点数,无法分配一个数字表示“错误”,这时候可以考虑返回一个元组,用一个元素表示正确还是错误,第二个元素才是浮点数本身。
    • 异常 Exception 机制。比如写了一个接受浮点数作参数的函数,当没有接收到浮点数类型时便 raise 一个 exception ,raise 之后就从当前函数返回,去看主程序里有没有处理这个异常。如果没有处理,则抛出给调用者。如果调用者没有处理,则直接抛给python 解释器。python 解释器一般会直接把异常的调用栈打印出来。例如:
1
2
3
4
5
6
7
8
x = float("abc")
print(x)

# Traceback (most recent call last):
# File "D:\MyProjects\PycharmProjects\MyStart\day03.py", line 130, in <module>
# x = float("abc")
# ^^^^^^^^^^^^
# ValueError: could not convert string to float: 'abc'

🏹 当程序试图把 abc 转化为浮点数时,发现它无法转换,于是抛出异常。但是用户程序里并没有处理这个异常,于是直接由 python 解释器来处理。python 解释器就会像上面这样打印出来调用栈。

  • Traceback (most recent call last):

    Traceback ,意思是“回溯”,most recent call last 是说 最近的调用显示在最后。在堆栈跟踪中,最近的函数调用在最上面,然后是调用它的函数,以此类推,直到达到导致异常的根本原因。

🏒 python 里的异常处理

  • try:

    ​ 将来运行时可能会遇到错误的代码

  • except:

​ 处理方法

  • finally:
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
try:
x = float("abc")
except:
print('无法转换')

print('DONE')
# 无法转换
# DONE


try:
x = float("abc")
except Exception as e:
print(e.args)

print('DONE')


try:
x = float("abc")
except Exception as e:
print(e.args)

print(x)
# 。。。
# NameError: name 'x' is not defined

⚠ 因为执行 浮点数转换时出现异常,导致 x 没有被构造出来。我们希望通过一些操作,能正常输出这个 x

​ 注意不管发没发生异常,都会执行 finally ,所以一般用来执行类似最后关闭文件等必要操作。下面这段代码的问题在于, x 会始终等于 0 .可以修改为把 x = 0 放在 try 的第一句。

1
2
3
4
5
6
7
8
try:
x = float("abc")
except Exception as e:
print(e.args)
finally:
x = 0

print(x)

try 后面可以跟多个 except 子句,用来处理不同类型的异常(try 的代码块可能发生多种异常),异常的种类很多,可以看文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def askuser():
while True:
try:
a = float(input('请输入一个浮点数:'))
return a
except Exception as e:
print('Error:' + e.args[0])


print(askuser())
# 请输入一个浮点数:ds
# Error:could not convert string to float: 'ds'
# 请输入一个浮点数:erdsa
# Error:could not convert string to float: 'erdsa'
# 请输入一个浮点数:ffe
# Error:could not convert string to float: 'ffe'
# 请输入一个浮点数:-9.3
# -9.3

文件处理

先用 open() 把文件打开,返回一个对象 f = open( … ),之后可以 readline 读取一行,或者 readlines 读取所有行,每一行作为列表的一个元素。写文件则对应 writeline(注意需要自己加回车)和 writelines(会把列表写到文件里)

⚠ 记得关闭文件 f.close()

因为要处理异常,所以一般会把打开文件的操作放进 try 里。记得在 finally 里关闭文件。

🍬 具体使用时,我们会用到一个语法糖 with

1
2
3
4
5
6
7
8
9
try:
f = open('test.txt', 'r')
lines = f.readlines()
except Exception as e:
pass
finally:
f.close()

print(lines)
1
2
3
4
5
with open('test.txt', 'r') as  f:
lines = f.readlines()

for line in lines:
print(line)

使用了 with ,会自动检查异常和关闭文件。

下面这段代码可以去掉多余的回车换行和开头缩进,并且把’。’作为分隔符分成一个个句子,然后让每个句子成为一个列表元素。

1
2
3
4
5
6
7
8
9
10
11
12
with open('test.txt', 'r') as  f:
lines = f.readlines()

sents = []
for line in lines:
line = line.strip()
tokens = line.split('。')
for token in tokens:
if len(token) > 0:
sents.append(token)

print(sents)

filter函数,列表生成式

列表生成式即List Comprehensions,是Python内置的非常强大的可以用来创建list的生成式。

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11))

1
2
>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:

1
2
3
4
5
6
>>> L = []
>>> for x in range(1, 11):
... L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

1
2
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。

for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

1
2
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列:

1
2
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

三层和三层以上的循环就很少用到了。

对于一个列表,可以使用“列表生成式”和 filter( , ) 函数

接受两个参数,第一个参数是一个函数,第二个参数是一个可迭代对象。函数有一个参数是后面的可迭代对象的元素,依次取出元素进行运算。如果函数结果为真,就保留这个元素。相当于用这个函数,对后面的可迭代对象进行了一轮“筛选”。

1
2
3
numbers = [1, 2, 3, 4, 5]
list(filter(lambda x: x % 2 != 0, numbers))
# [1, 3, 5]

注意,由于 lazy 机制,filter 语句并不会真的进行筛选,而是要等到真正用到该语句时才执行。(所以需要这个 list)

列表生成式的其他用法:

1
2
3
4
# 输出每行的长度
lens = [len(sent) for sent in sents]
print(lens)
# [19, 7, 53, 20, 69, 42, 43, 21, 78, 63]
1
2
3
[(x, y) for x in range(1, 11) for y in range(1, 11)]
# [(1, 1), (1, 2), ... , (5, 5), (5, 6), ... ,(10, 8), (10, 9), (10, 10)] # 共100个点

for循环其实可以同时使用两个甚至多个变量,比如dictitems()可以同时迭代key和value:

1
2
3
4
5
6
7
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
... print(k, '=', v)
...
y = B
x = A
z = C

因此,列表生成式也可以使用两个变量来生成list:

1
2
3
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']

生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

1
2
3
4
5
6
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

创建Lg的区别仅在于最外层的[]()L是一个list,而g是一个generator。

1
2
3
4
5
6
7
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator:

1
2
3
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

这里,最难理解的就是generator函数和普通函数的执行流程不一样。普通函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

读出含有特定内容的句子

输出所有含有“编码”两字的句子

1
2
3
for sent in sents:
if '编码' in sent:
print(sent)

简单的正则表达式

而有时候,我们需要选出所有含阿拉伯数字的句子,这时候我们可以考虑使用一个名为“ re ” 的正则表达式包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re

with open('test.txt', 'r') as f:
lines = f.readlines()

sents = []
for line in lines:
line = line.strip()
tokens = line.split('。')
sents.extend(list(filter(lambda x: len(x) > 0, tokens)))

regex = '编码'
for sent in sents:
if re.search(regex, sent) is not None:
print(sent)
1
2
3
4
regex = '可.'
for sent in sents:
if re.search(regex, sent) is not None:
print(sent)

上面这种会匹配所有有“可”字开头的两字词。其中“ . ”是通配符,匹配任意一个字符。

1
2
3
4
regex = '[克早]'
for sent in sents:
if re.search(regex, sent) is not None:
print(sent)

上面这种会匹配任何含有“克”或者“早”的成员。

1
2
3
4
regex = '[0-9]'
for sent in sents:
if re.search(regex, sent) is not None:
print(sent)

则可以实现筛选所有含有阿拉伯数字的句子。

🥟 如果要找所有1000到2000之间的年份呢?

1
2
3
4
regex = '1[0-9]{3}'
for sent in sents:
if re.search(regex, sent) is not None:
print(sent)

除了 search,还有另一个常用的正则表达式的 findall ,下面的代码可以找出所有数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re

with open('test.txt', 'r') as f:
lines = f.readlines()

sents = []
for line in lines:
line = line.strip()
tokens = line.split('。')
sents.extend(list(filter(lambda x: len(x) > 0, tokens)))

regex = '[0-9]+'
for sent in sents:
if re.search(regex, sent) is not None:
print(re.findall(regex, sent))

注意这个“ + ”,如果没有,程序会把多位数拆成一位数再返回。“ + ”意味着前面的数字至少重复一次。

🍜 记得看文档,里面有正则表达式的详细知识。

写操作

注意输出的文件没有换行,需要手动为每一行加上回车。可以采用列表生成式。

1
2
3
4
5
6
7
8
9
10
11
12
with open('test.txt', 'r') as  f:
lines = f.readlines()

sents = []
for line in lines:
line = line.strip()
tokens = line.split('。')
sents.extend(list(filter(lambda x: len(x) > 0, tokens)))

with open('ouput.txt', 'w', encoding='utf-8') as f:
sents = [sent + '\n' for sent in sents]
f.writelines(sents)

以后遇到二进制文件等其它类型的文件,看看文档应该就够了。25