Python光速入门教程(适合已经会其他编程语言的人)

目录

0x00 前言

这篇教程是写给已经学会c++、java等面向对象语言的人快速入门Python的教程,这篇教程的目的是为了减少学习Python过程中不必要的时间,因此这里假定读者已经明白了计算机和编程语言中的一些基础概念,本文不再复述这些概念。
如果对哪些概念不太清楚,请善用搜索引擎。

在这个前提下,本文可能会比较硬核,相比一些初学Python的书籍,本教程一方面会大幅减少解释的文字,直接类比其他语言的特性,甚至直接不展示一些显而易见的输出结果,另一方面对每个章节都会多塞一些深入但常用的知识点。不过不用担心,Python这门语言非常简单,在有编程基础的前提下,你很快就能掌握它。

Python有着丰富和强大生态,几乎任何常见的操作你都能找到相应的模块帮助你编写程序。但是,在编写一般的程序时,请不要首先去寻找是否已经有现成的类库,否则你除了能够“三行代码写一个网站”、“十行代码写一个游戏”以外,将不会有任何收获。使用Python是为了让你在编程时不受限于编程语言本身,而把注意力放在解决实际问题上。因此,为了防止你的编程能力降低,对于一般的程序,第一次编写建议时先自己思考怎么解决。另外,不建议将Python作为你的首要编程语言。

0x01 安装与配置

这里以 Windows 平台为例。

首先打开 Python官方网站的下载页面,点击Download Python x.x.x即可。下方也有不同Python版本可选,本文基于Python 3.9.13,读者可以选择更新的版本。

为方便起见,可以不更改安装目录,直接一路点击下一步完成安装即可。

设置环境变量

在Windows平台,默认情况下,Python安装位置为C:\Users\你的用户名\AppData\Local\Programs\Python\Pythonxx,将这个地址添加到环境变量中即可。

为验证环境变量是否配置正确,可以打开命令提示符并输入python --version(或python3 --version,以下使用python指令)。

常见问题

在Windows10的命令行输入python并回车时,可能会出现弹出微软应用商店的迷惑现象(或者执行python --version时无反应)。如果出现这种情况,可以看这篇文章

使用python命令会卡很久,经常卡几个小时都不动。遇到这种情况可以检查一下环境变量,有时候在安装时让Python自动设置环境变量,可能并没有设置正确。

值得一提的是,对于Python这门编程语言而言,版本问题一直是许多初学者乃至科研人员头疼的问题,比如许多老旧的库或项目仍停留在Python 2时代,而某些库或项目所依赖的库或项目建立在不同Python版本,且有些不向上或向下兼容。因此,当你复现一个依赖复杂的项目甚至只是严格照着教程安装一个环境时,可能就会出现各种教程中没有提到的报错,其中很大概率是版本问题。这里着重强调这点就是为了提醒大家注意版本问题,因为太多人甚至我实验室经验丰富的师兄师姐都头疼过这个问题。当遇到问题时,是不是版本问题,应该作为一个排查因素。

编辑工具

Python代码使用VSCode、PyCharm等IDE都可以,Eclipse、Visual Studio等老牌IDE均有插件用来编辑Python代码,这里推荐使用PyCharm,其中社区版是免费的。

0x02基础知识

1. 快速上手

“Hello World”

创建一个main.py的文件并打开它,开始编写喜闻乐见的第一行代码,打印Hello World:

print("Hello World")

仅需一行代码,无需主函数之类的。有点像shell,你可以把Python理解为一个脚本语言,只不过它很强大,它的代码简洁而优雅,而且比一般的脚本语言可以做更多的事。

虽然如此,还是建议加个判断语句做保护,到后面会说明为什么:

if __name__ == '__main__':
    print('Hello World')

为了节省篇幅,后面的示例代码中不一定会加上这个判断条件,但最好养成习惯加上它。

为了执行它,你可以使用命令提示符并定位到该文件所在的目录下,并输入python main.py

加点变量和运算

在Python中,变量无需声明类型,直接使用名称并赋值即可。

a = 1
b = 2
print(a + b)

在Python命令行中编写

如果你不想为了临时执行一个简单的操作而特地创建一个文件,Python也可以不将代码写在文件中。打开命令行,输入python并回车,即可进入到python命令行。

接着输入a=1并回车,即可定义变量a并赋值1.

再定义一个变量b=2,并输出a+b的值:

C:\Users\Lyzen>python
>>> a = 1
>>> b = 2
>>> print(a + b)
3

你也可以直接输入a + b以输出它的值:

>>> a = 1
>>> b = 2
>>> a + b
3

这种方法(不用print而用a+b)的输出,会自动记录到_中(即最近一次输出),你可以用它方便的做运算。

>>> 1 + 2
3
>>> _ * 2
6

使用Python命令行执行指令可以在不创建文件的情况下快速完成一定的操作,熟练了之后你可以在极短时间内实现你想要的临时操作,比如批量更改文件名,使用爬虫随便爬点东西等等。

为了节省篇幅,后面的短代码和运行结果会使用这种方式表示。

当一个变量需作为他用或不小心赋值错误时,你可以覆盖它:

>>> a = 1
>>> b = 22  # 这里打多了个2
>>> b = 2   # 覆盖变量b
>>> a + b
3

同理,你也可以覆盖一个函数,当你用Python命令行定义函数时要注意缩进,至于如何定义函数会在后面提到。

字符串

Python的字符串可以用单引号或双引号括起来,比如

str1 = "abc"
str2 = 'edf'
str3 = "I'm Peter."

用哪个引号开始就要用哪个引号结束,且可以在字符串内使用另一种引号,比如上面的str3。当然,你也可以用转义字符\'\"来在字符串内表示引号。除此之外Python还支持换行\n,制表符\t,反斜杆\\等常用的转义符。

在字符串的前引号前加上r,就可以忽略转义字符。

>>> print(r"abc\def")
abc\def

在字符串前引号前加上f,可以在字符串内使用变量或表达式:

name = "Peter"
score = 68
print(f"{name}'s score is {score / 10}.")

运行结果

Peter’s score is 6.8.

这是一种很方便的格式化字符串的方法,也是Python非常甜的语法糖,推荐大家使用。

而在Python3.5之前,则需要使用"{}'s score: {}".format(name, score / 10)

rf等还可以连用,比如

print(rf'Hi, {name}. \(^o^)/')

可以使用加号+进行字符串连接,实际上如果是两个用引号确认的字符串常量的连接,连+号都不需要。

print('abc'+'def')
print('123'+'456')
print('Life is like a box of chocolate, '
      'you never know what you are gonna get.')

在上面第三个print中,将字符串分成两行以便提高超长字符串的可阅读性。同时,换行后的字符串前引号与上一行的字符串的前引号对齐,这样的代码规范可以提高代码的可阅读性。

Python还提供了这样的语法糖,使用乘法构造字符串:

>>> s = '-' * 20
>>> s
--------------------

代码规范及 PEP 8

PEP 8是一个代码风格规范,它规定了一系列要求来让程序员写出一定风格的代码。在实际的开发特别是团队开发中,使用一致的代码风格写出高可阅读性的代码可以提高开发效率。PEP 8的内容非常多,你可以在这里看到它的详细内容。不用担心记不住这么多规范,如果你使用的是PyCharm来编写python代码,在写出不符合PEP 8的代码时,它会提示你而且可以一键修改。在后面的内容中也会穿插相关的建议来帮助你写出好看的代码。

部分PEP8的内容

  • 每行代码控制在80个字符以内。在大部分代码编辑器中都会有一条竖线告诉你第80个字符在哪里,你只要不超出这条竖线即可。如果代码太长,可以使用\来换行
    result = varible1 + varible2 + varible3 + \
             varible4
  • 不要使用制表符(Tab)作为缩进,每一级缩进应该使用4个空格来表示。大部分代码编辑器在编辑python代码时,tab键的缩进都是4个空格。
  • 在文件的最后留一行空行。
  • 二元运算符两边留一个空格,比如a + b而不是a+b

养成良好的编码习惯

良好的编码习惯是成为优秀程序员的关键,零乱的代码只能让外行人投来羡慕的目光(或许不能),特立独行的后果是当你急于证明自己时才发现根深蒂固的坏习惯已经难以改正。

注释

单行注释使用#开头。
PEP 8建议在#后加一个空格然后再编写注释。
如果注释要写在某行代码的右边,就在#前空两个空格再写。

print('Hello World')  # 这是注释

多行注释可以使用连续的三个引号(单引号和双引号都可)起始和结尾。

'''
这里是多行注释的示例
作者:Lyzen
时间:2022年7月19日
'''
print("Hello World")

它还可以作为多行字符串使用,但不建议这样用,因为为了去掉字符串中首尾的换行和每行前面的空格会导致代码很难看:

if __name__ == '__main__':
    print('''
          Hello
          World
          ''')
    str = """ \
line 1
  line 2
    line 3\
"""
    print(str)

运行结果


         Hello
         World
         
line 1
  line 2
    line 3

可见,对于第一个输出“Hello World”中,原代码的缩进对齐会导致输出也多出缩进,而为了去掉这些缩进,又会使代码变得很难看(第二个输出)。
如果有多行字符串的需求,建议使用字符串连接以及换行转义:

print('Life is like a box of chocolate, \n'
      'you never know what you are gonna get.')

或者使用全局变量:

msg_usage_en = \ 
'''
Usage:
1. Open the fridge.
2. Put the elephant into the fridge.
3. Close the fridge.
'''
if __name__ == '__main__':
    print(msg_usage_en)

用户输入

使用函数input(mes)即可要求用户输入,得到的内容是字符串,mes为给用户输入的提示。

name = input("Tell me your name:")
print(f"Your name is {name}")

如果需要将输入的内容转化为整数,可以使用int()

age = input("Tell me your age:")
if age >= 18:
    print("You are an adult")

2. 变量与运算

什么是动态语言?跟静态语言有什么区别?

以Python为例,Python定义的变量不需要指定类型,而且可以让同一个变量指向不同类型:

a = 1
print(type(a))
a = "abc"
print(type(b))

运行结果

int
str

对于函数来说,有点像某些语言中的泛型?

def func(a, b):
    return a + b

print(func(1, 2))
print(func("Hello", "World"))
print(func("Hello", 1))

运行结果

3
HelloWorld
TypeError: Unsupported operand type(s) for +: ‘int’ and ‘str’.

动态类型的语言是指在运行期间才去做数据类型检查的语言,而静态语言是在编译时做数据类型检查。动态语言的变量在运行时才能确定具体类型,和静态语言在编译时就已经确定类型了。

除此之外还有强类型定义和弱类型定义的区别。

对于强类型语言,某个数据类型的变量如果不经过强制转换,那么就永远是给定的数据类型。

Python属于动态强类型定义语言,还是以Python为例,比如一个整型变量,不经过转换,它就不能跟字符串做运算:

print(1 + "s") 错误

print(str(1) + "s") 正确

而对于弱类型语言,则可以执行该操作,比如javascript:

console.log(1 + "s")  // 输出 1s

由于动态语言特别是弱类型动态语言的特性,数据类型可能是不安全的,所以要特别注意这种情况。这里是一个因为小小的数据类型错误而造成重大损失的案例:bilibili 2021.07.13崩溃原因

而Python是强类型定义语言,它可以避免许多错误,但这并不意味着数据类型安全可以被忽略。

注:对于Java,1+"s"中的+为连接符,会自动把1转化为字符串再做连接,但Java是静态强类型语言。

不过,Python也允许你指定变量的类型,称为标注或注解。在大型程序的编写过程中,未知的变量类型可能会导致混乱,因此只要你想,就可以在变量名的后面加上:type来指定变量类型:

a: int = 1  # 整型
b: str = "Hello World"  # 字符串

但这并不能阻止变量被赋予其他类型,仅起提示的作用,当你给一个指定了类型的变量不同类型的值时,PyCharm会提醒你。当然你都指定类型了,相信不会故意赋值其他类型吧。

对于一个结构复杂的项目,建议大家指定变量的类型,以提高代码的可阅读性,特别是函数参数,如果该函数参数是限制类型的,一般情况理应对其进行标注。

此外,在使用Python连接字符串与其他类型变量时经常可能会忘了将其他类型变量转为字符串(str(number) + "a string"),从而导致报错,这里再次提醒。

变量的命名

与其他大多数语言一样,变量使用字母、数字和下划线命名,数字不能放在最前面,不能和关键字(保留字)重复等。

Python的变量一般全小写,多个单词用下划线隔开,比如my_name而不是myName。函数名、文件名、包名也用这种命名方式,而类名则使用”大驼峰”的命名方式,比如MyClass

常量一般全大写,多个单词用下划线隔开,比如MY_CONSTANT。一般我们看到全大写的变量都把它视作常量,不去修改它,但实际上它依旧是可修改的变量。

另外,如果下划线加在最前面或最后面,一般由特殊含义,特别是同时以两个下划线起始和以两个下划线结尾的,有许多是Python内置的变量或函数,比如上面提到的__name__,命名时要注意避开这些内置变量或函数,或者索性不要使用同时两个下划线起始和结尾。

如果以下划线开头,也会让变量赋予特殊的意义,这个会在后面讲到。目前暂时不要命名以下划线开头的变量和函数。

即便没有使用到下划线,依然可能和Python内置的变量或函数命名冲突,比如max,一旦发生这种情况一般编辑器会提示你,你可以把它改成max_valuemax_等。

浮点数

跟其他大多数编程语言一样,浮点数在运算过程中会变得不精确:

>>> 0.1 + 0.2
0.30000000000000004

如果你对其原因感兴趣,可见IEEE 754IEEE 754 百度百科,或者翻阅计算机组成原理相关书籍或文章。

请不要直接比较两个浮点数是否相等:

>>> 0.1 + 0.2 == 0.3
False

你应该做的是判断两个浮点数相差是否小于某个范围:

>>> abs(0.1 + 0.2 - 0.3) < 1e-6
True

你也可以使用isclose()来判断:

>>> from math import isclose
>>> isclose(0.1 + 0.2, 0.3)
True

可以使用函数round()来指定精确的小数点位数:

>>> a = round(3.1415926, 2)
>>> a
3.14

如果是在字符串格式化中输出,可以这样指定小数点位数:

>>> a = 3.1415926
>>> print(f'{a:.2f}')
3.14

在Python2,整型除以整型是等于整型的,而到了Python3,整型除以整型会产生浮点型,即便得到的是整数。

>>> 6 / 2
3.0

如果需要向下取整,则用//

>>> 5 // 2
2

要注意的是这是标准的向下取整,所以-5 // 2会得到-3而不是-2。如果你希望得到-2,就像绝大多数编程语言那样舍弃小数点,你可以使用int(-5 / 2),注意中间是/而不是//

这与大多数编程语言不一样,你无需担心除数是整型导致运算结果是整型,这是一种更符合直觉的运算方式,也让Python更好的用于数学和科学计算。

如你所见,使用int(var)可以将某变量转换为整数。如果你需要将字符串转换为整数或浮点数,只需:int(string)float(string),注意对输入的字符串作合法性检查。
注意常见的错误:将字符串转为布尔值时,使用bool(string)是错误的:

>>> bool("False")
True

对于bool()来说,输入为0、空字符串、空容器等会得到False,所以对于字符串”False”而言,显然它不是空字符串,所以会得到True。
如果当字符串为True时,转为布尔值True,则需这样做:

>>> string = "True"
>>> b = string == "True"
>>> b
True

如果需对其大小写不敏感,可以这样做:

>>> string = "tRue"
>>> b = string.lower() == "true"
>>> b 
True

如果仅允许输入“true”或”false”,可自行对输入作检查。

更多类型转换的例子可见这篇文章

python的数字精度

对于Python 3,其int类型的数字是动态可变精度的,这意味着可以使用非常大的整数而不用担心溢出。
如果需要像其他编程语言那样限制整数表示的范围,可使用如numpy中的numpy.int32numpy.int64来实现。

Python中的浮点数是基于IEEE 754双精度浮点标准实现的,因此与其他编程语言一样会发生上溢出和下溢出。如果需要用到更高精度的浮点数,可以使用decimal等模块。

其他运算

Python可以这样子在一行给多个变量赋值

a, b, c = 1, 2, 3

于是,交换两个变量的值,你可以这样操作:

a, b = 1, 2
a, b = b, a  # 交换a和b的值

我们还可以用元组(后面会学到)给多个变量赋值:

a = (1, 2, 3)
x, y, z = a

可以使用单下划线分割数字来方便阅读大数字,Python会忽略数字中的下划线:

>>> a = 1_000_000_000
>>> a
1000000000

Python还有复数类型:

>>> a = 1 + 2j  # 1为实部,2为虚部,注意是用j表示而不是i
>>> a
(1+2j)
>>> a.conjugate()  # 求共轭复数
(1-2j)

Python提供了一个非常方便的计算幂数的语法:

>>> 2 ** 3
8

许多其他编程语言都有的运算符,Python也有,比如求余%,位运算&|^<<>>~,赋值=+=-=*=/=%=**=//=&=|=<<=>>=

赋值还有:=,在C中有这样的写法:

int a;
printf("%d", a=1);

在第二行中,a=1给a赋值,然后将赋值后的a传入函数,相当于:

int a;
a = 1;
printf("%d", a);

在python中,类似的写法作用是关键词传值(后面会提到),而不是给变量赋值:

connect(ip="123.456.78.9", port=3306, timeout=10, database="test")

:=就可以在表达式中给变量赋值:

>>> print(a:=1)
1
>>> a
1

但Python没有i++这种运算符,所以如果要用,就需要使用i+=1

比较运算符==!=><>=<=

身份运算符isis not,它与==!=的区别是,is用于判断两个变量是否是同一个对象(对应同一个内存地址),而==用于判断值是否相等,学过java的应该就知道==类似于java的.equal,而is相当于java的==。到后面的内容会学到如何自定义这个==

如果要判断一个变量是否为None(类似于Java的null、c++的nullptr等),应该用a is Nonea is not None而不应该用==。这是因为所有的None地址都相同,而且用is的效率比用==要高,且运算符==可能被重载导致结果不符合预期。

逻辑运算符andornot

成员运算符innot in这个到后面学。

3. 流程控制

if语句

a, b = 1, 2
if a > b:
    print("a更大")
elif a < b:
    print("b更大")
else:
    print("a、b相等")

注意else后面也有一个冒号。if语句中可以只有if,也可以只有if和else,或者只有if和一个elif。elif可以有多个。

Python中语句的范围是由缩进决定的(还记得前面的缩进的规范吗)。

a = 1
if a == 0:
    print("1")
    print("2")
print("3")

运行结果

3

对于a > 1 and a <= 3可以这样写

if 1 < a <= 3:
    pass  # 代表什么都不做,因为语句中不能没有东西

另外,Python中的布尔值真和假的首字母是大写的:

success = True
failed = False

Python没有A ? v1 : v2这种三元运算符的写法,在很长一段时间内,人们用(A and v1) or v2来模拟它,但这种写法存在许多问题。Python2.5之后加入了v1 if A else v2的语句,可以用来代替这种写法。

max_num = a if a > b else b

但请注意如果条件非常复杂,还是老老实实用if语句吧,千万不要嵌套好几次这种运算符,否则代码的可阅读性将会变得很糟糕,并不会显得代码很酷。

for语句

语法:for i in <iterator>:,比如

for i in range(3):
    print(i)

运行结果

0
1
2

其中,range()函数还可以这样用

for i in range(2, 5):
    print(i)

运行结果

2
3
4

for i in range(2, 7, 2):
    print(i)

运行结果

2
4
6

PEP 8要求在调用函数时有多个参数,每个逗号后参数前要加一个空格。

while语句

语法:while 条件:

i = 0
while i < 5:
    print(i)
    i += 1

请注意缩进,它决定了循环的范围

break、continue和else

break和continue在Python循环语句中的作用和其他语言一样,在这不再赘述。不过Python没有像Java那样可以在多层循环中选择继续或跳出指定的某一层循环的功能。

else语句可以在for中的iterator迭代完之后,或者while的条件为False时运行。但是,如果循环是被break结束的,则不会运行else中的语句。

for i in range(3):
    print(i)
else:
    print("结束")

运行结果

0
1
2
结束

match语句

注意match语句是Python3.10以后才有的。它有点像C++、java等语言的switch语句,但它可以做到的更多。由于涉及到后面的知识,而且它直到3.10以后才支持,所以可以等后面面向对象学完了或者以后需要用到的时候再回来学。Python官方文档描述了它的详细用法。

语句中的变量在语句外的访问

Python中的变量不像某些语言那样离开了它定义时所在的代码块,生命周期就会结束:

if a > b:
    temp = 1
print(temp)

不过如果a > b不成立,那么temp将未被定义,此时第三行代码就会报错。这样子写一般编辑器会给出警告Name 'temp' can be undefined

这个可能导致某个名称的变量不小心被改写,导致奇怪的bug,比如下面这段代码:

i = True
...  # 省略n行代码
if i:
    ...  # 省略n行代码
    for i in range(8):
        pass
...  # 省略n行代码
print("i是真是假:"+i)

运行结果

i是真是假:7

4. 基本数据结构

列表(List)

列表的定义和访问

下面定义了一个列表

>>> names = ['Amy', 'Peter', 'John', 'Adam', 'Martin']

PEP 8要求在定义列表时,每个逗号后元素前要有一个空格。

我们还可以使用list(range())来生成列表

nums1 = list(range(3))
nums1 = list(range(1, 5))
nums2 = list(range(2, 11, 2))
print(nums1)
print(nums2)

运行结果

[0, 1, 2]

[1, 2, 3, 4]

[2, 4, 6, 8, 10]

访问列表中的元素,下标0为第一个

>>> names[0]
'Amy'
>>> names[1]
'Peter'

如果下标为负数,则会倒着数,比如-1为最后一个元素,-2为倒数第二个。

>>> names[-1]
'Martin'
>>> names[-2]
'Adam'

使用len()函数来获取列表元素数

>>> len(names)
5

列表中可以嵌套列表:

>>> matrix = [[1, 2], [0, 1]]
>>> matrix[0][1]
2

列表看起来有点像其他语言中的数组,但它并不是静态的数组,它的大小是可变的。且列表可当作栈或队列使用。

增删改

使用.append()函数给列表表尾增加内容

>>> names.append('Carl')
>>> names
['Amy', 'Peter', 'John', 'Adam', 'Martin', 'Carl']

使用.insert()来向某个位置插入元素

>>> names.insert(1, 'Jack')
>>> names
['Amy', 'Jack', 'Peter', 'John', 'Adam', 'Martin', 'Carl']

使用del来删除列表中特定位置的元素

>>> del names[1]
>>> names
['Amy', 'Peter', 'John', 'Adam', 'Martin', 'Carl']

使用.pop()来取出列表中最后一个元素

>>> name = names.pop()
>>> name
'Carl'
>>> names
['Amy', 'Peter', 'John', 'Adam', 'Martin']

使用.pop(index)取出特定位置的元素

>>> name = names.pop(1)
>>> name
'Peter'
>>> names
['Amy', 'John', 'Adam', 'Martin']

使用.remove()移除列表中某个值

>>> names.remove('Adam')
>>> names
['Amy', 'John', 'Martin']

修改列表中某个位置的值

>>> names[0] = 'Jenny'
>>> names
['Jenny', 'John', 'Martin']

列表排序

使用.reverse()对列表中的元素排序进行反转

>>> names.reverse()
['Martin', 'John', 'Amy']

使用.sort()对列表中的元素进行排序

>>> nums = [3, 1, 5, 4, 2]
>>> nums.sort()
>>> names
[1, 2, 3, 4, 5]
>>> nums.sort(reverse=True)  # 反转排序
>>> nums
[5, 4, 3, 2, 1]
>>> words = ['d', 'b', 'bb', 'a', 'ba']
>>> words.sort()  # 字符串也可以排序
>>> words
['a', 'b', 'ba', 'bb', 'd']
>>>
>>> # 如果不想改变原列表的顺序,想要产生一个新列表
>>> nums = [3, 1, 5, 4, 2]
>>> nums_sorted = sorted(nums, reverse=False)
>>> nums
[3, 1, 5, 4, 2]
>>> nums_sorted
[1, 2, 3, 4, 5]
>>>
>>> # 对于列表中元素比较复杂的情况
>>> pairs = [('A', 2), ('C', 4), ('D', 3), ('B', 1)]
>>> pairs.sort()
>>> print(pairs)  # 可以看到是按字母进行排序
[('A', 2), ('B', 1), ('C', 4), ('D', 3)]
>>> # 使用lambda表达式决定怎么排序
>>> pairs.sort(key=lambda pair: pair[1])  # 按数字排序
>>> print(pairs)
[('B', 1), ('A', 2), ('C', 3), ('D', 4)]

最大、最小值和求和

>>> min(nums)
1
>>> max(nums)
5
>>> sum(nums)
15

in 和 not in

这两个运算符可以用来判断元素是否在列表中:

lst = [1, 2, 3]
if 1 in lst:
    pass  # do something
if 4 not in lst:
    pass  # do something

如果要判断一个列表是否有内容,直接这样就可以

lst = []
if lst:
    print('列表中有内容')
else:
    print('列表为空')

同样地,它也可以用在while里:

lst = [1, 2, 3]
while lst:
    print(lst.pop())

运行结果

3
2
1

在上面这段代码中,如果列表中有内容,while的条件就会是True,注意不要陷入死循环。

使用循环访问列表

lst = ['a', 'b', 'c']
for ele in lst:
    print(ele)

运行结果

a
b
c

可以通过循环+下标的方式访问列表

lst = ['a', 'b', 'c']
for index in range(len(lst)):
    print(lst[index])  # 运行结果同上

使用enumerate()可以同时使用下标和对应的元素,这是一个很常用的函数。

lst = ['a', 'b', 'c']
for index, ele in enumerate(lst):
    print(str(index) + ':' + ele)
print('----')
# enumerate()还有一个参数,会让得到的index加几
for index, ele in enumerate(lst, 5):
    print(f'{index}: {value}')

运行结果

0:a
1:b
2:c

5:a
6:b
7:c

使用zip()可以把两个或多个列表中相同下标的元素合在一起,如果列表的长度不同,则得到的新列表长度为最小的列表的长度。

lst1 = [1, 2, 3]
lst2 = ['a', 'b']
for ele in zip(lst1, lst2):
    print(ele)

运行结果

(1, ‘a’)

(2, ‘b’)

合并两个列表

>>> lst1 + lst2
[1, 2, 3, 'a', 'b']

重复列表中的元素

>>> lst3 = lst1 * 3
>>> lst3
[1, 2, 3, 1, 2, 3, 1, 2, 3]

使用列表推导(List Comprehensions)生成列表

如果要生成一个列表,为1到10的平方,一般的写法是

>>> arr = []
>>> for i in range(1, 10):
...     arr.append(i ** 2)
...
>>> arr
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

使用列表推导可以简化代码:

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

加上条件判断,比如加上if value != 5

>>> [value ** 2 for value in range(1, 11) if value != 5]
[1, 4, 9, 16, 36, 49, 64, 81, 100]

循环嵌套,比如为了生成 arr=(x,y), x!=y, 且x和y为1~3:

arr = []
for x in range(1, 4):
    for y in range(1, 4):
        if x != y:
            arr.append((x, y))

使用列表推导可以写成:

>>> arr = [(x, y) for x in range(1, 4) for y in range(1, 4) if x != y]
>>> arr
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

当然,不要写太复杂的列表推导,否则对代码的可阅读性会造成灾难性的影响。

列表切片

使用lst[起始下标:终止下标]可以获得一个新的列表,内容是旧列表起始下标到终止下标-1的元素。

>>> lst = [1, 2, 3, 4, 5]
>>> lst[1:4]
[2, 3, 4]

如果不写起始下标,则是从下标0开始,如果不写终止下标,就是直接终止到列表末端。

>>> lst = [1, 2, 3, 4, 5]
>>> lst[:3]
[1, 2, 3]
>>> lst[2:]
[3, 4, 5]

还可以使用负数下标,比如获取最后三个元素:

>>> lst = [1, 2, 3, 4, 5]
>>> lst[-3:]
[3, 4, 5]

使用列表切片可以存在超出范围的下标:

>>> lst = [1, 2, 3, 4, 5]
>>> lst[2:999]
[3, 4, 5]
>>> lst[999:]
[]

删除列表元素也可以用这种方法来同时删除多个元素

>>> lst = [1, 2, 3, 4, 5]
>>> del lst[2:]
>>> lst
[1, 2]

如果起始和终止下标都不写,则会复制整个列表。如果需要复制列表,请使用这种方法而不是直接使用=,否则新列表和旧列表其实是同一个列表,即内存地址相同的列表。

>>> lst1 = [1, 2, 3]
>>> lst2 = lst1
>>> del lst2[:]  # 删除lst2的所有元素
>>> lst1  # lst1的元素也全被删除,因为他们俩是同一个列表
[]
>>> lst1 = [1, 2, 3]
>>> lst2 = lst1[:]  # 复制列表
>>> del lst2[:]
>>> lst1  # lst1的元素都还在,因为lst1和lst2不是同一个列表
[1, 2, 3]

元组(Tuples)

元组可以理解为不可改变的列表,使用圆括号包含起来。

>>> vector = (1, 2, 3)
>>> vector
(1, 2, 3)
>>> vector[1] = 4
TypeError: 'tuple' object does not support item assignment

但你可以给vector重新赋值,这里的不可变不是指变量是常量,而是长度和内容不可变。

如果你要创建一个只有一个元素的元组,如果这样写:s = (1)(想想在做运算时的圆括号是用来干什么的),那么s就是整数1。所以你得这样写:s = (1,)

集合(Set)

集合是一个无序且元素不重复的序列,用大括号包含起来:

>>> s = {1, 2, 2, 3}
>>> s
{1, 2, 3}

我们不能通过下标和循环的方式去获取它的内容,但我们可以使用innot in来判断元素是否在集合内。

我们可以使用.add()方法给集合添加元素,使用.remove()方法移除集合内特定元素,使用.pop()方法移除并返回集合内随机一个元素。

使用set()方法将列表转为集合:

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

集合的运算:

>>> set1 = {1, 2, 3, 4, 5}
>>> set2 = {2, 5, 6}
>>> set1 | set2  # 取并集
{1, 2, 3, 4, 5, 6}
>>> set1 - set2  # set1中不包含set2元素的内容
{1, 3, 4}
>>> set1 & set2  # 取交集
{2, 5}
>>> set1 ^ set2  # set1和set2的元素中不在两个集合内同时存在的内容
{1, 3, 4, 6}

如果要创建空集,请使用s = set()而不是s = {},因为后者是创建空字典。

字典(Dictionaries)

字典是一种包含了键值对的数据类型,用大括号包含起来:

stu = {'name': 'Peter', 'age': 18}

PEP 8要求:冒号后有空格,而冒号前无空格。

为了让字典看起来更好看一些,我们可以给它换行,注意对齐。为了方便后续添加(指在代码中对下面的stu做修改而不是使用字典函数动态添加),最后一个键值对后面可以留一个逗号而不会报错。

stu = {
    'name': 'Peter',
    'age': 18,
}

增删改查

# 查询某个键对应的值
print(stu['name'])  # 键不存在时报错
print(stu.get('name'))  # 键不存在时返回None
# 判断某个键是否存在
if 'class' in stu:
    pass  # do something
# 增加一个键值对
stu['class'] = 3
# 修改某个键对应的值
stu['age'] = 19
# 删除某个键值对
del stu['class']

如果某个键值对不存在时,使用stu[key]访问时会报错,使用.get()访问时会返回None,可以使用.get(key, default)指定键不存在时返回的默认值。

对字典进行循环操作

# 同时获取键和对应的值
for key, value in stu.items():
    print(f"{key} = {value}")
# 获取键
for key in stu.keys():
    print(key)
# 获取值
for value in stu.values():
    print(value)

可以对字典进行排序后再循环:

stu = {
    3: "Peter",
    1: "Amy",
    2: "Phil",
}
for i in sorted(stu):
    print(f"{stu[i]}'s id is {i}")

运行结果

Amy’s id is 1
Phil’s id is 2
Peter’s id is 3

嵌套(Nesting)

以上的数据类型都可以嵌套,比如:

lst = [{"key": "value"}, {"key": "value"}]
s = {"key": "value", "list": [1, 2, 3]}
students = {
    "Linda": {
        "age": 18,
        "language": ["Chinese", "English"]
    },
    "Peter": {
        "age": 19,
        "language": ["English", "French"]
    }
}

5. 函数与模块

函数的定义

函数使用def 函数名(参数):来定义,使用函数名(参数)来调用,比如:

# 定义函数
def print_hello_world():
    print('Hello World!')
 
 
def get_max(a, b):
    return a if a > b else b
 
 
# 调用函数
print_hello_world()
max_num = get_max(1, 2)

函数需要先被定义才能生效,因此函数的调用要写在函数之后,否则会提示“函数未定义”。

注意函数名不要跟已有的变量名冲突,变量名也不要和已有的函数名冲突。

PEP 8要求(非类里的)函数与函数间、函数与其他代码间要间隔两行,比如上面的代码。

对于没有返回值的函数,你可以认为它返回None,比如:

>>> a = print('123')
123
>>> print(a)
None

因此我们可以这样做:

>>> print('yes') if a > b else print('no')

空函数

如果一个函数的函数体为空,你不能留空白,而是需要添加一个pass...:

def func1():
    pass


def func2():
    ...

我们在前面流程控制已经用过pass了,它表示什么都不做,因为在if或while等流程控制语句后不能是空白行,因此加一个pass,函数中也可以这样做。
...也可表示什么都不做,不过通常用来表示函数、类等未实现,即你可以先根据思路定义一些函数或类并使用...占位,然后再一个个实现它们。

函数的文档

在函数体内首行开始使用多行注释编写文档,用来说明该函数的作用,使用方法等。

文档的第一行应简明说明函数的简短摘要,最好只用一句话说明白。如果文档不止一行,应在第一行说明摘要后空一行再继续写。

def send_message(user, mes):
    """
    向用户发送消息。

    参数:
    user: 向哪个发送消息
    mes: 消息的内容
    """
    ...

通过sendmessage.__doc__即可获取该函数的文档。

函数参数的默认值

定义函数时,可以指定参数的默认值,这样在调用函数时就可以省略一部分参数。有默认值的参数必须放在无默认值参数的后面。

def send_message(user, mes, color='red'):
    ...
 
 
send_message(user, 'Hello')
send_message(user, 'World', 'green')

警告:如果默认值是可变类型,比如列表、字典、类对象,则会产生累积作用,如下:

def func(a, lst=[]):
    lst.append(a)
    print(lst)
 
 
func(1)
func(2)
func(3)

运行结果

[1]
[1, 2]
[1, 2, 3]

如果你想避免这种情况,可以这样写:

def func(a, lst=None):
    if lst is None:
        lst = []
    lst.append(a)

在PyCharm中,写成前者会提示你存在这种情况,并可以一键修改。

在传递列表等可变类型参数时,列表可能被函数修改,比如:

def func(lst: list):
    print(lst)
    lst.append('a')
 
 
l = [1, 2, 3]
func(l)
print(l)

运行结果

[1, 2, 3]

[1, 2, 3, ‘a’]

如果想避免这种情况,可以传入一个复制的列表:

l = [1, 2, 3]
func(l[:])

返回多个数据

Python的函数可以同时返回多个数据:

def get_location():
    return 1, 2, 3
 
 
x, y, z = get_location()

如果你只想获得其中一部分数据,比如上面这行代码你只对y感兴趣,你可以把另外几个数据换成_

_, y, _ = get_location()

函数参数的注解

如果你的函数参数、返回值的类型是非常确定的,你可以给他们加上注解作为提示。

def func(a: int, b: str = "default value") -> str:
    ...

这段代码表明参数a应传入整型,参数b应传入字符串类型,而且它的默认值为"default value",函数的返回值是字符串类型。

但实际上,这并不能阻止用户传入其他类型。不过如果这样做,编辑器一般会给出警告提示用户传错参数了。

你也可以使用字符串作为注解:

def func(a: int, b: 'int>0', c: 'do not pass a int here') -> 'Nothing will be returned':
    ...

关键词参数

对于这样的一个函数:

def send_message(title, text, color='red', size='32px'):
    ...

除了按位置传递参数,你也可以使用关键词来传递参数,这样关键词部分就可以随意调整位置了。比如:

send_message(title='Here', text='Hello', size='16px')
send_message('Here', size='16px', color='green', text='Hello')

但关键词部分全部都要放后面,不能关键词传参后面又带一个位置传参,比如这样写是错的

sendmessage(title='Here', 'Hello')  # 错误写法

*name和**name参数

如果函数形参中有这两种参数,他们要放在参数的最右边,且*name要放在**name的前面。*name用于接收形参列表之外的位置参数并形成一个元组(Tuple),比如:

def send_message(user, *messages):
    ...

调用该函数:

send_message(user, 'Hello', 'World', 'Hi')

**name接收形参列表之外的关键词参数并形成一个字典(Dictionary),比如:

def send_message(user, *messages, **datas):
    ...

调用该函数:

send_message(user, 'Hello', 'World', color='red', size='32px')

这个功能对于一个经常更新的函数非常有用,假设你维护一个函数,这个函数已经被大量的项目调用,且需要经常更新,你可以把新增的内容放在*name**name中,以在增加功能的同时确保其兼容性,如:

def login(user_name, password, **configurations):
    """
    登录
    
    参数:
    user_name: 用户名
    password: 用户密码
    configurations: 登录选项(用关键字传入)
    
    configurations中可选配置:
    two_factor_auth: 是否启用二重验证
    """
    
    if not check(user_name, password):
        return "登录失败:用户名或密码错误"
    if configurations.get("two_factor_auth"):  # 如果 two_factor_auth 不存在或为 False 则不执行下一行
        return two_factor_auth(user_name)

当需增加一个配置时,只需增加一个if,并在doc中增加对新参数的说明(如需要),无需修改函数形参。

仅位置参数和仅关键词参数

这个特性在Python3.9及以后才支持。

在函数形参中使用/*标记分隔,可确定仅位置参数和仅关键词参数。

格式:def 函数名(仅位置参数, /, 一般参数, *, 仅关键词参数)

比如,一个函数必须全部按位置传参,你可以这样写:

def func(a, b, c, /):
    ...

调用该函数时,只能使用func(a, b, c)而不能使用关键词。

一个函数必须全部使用关键词:

def func(*, a, b, c):
    ...

调用该函数时,只能使用func(a=xxx, b=xxx, c=xxx)的形式。

对于

def func(a, b, /, c, d, *, e, f):
    ...

ab必须按位置传参,cd既可以按位置也可以按关键词传参,ef必须按关键词传参。

函数定义的位置

函数还可以定义在函数内,比如:

def funcA():
    a = 2
    def funcB():
        print(a * 2)
    funcB()

可以看到,函数funcA内部又定义了一个函数funcB,而且funcB可以访问外部变量a,但不能修改它。由于funcBfuncA的内部,因此在funcA的外部是不能调用funcB的。

除此之外,在ifwhile等语句内都可以定义函数,那么你就可以做出这样的骚操作:

import platform
if platform.system() == 'Windows':
    def func():
        print('a')
else:
    def func():
        print('b')

func()

如果运行这段代码的操作系统是Windows系统,则执行func()打印的结果是a,否则则是b

将函数作为变量

函数也是可以赋给变量的,如下:

def func(a, b):
    return a + b
 
 
f = func
print(f(1, 2))  # 输出3

比如说,你想添加一个监听器,当用户点击按钮时执行一段命令,此时就可以:

def on_click():
    ...


button.add_click_listener(on_click)

当然,不一定要把函数写在外头:

def init_listeners():
    def on_click():
        ...
    button.add_click_listener(on_click)

匿名函数:lambda表达式

如果上面那个例子你不想单独写一个函数,而且点击事件非常简单(比如就只有一个print()),可以使用匿名函数。

button.add_click_listener(lambda: print('Clicked'))

你也可以用它创建简单函数:

f = lambda x, y: x + y
print(f(1, 2))# 输出3

或更直接,创建一个匿名函数并运行:

>>> (lambda x, y: x + y)(1, 2)
3

这适用于使用次数比较少、无需复用的简单函数,不过同样的,用它是为了让代码更加优雅,如果函数比较复杂,使用lambda表达式会影响可阅读性,就不建议使用这种方法。

变量的作用域

在函数中,可以读取函数外的变量,但不能修改它。

a = 1
def func():
    b = a + 1

如果我们希望外部变量是一个全局变量,在函数中也能被修改,该怎么做呢?

答案是在修改前加一个global 变量名

a = 1
def func():
    global a
    a = 2

如果在函数内定义一个函数,且需要修改外函数内的一个变量,则是使用nonlocal

a = 1
def funcA():
    global a
    a = 2
    b = 1
    def funcB():
        nonlocal b
        b = 2

多文件项目

当程序变得复杂时,我们便需要将代码写到多个文件中以便管理。

模块的导入

比如创建一个文件my_module.py并编写代码:

# my_module.py
print('This is my module')

然后在main.py中导入它:

# main.py
import my_module

运行main.py,就可以看到输出

This is my module

我们也可以把函数写到my_module.py里面:

# my_module.py
print('This is my module')
 
 
def func(mes):
    print(mes)

然后在main.py中调用它:

# main.py
import my_module

my_module.func('Hello')

运行结果

This is my module

Hello

这里可以看到,当我们导入模块时,也会运行模块中的代码。

在Python中有一个叫this的模块,导入它就会打印一段信息,这段信息被称为”The Zen of Python”(Python之禅),它总结了Python的风格,感兴趣的可以上网查询。

import this

if __name__ == ‘__main__‘

如果希望别人导入我的my_module时不运行不必要的代码,该怎么做呢?

还记得最前面提到的这段代码吗?

if __name__ == '__main__':
    ...

这个__name__是什么呢?我们创建my_project.pymy_module.py两个项目,分别打印出__name__的值,并在my_project中导入my_module

# my_project.py
import my_module
# my_module.py
print(__name__)

运行my_module,输出:

__main__

运行my_project,输出

my_module

在运行my_module时,解释器会把'__main__'赋值给__name__,而在my_project中导入my_module时,my_module中的__name__就是它的名称。我们可以通过my_module.__name__来获取模块的名称。

为了让自己的模块被导入时不会运行到不必要的代码,我们加上这个if来判断运行的是不是这个模块。

导入模块的其他操作

导入模块时,可以给这个模块起别名:

import my_module as mm

mm.func()

可以单独导入模块的某些内容,这样可以不用加上模块前缀my_module.即可调用函数,同样可以给导入的函数起别名

from my_module import func1, func2, func3 as f3, func4 as f4

func1()  # 不需要 my_module.func1()
f3()

从模块导入所有内容,不建议使用,因为这样非常容易发生命名冲突:

from my_module import *

导入模块可以不写在开头,它也可以写在某个函数或流程控制语句中,如:

import platform


def init(): 
    if platform.system() == 'Windows':
        import module_win
        module_win.init()
    else:
        import module_others
        module_others.init()

不过要注意导入某个模块后才能使用模块内的内容。

导入第三方模块

很多时候我们需要用到第三方模块,要怎么导入这些模块呢?

这里以PyCharm为例。使用PyCharm创建项目时,默认情况下会在这个项目下创建一个虚拟环境venv,这样可以让不同的项目使用不同的Python版本和不同的库。你也可以在设置里调整项目使用的虚拟环境,比如调到你系统安装的环境中。但是可能会产生这样的问题,比如项目A使用了1.0.0的X库,项目B使用了2.0.0的X库,这两个不同版本的库是不兼容的,而一个虚拟环境不能同时安装两个不同版本的同一个库,因此需要分出多个虚拟环境。

为了给你的项目添加库而不是给系统环境添加库,你可以在PyCharm中点击左上角的File(文件)-Settings(设置)-Project(项目):你的项目名-Python Interpreter(Python解释器),将解释器调为你项目中的虚拟环境并点击+号,即可添加第三方模块。

由于Python仓库服务器在国外,下载速度极慢甚至容易失败,建议使用国内镜像源。

  1. 在上文提到的那个位置点击+号后,有一个Manage Repositories的按钮,点击它即可添加源。
  2. 如果1中找不到那个按钮,则可以关闭设置页码,然后在编辑器的下方找到Python Packages(Python包)并点击它,然后在弹出的标签中点击搜索栏后的齿轮按钮,即可添加源。

常用源:

清华:https://pypi.tuna.tsinghua.edu.cn/simple/

阿里云:http://mirrors.aliyun.com/pypi/simple/

豆瓣:http://pypi.douban.com/simple/

中国科技大学:https://pypi.mirrors.ustc.edu.cn/simple/

华中理工大学:http://pypi.hustunique.com/

山东理工大学:http://pypi.sdutlinux.org/

如何使用命令添加第三方模块呢?

当然是使用pip命令。如果没有安装pip,也可以用python -m pip

  • 安装某个模块:pip install xxx
  • 安装某个模块的某个版本:pip install "xxx==1.0.0"
  • 使用豆瓣镜像源安装:pip install -i http://pypi.douban.com/simple xxx --trusted-host pypi.douban.com
  • 升级某个模块到最新版本:pip install --upgrade xxx
  • 查看某个模块的具体信息:pip show xxx
  • 查看当前环境安装了哪些模块:pip list
  • 卸载某个模块:pip unistall xxx

当你把你的项目交给用户使用时,你需要让用户也安装项目依赖的模块,但你的项目可能会有非常多的依赖,而且还需要是指定版本以防止不兼容,让用户自己一个一个去安装不太现实,此时你就可以使用:

pip freeze > requirement.txt

它会生成一个文件requirement.txt,里面说明了需要的模块和指定的版本。将这个文件跟项目一并打包然后发送给用户。

用户只需pip install -r requirement.txt即可安装需要的所有模块

dir()函数

dir()是一个内置函数,它可以得到模块定义的名称,是一个排序过的列表:

>>> a = 1
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__',
'__spec__', 'a']

同样你也可以用它获取某个模块内定义的名称:

>>> import sys
>>> dir(sys)
['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__', '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__', ...此处省略...]

Python中可以在项目所在目录下创建子目录作为包,然后在子目录下创建新的.py文件。

比如,在项目根目录下创建一个目录effects,然后在这个子目录下再创建一个目录managers,然后再在这个子目录下创建一个文件my_module.py

导入和使用这个模块的方法是

import effects.managers.my_module

# 调用my_module模块里的函数func()
effects.managers.my_module.func()

可以发现,使用导入的模块需要加上很长的一段前缀,如果不想加上这些前缀,可以这样写:

from effects.managers import my_module

在Python 3.2之前,需要在包下创建__init__.py文件来告诉Python这是个包。PyCharm在创建包时,也会自动创建这个文件。最简单的情况下,这个文件里面啥都不用写。这是一个目录结构:

project/
├ main.py
└ effects/
  ├ __init__.py
  └ managers/
    ├ __init__.py
    └ my_module.py
    └ my_module2.py

在导入包下的模块时,会先运行__init__.py中的代码,你可以在这里做一些初始化的操作等。

在导入包内的所有模块时,你会发现直接这样写并不会导入任何模块:

from effects.managers import *

my_module.func()  # 报错,找不到my_module

你需要在managers下的__init__.py中添加

__all__ = ['my_module', 'my_module2']

这样上面报错的代码就不会报错了。不过在生产环境中,不建议使用import *

0x03. 高级内容

这一部分将会介绍Python更加高级的内容。如果你不准备使用Python的复杂功能,可以根据需求选择性阅读或调整阅读顺序,或者是在阅读其他人代码遇到困难时回来继续阅读。这里建议阅读类、文件和异常处理,他们既重要又不难。

6.类

类的定义与使用

# 定义一个类
class ABC:
    pass
 
 
# 实例化一个类对象
abc = ABC()

与函数相同,类必须先声明才能使用,比如上面的abc = ABC()不能写在class ABC:的上边。

每个类与其他代码之间应间隔两个空行。

类中的函数

类中的函数必须使用self作为第一个参数,但在调用函数时并不需要传参给self(事实上self能够改为其他名称,但约定俗成把名称定为self,不建议修改)。

class Animal:
    name = ''
    # 构造函数
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 构析函数
    def __del__(self):
        pass

    def get_name(self):
        return self.name

调用函数:

animal = Animal('Jenny', 2)
name = animal.get_name()

一般情况下我们不需要像C++那样在构析函数内释放内存,因为Python会自动进行垃圾回收,在使用Python时不需要考虑这种问题。

PEP 8:类与类之间要间隔两行,类中的函数与函数之间要间隔一行。

不像C++、Java那样类中的数据成员需要声明,Python的类数据不需要声明,在第一次赋值时就可以产生:

animal.extra_variable = 2
print(animal.extra_variable)

上面这行代码可以看到,类Animal中本无extra_variable这个变量,但你也可以创建它并赋值。

与函数中的默认值类似,如果在类中声明可变类型变量,则不同实例会共享该变量并产生累积作用:

class Animal:
    foods = []

    def add_food(self, food):
        self.foods.append(food)
 
 
cat = Animal()
dog = Animal()
cat.add_food('Fish')
dog.add_food('Bone')
print(cat.foods)
print(dog.foods)

运行结果

[‘Fish’, ‘Bone’]

[‘Fish’, ‘Bone’]

解决的方法是,不声明foods,在构造函数中创建实例变量:

class Animal:
    def __init__(self):
        self.foods = []

    def add_food(self, food):
        self.foods.append(food)
 
 
cat = Animal()
dog = Animal()
cat.add_food('Fish')
dog.add_food('Bone')
print(cat.foods)
print(dog.foods)

运行结果

[‘Fish’]

[‘Bone’]

类的继承

class Cat(Animal):
    ...

当然,如果类在另一个模块里:

import my_module
class Cat(my_module.Animal):
    def __init__(self, name, age):
        super().__init__(name, age)

(对java程序员的提示:别忘了super后面的括号)

派生类会重写其基类的方法,比如

class Parent:
    def f(self):
        print('1')
 
 
class Child(Parent):
    def f(self):
        print('2')
 
 
c = Child()
c.f()

运行结果

2

(对 C++ 程序员的提示:Python 中所有的方法实际上都是 virtual 方法。)

如果要在Child中运行父类的f(),可以这样做:

c = Child()
Parent.f(c)

Python的类支持多重继承:

class Parent(Base1, Base2, Base3):
    ...

如果某个属性(变量、函数等)在Parent中没有找到,那么Python会先在Base1中找,然后再到Base2中找,以此类推,是按照顺序找的。同样的,super()也是按顺序找。

id()、type()和isinstance()

id()用来获取数据所在的内存地址,通过id(obj1) == id(obj2)来判断两个变量是否为同一个对象,相当于obj1 is obj2

type()用来获取数据的类型,如type(2.5) == float判断是否为浮点型;type(vec) == Vector2D判断变量vec是否为Vector2D的类型。

假设类Dog继承与类Animaldog = Dog(),我们就不能使用type(dog) == Animal来判断dog是否为Animal类型了。

此时我们可以使用isinstance(dog, Animal),类似于Java的instanceof

事实上,Python并没有严格意义的多态,不像C++和Java的多态,当一个类形参被声明为某个类型时,只能传入该类型或该类型的子类,而Python的变量可以随时被赋值为任何类型,没有这种限制。

访问权限(下划线命名)

如果在一个模块中的变量或函数用单个下划线开头命名,如def _func(),那么它应该被视为非公有部分(即我们不应该访问它)。在PyCharm中调用另一个模块用单下划线命名的方法时,则会有警告。但这并不能阻止用户访问它。

如果在一个类中的变量或方法使用两个下划线开头命名,则应该把它视为类私有的变量或方法,子类不应该访问它。Python会自动把这些变量改名,比如__name会改为_classname__name,所以我们实际上还是可以通过_classname__name来访问它:

class Animal:
    def __do_not_call_me(self):
        ...
 
 
animal = Animal()
animal._Animal__do_not_call_me()

用空类来储存数据

class Datas:
    pass
 
 
data = Datas()
data.name = 'Peter'
data.result = 5

迭代器

重写__iter____next__方法即可实现迭代器。

class IteratorDemo:
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        self.index -= 1
        if self.index == -1:
            raise StopIteration
        return self.data[self.index]
 
 
ite = IteratorDemo([1, 'a', 3.14])
for i in ite:
    print(i)

运行结果

3.14
a
1

使用next()函数:

ite = IteratorDemo([1, 'a', 3.14])
print(next(ite))
print(next(ite))
print(next(ite))

运行结果

3.14
a
1

生成器与yield

生成器

仔细看这两行代码的区别:

list1 = [i for i in range(9999999)]
list2 = (i for i in range(9999999))

第一行就是上面学过的列表推导(List Comprehensions),而第二行的圆括号…难道是元组推导?其实并不是。看这段代码运行时消耗的时间

import time

start = time.time()
list1 = [i for i in range(99999999)]
end = time.time()
print('list1消耗: '+str(end - start)+'s')

start = time.time()
list2 = (i for i in range(99999999))
end = time.time()
print('list2消耗: '+str(end - start)+'s')

运行结果

list1消耗: 5.164059162139893s

list2消耗: 0.0s

可以看到list1需要消耗长达5s的时间,而list2由于太快几乎瞬间就完成了。

这是由于,list1是真的生成了一个长度99999999的列表,它需要占用非常大的内存空间;而list2是一个生成器,它记录的是一个算法,用来生成这些数字,实际上这些数字还没有被生成,所以它占用的内存空间很小。

为了获取list2的内容,我们需要使用next()

>>> list2 = (i for i in range(99999999))
>>> next(list2)
0
>>> next(list2)
1
>>> next(list2)
2

它同时也是迭代器,可以使用for循环获取内容:

list2 = (i for i in range(10))
for i in list2:
    print(i)

这就是生成器,在生成有一定规律的大量数据,但又不需要立刻得到所有结果时,使用它可以节省时间和内存空间。

yield

在函数中,return的作用是返回一定的数据并结束函数的运行。而在函数中使用yield关键词可以让一个函数变成生成器:

def func():
    print('a')
    yield
    print('b')
    yield
    print('c')
    yield
    print('d')
    yield


f = func()
next(f)
next(f)
next(f)
next(f)
next(f)

运行结果

a
b
c
d
Traceback (most recent call last):
File “<stdin>“, line 1, in <module>
StopIteration

我们可以给yield添加返回值:

def func():
    yield 1
    yield 'a'
    yield 3.14
 
 
for i in func():
    print(i)

运行结果

1
a
3.14

yield一般搭配for循环一起使用,比如按顺序生成自然数的平方:

def gen(n):
    for i in range(n + 1):
        yield i ** 2
 
 
for i in gen(5):
    print(i)

运行结果:

0
1
4
16
25

__str__和__repr__

当我们创建一个类并创建它的实例时,使用str()会发现输出的是它所在的内存地址:

>>> class A:
...     pass
...
>>> a = A()
>>> str(a)
<__main__.A object at 0x0000028D440D36D0>

为了修改输出的内容,我们可以重写__str__():

class A:
    def __str__(self):
        return 'I am A'
 
 
a = A()
print(str(a))

运行结果:

I am A

当如果你把A放容器里再使用str(容器)时,会发现它又变成了地址:

print(str([a]))

运行结果

[<__main__.A object at 0x0000028D440D36D0>]

此时就需要重写__repr__(),且我们可以通过repr()来获取它的内容。

值得注意的是,如果只重写了__repr__(),那么str()repr()获取的都是__repr__()返回的结果。

一般情况下,__str__()返回的是给用户看的,而__repr__()返回的是用于调试的内容,所以__repr__()需要返回尽量详细的内容,比如:

class Student:
    def __init__(self, name, gender, age):
        self.name = name
        self.gender = gender
        self.age = age

    def __str__(self):
        return f'一个叫{self.name}的学生'

    def __repr__(self):
        return f'一个性别{self.gender},年龄{self.age}岁,名为{self.name}的学生,该变量在内存中的地址(16进制):{hex(id(self))}'
 
 
stu = Student('小明', '男', 18)
print(f'我们有{stu}')
print(f'debug: {repr(stu)}')

运行结果

我们有一个叫小明的学生
debug: 一个性别男,年龄18岁,名为小明的学生,该变量在内存中的地址(16进制):0x28d44712550

运算符重载

Python的运算符也能重载,不过不是所有运算符都能重载,比如isorandnot不可以被重载。(请注意区分逻辑运算orand和逻辑位运算|&)

比如编写一个向量类,并重写加号(+)和等号(==):

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector2D(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __repr__(self):
        return f'({self.x}, {self.y})'
 
 
vec1 = Vector2D(1, 2)
vec2 = Vector2D(1, 2)
print(vec1 + vec2)
print(vec1 == vec2)

运行结果

(2, 4)

True

我们同样可以通过重写__mul__方法来实现vec * 2,但是,如果我们想要2 * vec呢?此时可以重写__rmul__

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __rmul__(self, other):
        return Vector2D(self.x * other, self.y * other)
 
 
vec = Vector2D(1, 2)
print(2 * vec)

这里可以看到,__mul____rmul__的功能是一样的,把他们都单独实现一遍会造成代码重复,此时我们可以:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __mul__(self, other):
        return Vector2D(self.x * other, self.y * other)

    __rmul__ = __mul__  # 让__rmul__和__mul__实现相同操作

当然,如果在a * b,a有实现__add__方法,则优先运行a__add__方法,即便b实现了__radd__

此外,对于vec *= 2,可以通过重写__imul__实现,但它也可以被理解为vec = vec * 2

对于vec *= 2,如果__imul__没有被重写,它就会尝试去运行__mul__,如果__mul__也没有被重写,就会报错。

下面通过运算符重载来模仿c++的cout效果(仅模仿效果,不模仿原理):

# cpp_cout.py
Class CppCout:
    def __lshift__(self, other):
        print(other, end='')
        return self
 
 
cout = CppCout()
endl = '\n'
# cpp_cout_test.py
from cpp_cout import cout, endl

if __name__ == '__main__':
    cout << 'Hello' << 'World' << endl

运行结果

HelloWorld

这些以两个下划线起始和两个下划线结尾的方法被称为魔法方法,魔法方法非常多,你可以点击这里查看重要的魔法方法。

7.装饰器

装饰器(Decorator)是Python的一种语法糖,它和Java的注解长的有点像,可以让代码变得更加简洁优雅。

比如这里有一个函数:

def func():
    lst = [i for i in range(99999999)]

我们想给这个函数添加一个统计运行时间的功能,直接修改这个函数:

import time

def func():
    start = time.time()
    lst = [i for i in range(99999999)]
    end = time.time()
    print('该函数运行用时:'+(end - start)+'秒')

但这样修改了函数的代码,让这个函数的代码变得更复杂,也不便于去除该功能,与是我们这样做:

import time

def func():
    lst = [i for i in range(99999999)]

def cal_time(f):
    start = time.time()
    f()
    end = time.time()
    print('该函数运行用时:'+(end - start)+'秒')

def main():
    cal_time(func)

但这样需要把原先运行func()改为cal_time(func),改变了运行的逻辑。

这时,装饰器便起了作用:

def cal_time(f):
    def wrapper():
        start = time.time()
        f()
        end = time.time()
        print('该函数运行用时:'+(end - start)+'秒')
    return wrapper

@cal_time
def func():
    lst = [i for i in range(99999999)]

这样,当我们运行func()时,实际上是运行了:

w = cal_time(func)
w()

但我们会发现,如果函数内有参数,包括各种各样的位置参数和关键词参数时,上面的方法就失效了,因此,我们要这样写装饰器:

def cal_time(f):
    def wrapper(*args, **kwargs):
        start = time.time()
        f(*args, **kwargs)
        end = time.time()
        print('该函数运行用时:'+(end - start)+'秒')
    return wrapper

@cal_time
def func(n):
    lst = [i for i in range(n)]

这样,我们便可以把函数的参数一并交给装饰器了。

当我们不需要这个功能时,只需把@cal_time去掉即可。

既然我们可以拿到参数,也可以在装饰器内给参数做校验,比如:

def not_none(f):
    def wrapper(*args):
        for index, ele in enumerate(args, 1):
            if ele is None:
                raise ValueError(f'第{index}个参数为None!该函数接受的参数不能为None。')
        f(*args)
    return wrapper

@not_none
def func(n1, n2, /):
    ...

如果我们要给函数添加多个功能,那就多加几个装饰器:

@cal_time
@not_none
@more_decorators
def func(n1, n2):
    ...

可见,当我们想给一个函数添加一个新功能,比如统计该函数运行的时间,但我们不能修改它,这时使用装饰器就可以在不修改函数代码的情况下给它添加功能,也可以轻松的去掉新功能。

给装饰器添加参数

装饰器也能有参数。

在能让用户输入的地方,都要做各种检测来避免用户输入非法的数据。输入长度检测是十分常见的检测,比如,用户名长度不能超过16,密码长度不能超过32,发表的评论长度不能超过500等,不同地方对输入长度的限制不同。
我们总不能写很多个装饰器给不同地方的检测使用吧,比如@text_limit16@text_limit500。为了减少重复,我们需要一个参数来告诉装饰器这个长度限制是多少。

def check_length(length_limit):
    def check(f):
        def wrapper(info):
            length = len(info)
            if length > length_limit:
                raise ValueError(f'输入的参数长度({length})超过限制{length_limit}')
            f(info)
        return wrapper
    return check

@check_length(64)
def send_message(info):
    ...

Python内置装饰器

@property

当我们在类中定义私有变量时,比如_data,我们不希望用户修改它(虽然事实上可以修改),但可以读取它,我们可以给他加一个get_data()方法,或者使用@property装饰器:

class ABC:
    def __init__(self):
        self._data = 123

    @property
    def data(self):
        return self._data

这样,用户就可以这样读取它:

abc = ABC()
abc.data

可以发现,abc.data后面无需加括号,而且无法修改它,即无法abc.data = 0

@xxx.setter

如果我们还想让用户修改私有变量的值,但需要给输入的值做一定的限制,这时除了添加一个set_data()方法,还可以:

class ABC:
    def __init__(self):
        self._data = 123

    @data.setter
    def data(self, value):
        if data < 0:
            data = 0
        self._data = value

此时,我们就可以通过abc.data = 1这样的方式来修改data的值了,而且,如果赋的值小于0,实际赋的值是0。

@classmethod

Python不支持构造函数重载,也就是说类里面只能有一个构造函数。我们虽然可以使用默认参数__init__(self, a, b='xxx', c='xxx')来解决多个参数问题,但遇到不同类型参数就比较麻烦了。这时,@classmethod便派上了用场。

假设一个向量,既可以通过设置x和y的值来构造,也可以通过长度为2的列表来构造,我们就可以在类里面添加一个“工厂方法”:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @classmethod
    def gen(cls, lst: list):
        if len(lst) != 2:
            raise Exception('列表长度不是2!')
        return cls(lst[0], lst[1])

这样,我们就能使用vec = Vector2D.gen([1, 2])来创建实例了。

这里的clsself一样是约定俗成的,一般不要使用其他命名。

@staticmethod

顾名思义,这是静态方法:

class ABC:
    @staticmethod
    def func():
        ...

于是我们就可以通过类名直接访问它:ABC.func()(也可以在实例中访问它)。

8.文件处理

基本的文件读写

Python将读写文件的过程进行了高度封装,于是你可以非常轻松地读取文件:

with open("file.txt") as file_obj:
    contents = file_obj.read()

只需两行代码即可读取文件中的所有内容。

不过这样读取的内容会在最后面多一个换行符号,为了清除掉这个换行符号,你可以:

contents = contents.rstrip()

你也可以一行一行读取:

with open("file.txt") as file_obj:
    for line in file_obj:
        print(line.rstrip())

将文件内容的每一行读取为列表:

with open("file.txt") as file_obj:
    lines = file_obj.readlines()
print(lines)

默认情况下,打开文件是以只读模式打开,无法写入。如果要写入,则需指定为写入模式:

with open("file_write.txt", "w") as file_obj:
    file_obj.write("a")
    file_obj.write(str(1))

关于模式的选择:

  • r为只读模式
  • w为写入模式
  • r+既可读又可写
  • a追加模式

你还可以指定打开文件的编码格式:

# 以UTF-8编码打开文件
with open("file.txt", encoding="utf-8") as file_obj:
    print(file_obj.read())

json

Python处理json文件非常的方便。

首先需要导入json模块:

import json

读取json文件:

with open("json.txt") as f:
    info = json.load(f)
    print(info)

一般json最外面都是用一个大括号括起来或一个方括号括起来的,比如大括号括起来:

{
    "key": "value",
    "list": [1, "a", {"key": "value"}],
    "dict": {
        "key": "value"
    }
}

是不是很像Python中字典、列表等数据类型的嵌套呢?在这个例子中,得到的info就是一个字典。

读取info,只要像正常的字典那样去读取就行了:

with open("my_data") as f:
    info = json.load(f)
value = info['key']
lst = info['list']
inner_dict = info['dict']
inner_value = inner_dict['key']

如果最外面是用方括号括起来,则info是一个列表,同样支持嵌套。

将数据保存为json也很简单,你只需把数据都保存到字典或列表中,然后再调用json.dump即可:

info = {
    "num": 1,
    "list": [1, 2, 3],
    "dic": {
        "num": 2
    }
}
with open("my_data.json", "w") as f:
    json.dump(info, f)

with … as 的用法

在上面文件操作的代码中,可以发现仅仅需要一个with open()即可,无需像其他语言那样做各种异常处理和关闭文件流等操作。其实这些已经在with对应的操作中帮你处理好了。

你也可以自己写一个with的处理方法:

class myWith:
    def __enter__(self):
        print('enter')
        return self

    def __exit__(self, type, value, trace):
        print('exit')

    def func(self):
        print('do something')

def test():
    return myWith()

with test() as m:
    m.func()

运行结果:

enter
do something
exit

可以看到,用with拿到myWith后,会先运行__enter__(),且__enter__()的返回值会传给as后的变量,即m。在with范围内的代码,即m.func()运行完毕后,调用__exit__()方法。于是,我们可以在__exit__中实现资源的关闭,比如文件流的关闭。

另外,可以看到__exit__()方法还有很多参数,其实__exit__()还可以对异常做处理:

class myWith:
    def __enter__(self):
        return self

    def __exit__(self, type, value, trace):
        print(type)
        print(value)
        print(trace)

    def func(self):
        return 1 / 0

def test():
    return myWith()

with test() as m:
    m.func()

可以看到,m.func()必然会产生错误,这个时候也会运行__exit__()方法,并拿到错误信息:

<class ‘ZeroDivisionError’>
value division by zero
<traceback object at 0x280b7ee6930>

os模块

Python的os模块提供了丰富的方法用来处理文件和目录,比如可以用

  • os.listdir(path): 列出path路径对应目录下的文件和目录列表
  • os.mkdir(path): 创建一个路径为path的目录
  • os.makedirs(path): 递归创建文件夹
  • os.remove(path): 删除路径为path的文件
  • os.rmdir(path): 删除路径为path的空目录
  • os.removedirs(path): 递归删除目录
  • os.rename(src, dst): 移动文件或重命名文件

更多操作可见这里

9.异常处理

捕获异常

try:
    a = 1 / 0
except ZeroDivisionError:
    print("除零错误")

也可以将捕获的异常实例赋给变量:

try:
    a = a / 0
except ZeroDivisionError as err:
    print(err)

运行结果:

division by zero

同时捕获多个异常:

except (ZeroDivisionError, TypeError):
    pass

捕获多个异常并对不同异常作不同处理:

except ZeroDivisionError:
    ...
except TypeError:
    ...

如果异常有继承关系,不应该先捕获父级再捕获子级,否则子级将永远无法被捕获。

捕获任意异常:

try:
    ...
except:
    ...

不建议这样做,否则异常将会难以追踪,甚至会出出现Ctrl C都无法结束程序的情况。

try-except-else结构,可以在try没有捕获到异常时执行else中的语句:

try:
    f = open('filename.txt')
except OSError:
    print('cannot open file')
else:
    print('open file successfully')
    f.close()

finally无论异常是否发生,都会执行:

def readAndClose(connection):
    try:
        return connection.read()
    except:
        print('出现异常')
    finally:
        connection.close()

不建议同时在finally中写return,否则会出现返回值被覆盖的情况:

def func():
    try:
        return 1
    finally:
        return 2

得到的返回值:2

你可以在这里查找内置异常。

抛出异常

raise NameError

也可给异常传递参数,比如给异常加上说明:

raise NameError('错误代码:0x0101,你可在help.xxx.com查找帮助。')

Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
NameError: 错误代码:0x0101,你可在help.xxx.com查找帮助。

传递多个参数,并在异常捕获中获得这些参数:

try:
    raise NameError('abc', 'def')
except NameError as err:
    print(err.args)

运行结果:

(‘abc’, ‘def)

如果你需要捕获异常后,继续将异常往外传递,只需在except中加上raise:

try:
    raise NameError
except NameError as err:
    print('异常产生!')
    raise

运行结果:

异常产生!
Traceback (most recent call last):
File “<stdin>”, line 2, in <module>
NameError

当你想要表达一个异常由另一个异常引起时,可以使用from:

try:
    connect()
except ConnectionError as err:
    raise RuntimeError('连接失败') from err

Traceback (most recent call last):
File “<stdin>”, line 2, in <module>
File “<stdin>”, line 2, in func
ConnectionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File “<stdin>”, line 4, in <module>
RuntimeError: 连接失败

自定义一个自己的异常:

class MyError(Exception):
    pass
 
 
raise MyError

10. 常用标准库和内置函数

Python包含了许多有用的内置方法和库,这里仅列出部分常用的,详细内容可看这里

字符串处理

字符串本身就有许多有用的方法,比如s.startswith()s.endswith()用于判断字符串是否以某个字符串开头或结尾,也可以是元组:

url = 'http://blog.lyzen.cn'
prefix = ('http', 'https')
b = url.startswith(prefix)

通过s.find()来获取某个字符串第一次出现的下标,s.rfind()则是获取某个字符串最后一次出现的下标,如果找不到,则返回-1。

正则表达式,需要导入re模块:

import re

if re.match(r'\d+.\d+', '3.14'):
    print('matched')

你也可以把正则表达式预编译,以便多次使用:

re_exp = re.compile(r'\d+.\d+')
b1 = re_exp.match('3.14')
b2 = re_exp.match('2.71')

还可以用re.findall()寻找字符串中符合要求的所有字符串:

>>> s = '3.14abc2.71'
>>> re.findall(r'\d+.\d+', s)
['3.14', '2.71']

对字符串进行替换,可以用.replace()

>>> ori = '123456789'
>>> ori.replace('123', 'abc')
abc456789

也可用re.sub()进行正则表达式替换:

>>> ori = 'abc3.14def'
>>> re.sub(r'\d+.\d+', '???', ori)
abc???def

以上操作可以以忽略大小写的形式进行,需要使用re模块并加上re.IGNORECASE标记:

>>> s = 'AbCdEf'
>>> re.sub('abc', '123', s, flags=re.IGNORECASE)
123dEf

将字符串转为全大写或大小写,使用s.upper()s.lower()即可。

我们可以使用str.join()来将一定的字符串序列以某个符号连接起来:

>>> parts = ['123', 'abc', '456']
>>> '-'.join(parts)
123-abc-456

对于字符串中变量的替换,可以这样做:

>>> ori = 'Hello {name}! Welcome to {where}.'
>>> ori.format(name='Peter', where='Guangzhou')
Hello Peter! Welcome to Guangzhou.

数字

由于浮点数的特性,进行浮点数计算时,会出现误差:

>>> 0.1 + 0.2
0.30000000000000004

如果对小数的精度有要求,则可以使用Decimal(当然,这是以损失性能为代价的):

>>> from decimal import Decimal
>>> a = Decimal('0.1')
>>> b = Decimal('0.2')
>>> a + b
0.3
>>> a + b == Decimal('0.3')
True

也可以控制小数点的精度:

>>> from decimal import Decimal, localcontext
>>> a = Decimal('22')
>>> b = Decimal('7')
>>> with localcontext() as c:
...     c.prec = 50
...     print(a / b)
...
3.1428571428571428571428571428571428571428571428571

当我们需要将一个类似于'123'的字符串转化为整数时,只需:

>>> a = int('123')
>>> a + 1
124

有时候我们需要将数字储存在字节串中,比如一个IP、一个UUID等。

对于IPv4,一段IP实际上只需4个字节即可储存,比如'192.128.1.102'可用(1)0xC0800166或(2)b'\xC0\x80\x01\x66'表示,将(2)转化为(1):

>>> ip = b'\xC0\x80\x01\x66'
>>> hex(int.from_bytes(ip, 'big'))  # 大端存储
'0xc0800166'
>>> hex(int.from_bytes(ip, 'little'))  # 小端存储
'0x660180c0'

将(1)转化为(2):

>>> a = 0xC0800166
>>> a.to_bytes(4, 'big')  # 4指4字节
b'\xc0\x80\x01f'

顺带提一下,这里你可能对这个\x01f感到疑惑,其实字符f在ASCII码中表”0x66”,而”0x01”在ASCII码中是个控制字符,所以将”\x66”表示成了”f”,而”\x01”不变。

分数计算:

>>> from fractions import Fraction
>>> a = Fraction(3, 4)
>>> a ** 2
Fraction(9, 16)
>>> float(a ** 2)
0.5625

随机数

生成一个0~10的随机整数(包括0和10):

import random
a = random.randint(0, 10)

生成一个0~1之间的浮点数:

import random
a = random.random()

生成5~10之间的均匀浮点数:

import random
a = random.uniform(5, 10)

生成均值为50,标准差为10的高斯分布的随机浮点数:

import random
a = random.gauss(5, 10)

设置随机数种子:

import random
random.seed(123)

生成一个随机数生成器并设置种子:

from random import Random
r = Random()
r.seed(123)
r.randint(1, 10)
r.random()

从列表中随机选一个元素:

import random
lst = [1, 2, 3, 4, 5]
a = random.choice(lst)

从列表中随机选多个元素:

>>> import random
>>> lst = [1, 2, 3, 4, 5]
>>> random.sample(lst)
[4, 3]

打乱列表中元素的顺序:

>>> import random
>>> lst = [1, 2, 3, 4, 5]
>>> random.shuffle(lst)
>>> lst
[3, 2, 5, 1, 4]

日期和时间

表示特定时间,并做运算:

>>> from datetime import datetime, timedelta
>>> print(datetime(2022, 8, 26))
2022-08-26 00:00:00
>>> print(datetime(2022, 8, 26, 1, 26, 59))
2022-08-26 01:26:59
>>> print(datetime.now())
2022-08-26 01:27:53.833984
>>> t = datetime(2022, 8, 26, 0, 0, 0)
>>> t += timedelta(days=1, hours=2, minutes=3)
2022-08-27 02:03:00

字符串与时间转换:

>>> from datetime import datetime
>>> text = '2022-08-26'
>>> t = datetime.strptime(text, '%Y-%m-%d')
>>> print(t)
2022-08-26 00:00:00
>>> from datetime import datetime
>>> t = datetime.now()
>>> text = datetime.strftime(t, '%a %b %d %Y %H:%M:%S')
>>> text
'Fri Aug 26 2022 01:32:17'

常用格式符:

%a 星期英文缩写,如星期五:Fri

%A 星期英文,如星期五:Friday

%b 月份英文缩写,如八月:Aug

%B 月份英文,如八月:August

%Y 年

%m 月份

%d 当前月的第几天

%j 当天是当年的第几天

%H 小时(24小时制)

%I 小时(12小时制)

%P 上午下午(AM or PM)

%M 分钟

%S 秒

%f 微秒,范围:[0, 999999]

等等

像时区、夏令时、闰秒等不在本文讨论范围内,感兴趣的可以上网搜索。

11. 多线程

这里假设读者对操作系统多线程的相关知识有过了解。

基本用法

import threading
import time

def task():
    time.sleep(5)  # 某耗时操作
    print("任务完成")


t1 = threading.Thread(target=task)
t2 = threading.Thread(target=task)
t1.start()
t2.start()

print("主线程其他任务")

t1.join()  # 等待任务1完成
t2.join()  # 等待任务2完成

print("所有任务完成")

如果task函数有参数,则可以这样:

import threading
import time

def task(task_id):
    time.sleep(5)  # 某耗时操作
    print(f"任务{task_id}完成")


t1 = threading.Thread(target=task, args=(1,))
t2 = threading.Thread(target=task, args=(2,))
t1.start()
t2.start()

print("主线程其他任务")

t1.join()  # 等待任务1完成
t2.join()  # 等待任务2完成

print("所有任务完成")

线程同步与互斥

当多个线程都要访问同一个资源时,需要进行互斥操作,以防止数据不一致。

使用Lock实现资源访问互斥

import threading

num = 0  # 共享变量
lock = threading.Lock()


def increase():
    global num
    for _ in range(1000000):
        lock.acquire()  # 获取锁
        num += 1        # 访问共享变量
        lock.release()  # 释放锁

        
t1 = threading.Thread(target=increase)
t2 = threading.Thread(target=increase)
t1.start()
t2.start()
t1.join()
t2.join()

print(num)

获取锁、访问共享变量和释放锁的过程也可简化为:

with lock:
    num += 1

值得一提的是,Python中的基本数据结构如列表、字典等,是线程安全的,因此在访问它们时无需加锁。

在大多数情况下,使用Lock就已经足够了。如果有更复杂的同步、互斥操作,可以看下面的工具。

使用信号量(Semaphore)实现同步与互斥

学过操作系统相关课程或书籍的应该对信号量以及PV操作(wait/signal操作)不陌生,Python也提供了相关操作来实现。

例如生产者-消费者问题:有一个容量有限(如100)的缓存区,当缓冲区不满时,生产者才能向其中添加数据;当缓冲区不为空时,消费者才能向其中读取数据。此外,生产者与消费者对缓冲区的访问应该互斥。

from threading import Thread, Semaphore, Lock

full = Semaphore(0)
empty = Semaphore(100)
mutex = Lock()


def producer():
    while True:
        empty.acquire()  # 相当于P(empty)或wait(empty)
        with mutex:  # 访问缓冲区互斥
            ...  # 往缓冲区中放入数据
        full.release()  # 相当于V(full)或signal(full)


def consumer():
    while True:
        full.acquire()  # 相当于P(full)或wait(full)
        with mutex:  # 访问缓冲区互斥
            ...  # 往缓冲区中读取数据
        empty.release()  # 相当于V(empty)或signal(empty)


producer_thread = Thread(target=producer)
consumer_thread = Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
consumer_thread.join()
producer_thread.join()

使用 Thread.Condition 实现同步

import threading
import time
import random

# 创建条件变量
condition = threading.Condition()
# 共享资源:队列
queue = []


def producer():
    global queue
    for i in range(5):  # 生产 5 个数据
        time.sleep(random.uniform(0.5, 1.5))  # 模拟生产过程的时间
        with condition:  # 获取锁
            queue.append(i)  # 将数据添加到队列
            print(f"生产数据 {i}")
            condition.notify()  # 通知消费者
    with condition:
        queue.append(None)  # 用 None 表示生产结束
        condition.notify()  # 通知消费者可以结束


def consumer():
    global queue
    while True:
        with condition:  # 获取锁
            while not queue:  # 如果队列为空,消费者等待
                condition.wait()  # 等待生产者通知
            item = queue.pop(0)  # 消费一个数据
            if item is None:  # 如果是 None,表示生产结束
                break
            print(f"消费数据 {item}")
        time.sleep(random.uniform(0.5, 1.5))  # 模拟消费过程的时间


producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()

print("任务完成")

输出:

生产数据 0
消费数据 0
生产数据 1
消费数据 1
生产数据 2
消费数据 2
生产数据 3
消费数据 3
生产数据 4
消费数据 4
任务完成

使用 Thread.Event 实现同步

from threading import Thread, Event
import time

event = Event()


def task():
    ...  # do something
    event.wait()  # 等待事件触发
    ... # do something


t = Thread(target=task)
t.start()

time.sleep(3)
event.set()  # 触发事件
t.join()

Event中还有is_set方法来获取事件的触发标志是否为True,clear方法来清除事件的触发状态。

协程

协程是一种用户态的轻量级线程,相比于系统级线程,协程的切换无需进入内核态,因此减少了系统级线程切换的开销。这意味着,你可以实际上使用单线程来实现异步操作。对于I/O密集型任务,使用协程会更高效。
由于用户态线程无法并行,即同一时刻只有一个线程在运行,即便处理器是多核心处理器且系统支持并行。于是对于协程来说,无需保留锁,协程本身就能保证同步或互斥,因为任意时刻只有一个协程在进行,不可能出现两个协程同时访问同一个共享变量的情况。

使用yield实现

在Python 3.5以前,可以使用yield、yield from等实现,在Python 3.5及之后,有更好的工具实现,因此这里主要介绍这个更好的工具,如果对yield等实现感兴趣可以自行上网搜索并学习。

使用await和async实现

使用async定义一个函数,表明这个函数在执行过程中可能暂停并转而去执行另一个协程。

import asyncio

async def async_task():
    await asyncio.sleep(2)  # 模拟异步 I/O 操作,非阻塞

async def main():
    tasks = [async_task() for _ in range(3)]
    await asyncio.gather(*tasks)  # 并发执行 3 个异步任务

start_time = time.time()
asyncio.run(main())
print(f"Total time: {time.time() - start_time}")

输出:

Total time: 2.002645969390869

12. 网络编程

对于简单的GET和POST请求,Python内置的urllib库就可以做到了:

from urllib import request, parse

url = 'http://api.test.com'
headers = { 
  'User-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36',
  'cookie': 'cookie'
}
params = { 
  'name1': 'value1', 
  'name2': 'value2' 
}
req = request.Request(url+'?' + parse.urlencode(params))
u = reqest.urlopen(req)
res = u.read()

但如果需要比较复杂的操作,则建议使用第三方库requests,这是一个非常常用的且好用的库。

更多Python网络编程(低级网络接口如socket等)可见这里

0x04. 常用第三方库

爬虫或网络操作

  • request
  • urllib
  • beautiful soup
  • web自动化:selenium
  • 高性能大型爬虫:Scrapy

    数学与科学计算

  • numpy
  • 数据分析:Pandas
  • 数学绘图:matplotlib
  • 数学动画:manim

    网站框架

  • 轻量级网站框架:flask、django

    游戏

  • pygame

    机器学习

  • TensorFlow
  • pytorch

除此之外,Python社区也有着非常丰富的第三方库等待你去发掘,像是天文学、化学、生物等不同学科的模块,不同数据库的模块等等,仅需上网一搜即可获取你想要的库。


Python光速入门教程(适合已经会其他编程语言的人)
https://blog.lyzen.cn/2022/07/06/PythonQuickTutorial/
Author
Lyzen
Posted on
July 6, 2022
Licensed under