Python笔记 #1 基础语法
本文最后更新于:2023年7月3日 中午
跟着贵系的暑培过了一遍语法,然而写代码时还是感到乏力,总觉得要打一遍才能记住,于是有了这篇博客。
本文部分内容参考了清华 AYF 同学的教程,部分参考了 Python 官方的一份 Tutorial,函数用法参考了 xyfJASON 的博客。本文将持续更新。
Python 特性
相比于 C 的编译型、弱类型、静态类型特点,Python 则是一种解释型、强类型、动态类型的语言。
交互式 vs 脚本
作为一种解释型语言,Python 不需要像 C 一样编译运行,它可以逐行运行代码,因此又分为交互式窗口运行与脚本运行两种模式:
- 交互式窗口:在 CLI 输入
python
,即可进入交互式窗口,输入一行代码即可运行。本文使用的交互式环境是 IPython,输入ipython
即可呼出。 - 脚本:在 CLI 输入
python xxx.py
,就会依次执行整个脚本文件。本文使用的 IDE 是 PyCharm。
Hello World
1 |
|
在交互式窗口,>>>
作为提示符,在 IPython 中则是 In [1]:
。执行 exit()
或者按下 Ctrl+D
就能退出窗口。
注意到,这行代码中没有 ;
分号,可以直接运行而无需编译,字符串用了单引号,注释用 #
开始【与 C 不同】。
简单数据类型
变量类型
【与 C 不同】,Python 不用声明变量类型,解释器自动解释。
int
:变长整数,默认是 4 字节,有需要时自动增长,用于高精度运算,还支持十六进制、八进制和二进制表示。complex
:自带的复数类型,表示为real + imag*1j
的形式,虚部为 1 的时候不可省略 1,可以用j
也可以用J
。实虚部分别为一个float
。float
:8 字节浮点数,【相当于 C 的 double】。bool
:True 和 False,注意首字母大写,用作数值计算时与 C 一样视作 0 和 1。NoneType
: None,空值,常用于返回值、特判。
需要单独说明的是,Python 会存储所有的 -5 到 256 的整数,其他任何变量是这些值时,会被指向这个预先开好的内存,因此任何两个值为 5 的 int 变量都指向同⼀内存地址。
尽量用小写变量名(下划线法),这是 Python3 的主流命名方式。
运算符
这里列出常见的运算符,运算符可以重载,重载需要修改其对应的定义函数。
算术运算符:
+
-
*
【与 C 相同】%
:35 % 4 == 3
,-35 % 4 == 1
,35 % -4 == -1
,-35 % -4 == -3
,【与 C 不同:负数对正数取模时返回正余数,而非像 C 那样返回负余数】/
:__trudiv__
,真除,得到float
结果//
:__floordiv__
,除后向下取整(不是舍弃小数),得到int
结果**
:__pow__
,幂运算
比较运算符:
<
<=
>
>=
==
!=
【与 C 相同】
位运算符:
&
|
^
~
<<
>>
【与 C 相同】
赋值运算符:
=
:赋值号,不能被重载+=
-=
*=
/=
%=
【与 C 相同】- 注意 Python 中没有
++
--
的运算符,只能通过+= 1
实现
逻辑运算符:
and
or
not
【类似 C 中的&&
||
!
,具有短路机制】- 对于
and
和or
,通常用于条件分支bool
的判断,如果非要连接int
变量,得到的结果不会直接转换为bool
,而是返回能够得出结果的最后一个变量【与 C 中的短路类似】
三目运算符:
a if cond else b
:相当于 C 中的cond ? a : b
,注意其参数顺序【与 C 不同】,但更贴近自然语言
特殊条件运算符:
in
:被包含于,详见下文「容器」,返回 boolnot in
:in
的否定,返回 boolis
:判断两个变量的地址是否相同,不可重载,返回 boolis not
:判断两个变量地址是否不同,不可重载,返回 bool
字符串
Python 将字符串封装成了基本类型并处理了多种运算,带来许多便利,注意基本类型本身是不可修改的,所谓修改其实是将重新生成另一个字符串,再将其赋值给目标。
此外,Python 中没有单独的字符类型,单个字符将被视为长度为 1 的字符串。【与 C 不同】,可以用 ""
或者 ''
括起字符串。
下面是一些常用函数,设 str
是一个字符串:
str.title()
:返回单词首字母大写,其余字母小写(不管以前是不是大写)的字符串str.upper()
、str.lower()
:返回全大/小写的字符串str1 + str2
:返回用加号拼接的字符串【与 C++ 的string
类似】str * 3
:返回重复三遍拼接的字符串- 制表符
\t
,换行符\n
【与 C 相同】 str.lstrip()
、str.rstrip()
、str.strip()
: 返回删除开头/末尾/两端空白的字符串str.replace(str1, str2)
:将字符串中的单词str1
全部替换成str2
str.split(str1)
:以str1
为分隔符把字符串拆成子串,并返回包含子串的列表,默认分隔符为空格str.zfill(n)
:将数字字符串补全前导零,n
为总位数' '.join("a", "b", "c")
:用' '
作为连接符,拼接所有子串得到一个长字符串
此外还有一种跨行字符串,用 ’‘’
或 ”“”
括起来,由于脚本中顺序执行时不会输出变量值,这类字符串常用于跨行注释,特别是函数头注释。
输入输出与编码
此处说明几个重要函数:
len(obj)
:获取obj
的长度,常用于获取字符串(注意是 Unicode,因此中文和英文字符都占 1 位)、字节串、容器的长度str(a)
: 把数字(整型或浮点型)a
转换成字符串chr(0x4f60)
:将整型变量i
转化成单个字符ord('你')
:获取单个字符的编码(Unicode),注意在 Unicode 中,英文字母的编码有意设置与 ASCII 码一致
最常用的输出语句 print
,本质上是将变量转化为字符串输出,在末尾自动换行。该函数可以有多个变量,变量间用 ,
分隔,输出时会用空格隔开。
如果要在一句话中插入许多变量,这条 print
语句可能会很丑陋,因此 Python 中有三种格式化字符串的方法。
%
:如print('I am %d' % (age))
,【与 C 类似】str.format()
:如print('hello, {}'.format(name))
f-string
:如print(f'hello,{name}.I am {age}')
其中 f-string
是 Python3.6 的新特性,最为直观便利。
此外,input('Press ENTER to continue')
是常见的一种输入语句,会显示对应的提示内容,读入内容以以字符串存储,可以用 int(input())
或 map(int, input().split())
处理。
字节串
bytes
即是 Python 中的字节串,它表示最纯粹的二进制数据,【类似 C 的 unsigned char *
】,但是在显示上它仅支持 ASCII 显示,因此用肉眼看显得有些不伦不类,通常它只存在于数据的处理过程中。
bytes
的构造与字符串类似,但是要加一个 b
做前导,如 print(b'\x41')
。
容器
Python 提供了一系列内置容器,它们如同 C++ 的 STL ⼀样,不过比 STL 的用法灵活得多。
同样先介绍几个重要函数:
type(obj)
:可以获取参数obj
的类型isinstance(obj, class_or_tuple)
:可以判断obj
是不是类的实例id(obj)
:获取obj
的地址,a is b
等价于id(a) == id(b)
列表 | List
列表(list)是很常用的容器,常被看作 Python 中的数组,但实际上【与 C++ 的 vector 类似】。设 lst
是一个列表:
基础操作
- 定义列表:
lst = [a, b, c, d]
,其中a,b,c,d
等是列表的元素,类型可以不同 - 构造空列表:直接写
[]
或list()
- 打印列表:
print(lst)
(会将列表中的元素列出,括在方括号里) - 访问元素:
lst[3]
, 下标从 0 开始。此外,还支持负数索引,-1
表示倒数第一个,-2
倒数第二个【与 C 不同】
修改、添加、删除元素
- 修改:直接访问元素并赋值
lst.append(x)
:在列表末尾添加元素x
lst1.extend(lst2)
:在列表末尾拼接另一个列表,也可以用运算符+=
lst.insert(idx, x)
:在列表索引idx
处插入一个元素x
(插入后,x
的索引是idx
,其后的元素后移一格)del lst[3]
:删除指定元素(删除后,其后元素前移一格)lst.pop()
:弹出并返回最后一个元素lst.pop(idx)
:弹出并返回指定元素lst.remove(x)
:删除第一个值为x
的元素
组织列表
lst.sort()
、lst.sort(reverse = True)
:对列表排序,永久性修改顺序sorted(lst)
、sorted(lst, reverse = True)
:返回排序后的列表,但不改变列表原有顺序lst.reverse()
:翻转列表,永久性修改顺序len(lst)
:返回列表长度,即元素个数(不论类型)
遍历列表
从头到尾遍历列表:
for i in lst:
循环表达式,【与 C 不同】i
是列表元素,不是索引;循环结束后i
停留为最后一个元素遍历列表时同时遍历下标:
for idx, value in enumerate(lst):
若要检查列表是否为空,可以用
if lst:
条件表达式,返回 bool
列表切片
lst[l:r]
:返回一个列表,元素依次是lst
列表的索引在左闭右开区间内的元素,省略l
或r
则默认从头开始或到尾结束lst[l:r:step]
:指定步长为step
切片,step
为 -1 时返回倒序,省略参数后写作lst[::-1]
- 可以用循环遍历列表切片:
for i in lst[l:r]:
- 复制列表:在切片中同时省略
l
和r
,即返回从头到尾的列表,如lst2 = lst1[:]
,而非lst2=lst1
,后者lst1
和lst2
实质是同一个列表【类似 C 的引用】
元组 | Tuple
元组就是元素值不可修改(弱意义上的,其中元素的元素可以被修改)的列表。设 tpl
是一个元组:
基础操作
- 定义元组:
tpl = (a, b, c, d)
,把列表定义中的方括号改成圆括号()
即可 - 定义时的小括号有时候可以省略,可以直接用
,
逗号构造元组【与 C 不同,没有逗号运算符】 - 构造单元组:
(1)
会被理解成表达式,要用(1,)
- 构造空元组,直接写
()
或tuple()
,但(,)
会报错 - 访问元素:
tpl[3]
, 下标从 0 开始。
遍历元组
for i in tpl:
和列表一样。
修改元组
元组中元素的值不能修改,但是元组变量本身可以被赋值,这点与字符串类似。此外,如果元组的中的元素是可修改的,如 List,则可以修改 List 内部的元素。
元组解包
经典的 Python 交换赋值代码:a, b = b, a
,利用了解包和逗号构造。
其余的若干种解包方法待补充:https://zhuanlan.zhihu.com/p/351369448
元组打包
zip()
函数用于将若干个迭代容器打包为元组(通常是两个 List),返回一个 zip
对象。如果初始 List 的大小不同,则会向最小的对齐。初始的 zip
对象不占内存空间(Python 3.x 新特性),如果要展示,则需手动 list()
转换。
1 |
|
集合 | Set
集合由一组无序、互不重复的元素构成(在数学上也是如此),在内部用哈希实现。设 st
是一个集合:
基础操作
- 定义集合:
st = {1, 2, 3}
,把列表定义的方括号改成花括号{}
即可 - 构造空集合:只能用
set()
,因为{}
的表达被空字典占用了 - 由列表转集合:
st = set([1, 1, 1, 2, 2, 3])
,会自动去重,常用于去重列表的遍历for i in set(lst):
- 注意集合的元素必须是可 hash 的,不能为 list 这种可变容器变量
添加、删除元素
st.add(4)
:添加一个元素st.remove(2)
:删除一个元素
字典 | Dictionary
字典是一系列「键值对」,用于存储一组有穷映射,可将任何 Python 对象作为值,【类似于更高端版本的 C++ 的 map】。
基础操作
- 定义字典:
dic = {'name': '张三', 'age': 18}
,花括号括起一系列键值对,键与值之间冒号:
分隔,键值对之间逗号,
分隔。 - 访问元素:
d['name']
,用键访问 - 注意字典的键必须是可 hash 的,不能为 list 或 set 这种可变容器变量,但可以是 tuple
添加、修改、删除
- 添加:直接赋值即可(即使键本来不存在),如:
dic['x'] = 0
- 修改:
dic['age'] = 18
,直接赋值即可 - 删除:
del dic['age']
遍历字典
- 遍历所有键值对:
for k, v in dic.items():
,其中items()
返回键值对的列表 - 遍历所有键:
for k in dic.keys():
,其中keys()
返回键的列表,可省略 - 由于
keys()
的本质是列表,各种对列表的操作也适用,如:for k in sorted(dic.keys()):
或if 'age' in dic.keys():
- 遍历所有值:
for v in dic.values():
,其中values()
返回值的列表
迭代器
前文提到的所有容器,包括字符串、字节串都是可迭代的,这意味着它们可以用 for
循环来遍历,也可以用生成式构造(下面介绍)。
但最为特殊的迭代器是 range
类型,作为数值列表,与列表有相似之处,但它实际上不占用内存(用于大循环时很节省空间)。下面是一些常用方法:
range(l, r)
:依次生成左闭右开区间中的整数【类似于 C++ 的for(int i = l; i < r; i++)
】,如果省略左端,会默认以 0 开始range(l, r, step)
:指定步长为step
【类似于 C++ 的for(int i = l; i < r; i += step)
】min(range(l, r))
、max(range(l, r))
、sum(range(l, r))
:返回数值列表的最小值、最大值、总和- 在
for i in range(10):
的循环中改变了i
的值,如i += 1
, 不会影响循环的次数,因为迭代器中的数值列表是不可修改的【与 C 不同】
生成式
使用生成式构造容器是非常常见、高效的操作,下面举几个例子:
a = list(range(10))
:直接转化数值列表为基本列表lst = [i ** 2 for i in a]
:列表生成式st = {x % 7 for x in a}
:集合生成式dic = {x ** 2: x for x in a}
:字典生成式,保留:
来分隔键与值tpl = tuple(x + 1 for x in a)
:元组生成式,直接用()
会被当成表达式
综上所述,生成式表达可以用三段式:表达式 for 循环变量 in 迭代对象 if 筛选条件
,其中最后的筛选条件不一定要。
此外,还可以用更简单的 map()
函数构造容器,其内置了一系列映射句柄,如 map(int, ['1', '2', '3'])
,或 map(square, [1, 2, 3])
。
流程控制
Python 中不用大括号来显式地分块,而是用冒号配合缩进(Indent)。代码块与代码块之间至少隔着一个空行表示结束。当一个代码块必须存在,又不想写任何语句的时候,可以写一个 pass
作为占位符。
条件分支
1 |
|
注意,elif
和 else
后面也要有冒号配合缩进,如果有多个条件,用 and
和 or
逻辑运算符连接。
for 循环
前面所列的容器、数值列表都可以用于 for
循环迭代,比较特别的是字符串也可以迭代:
1 |
|
此外,如果在数值列表的迭代中用不到迭代变量 i
,仅作为迭代次数使用,可以用 _
变量表达。
while 循环
1 |
|
跳出循环可以用 break
和 continue
【与 C 相同】
异常控制
在 Python 中,我们可以使用 try
和 except
语句来捕获和处理异常。异常处理使我们能够在程序出现错误时执行一些特定的操作,而不是让程序崩溃。下面是基本用法:
1 |
|
注意,如果这里存在 Exception 还无法捕获的异常,例如 KeyboardInterrupt(在命令提示符中如果按下 Ctrl+C 结束终止的键),则可以换成 BaseExpection,这是 Exception 的父类。
另一种常用的方法是循环 try
语句,适用于 API 接口的访问:
1 |
|
函数
基础函数
1 |
|
函数传参
Python 是不允许程序员选择采用传值还是传址的。Python 参数传递采用的肯定是「传对象引用」的方式【类似 C 中传值和传址的结合】。如果函数收到的是一个可变对象(比如 list、dict)的引用,就能修改对象的原始值;如果是一个不可变对象(比如 int、float、str)的引用,就不能直接修改原始对象。
传入参数的数量可以不固定,但是必须指定默认值;也可以调换顺序,但必须指明对象。
1 |
|
当然,也可以传递列表等容器,但传递的也是列表的地址,在函数中修改同样会改变原列表,如果不想修改原列表可以用 [:]
传递切片。
传递任意数量的参数
- 在形参前加星号
*
,Python3 会创建一个该形参名称的元组,本质上是一种元组解包 - 在形参前加双星号
**
,Python3 会创建一个该形参名称的字典
返回值
作为动态类型语言,函数返回值可以不固定,可以多个 return
在不同情况下返回不同值,或者没有 return
(等价于 return None
)。
值得注意的是,Python 中虽然不需要指定参数和返回值的类型,但可以使用类型提示(Type Hints)来在函数定义时体现参数和返回值的类型。这是一种对函数参数和返回值进行注释的方法,它可以增加代码的可读性和可维护性,并提供了一种方便的方式来指定函数的输入和输出类型。
1 |
|
函数模块调用
函数可以被存储在模块中被调用,模块是扩展名为 .py 的文件,包含函数的代码【类似于 C 的头文件】
- 导入整个模块:使用
import pizza
导入,调用时使用.
句点,如:pizza.make(16, 'green peppers')
- 导入模块中特定函数:使用
from pizza import make, eat
,调用时无需句点,直接用函数名 - 导入特定函数别名:使用
from pizza import make as mk
,调用时无需句点,直接用别名 - 导入模块中所有函数:使用
from pizza import *
,调用时无需句点,但是会污染命名空间,不建议使用
所有 import
都放在程序开头【类似于 C++ 的 #include<>
】。
第三方模块
作为一种工具语言,大部分情况我们都是通过「调包」完成任务,即导入第三方库。市面上的第三方库可以通过多种途径获取,包括 pip
、conda
等。在命令行中使用 pip list
即可查看当前安装的所有库和版本信息,也可以结合管道 pip list | findstr numpy
(Windows)。
模块封装
在 Python 的程序模板中经常看见 if __name__ = '__main__'
,这其实是 Python 中的一种 Magic Method。
Python 中所有 .py
文件都可被视为一个模块,当模块作为脚本被直接运行时,其 __name__
的值变为 __main__
。相反,当模块被其他导入其他脚本运行时,其 __name__
存放的就是模块的名字(类似环境变量)。
而作为一种解释型语言,Python 会将所有导入的代码全部顺序执行一遍。如果我们在一个脚本中实现了某个算法,想将其作为模块在其他脚本中调用,我们并不希望它在 import
的那一刻就执行,因此,正确的做法是将其封装成函数。
但如果要单独运行该模块,譬如调试功能时,使用 __name__
就可以完美解决:
1 |
|
Lambda 匿名函数
和很多语言⼀样,Python 中可以使用 Lambda 匿名函数完成一些简单的逻辑,但比较特殊的地方在于,Python 中匿名函数必须只由单个表达式构成,这个表达式的值也就是匿名函数的返回值。
lambda
关键字可以用来创建一个匿名函数,紧跟其后的是参数列表和用冒号 :
分割开的单个表达式。如,lambda x: 2 * x
是将任何输入的数乘 2,而 lambda x, y: x+y
是计算两个数字的和。
使用匿名函数的经验准则是保持简单以及只在本地使用一次。一种常见却不推崇的做法是将其作为简单函数的另一种声明方式,赋值给变量,如:
1 |
|
对 lambda 函数命名的唯一作用可能是出于教学目的,其问题在于这使得调试不那么直观——错误信息只会提示某个
lambda
函数存在问题,但不会提示哪个函数。
正确的做法应该是将其作为参数传递,如 .sort
函数、sorted
函数等,此时单个表达式会被计算为一个值并且参与后续的计算。
1 |
|
文件操作
open()
函数的第二个实参 w
表示写入(自动创建或覆盖原内容),r
表示只读,a
表示附加(自动创建或添加到文件末尾),r+
表示读写。如果不加第二个实参,则默认为 r
只读。
读取文件
一种【与 C 相似】的操作是:
1 |
|
此外,还有一种更推崇的操作方式,使用 with
上下文管理器,将打开的文件的对象存储在 as
后的变量,这样可以避免由于忘记关闭文件导致丢失数据:
1 |
|
回到读取本身,方法 read()
读取整个文件的内容并返回单个字符串,并包含每个 \n
换行符。
但实际操作中读入一个长字符串是丑陋而且难以处理的,我们更倾向于逐行读入,用 line.rstrip()
消除行末的 \n
:
1 |
|
另一种逐行读入是创建一个包含文件各行内容的列表,每个元素是一行内容的字符串,包含行尾的 \n
:
1 |
|
写入文件
1 |
|
方法 write()
表示将字符串写入文件。如果要写入数值,应先用 str()
将其转化为字符串。
同样,按行写入也有对应的函数 writelines()
,但要求传入的参数为字符串列表,同时每个字符串后面都要自带换行符,因此比较鸡肋。
如果要追加写入文件,则需要将 w
替换为 a
模式,需要注意的是新文本可能会在旧文本之后立即添加,因此可能需要添加一个换行符 file.write("\n")
。
类
当涉及到面向对象编程(Object-Oriented Programming, OOP),Python 提供了类(class)的概念,允许你创建对象、定义属性和方法。
类与对象
在 Python 中,类是创建对象的蓝图。类定义了对象的属性和方法,可以通过实例化(即创建对象)来使用类。下面介绍类与对象的基础用法。
定义类
可以使用 class
关键字来定义一个类。以下是一个简单的类的示例:
1 |
|
类包含了一个特殊的方法 __init__
,它被称为构造函数(Constructor)。构造函数在创建对象时被调用,并用于初始化对象的属性。在构造函数中,self
是一个特殊的参数,它代表实例化后的对象本身。我们可以使用 self
来引用对象的属性和方法。
创建对象
要创建类的实例(即对象),可以像调用函数一样使用类的名称并传递所需的参数。以下是一个创建 Person
类的对象的示例:
1 |
|
访问属性和调用方法
要访问对象的属性和调用对象的方法,可以使用点号 .
运算符。示例:
1 |
|
继承与多态
在面向对象编程中,继承和多态是重要的概念。继承允许创建一个新的类,它从现有的类派生,并继承其属性和方法。多态允许子类重写父类的方法,以实现不同的行为。
继承
要创建一个继承自另一个类的子类,可以将父类作为子类定义中的参数。示例:
1 |
|
这里定义了一个名为 Student
的子类,它继承自 Person
父类。子类重写了父类的构造函数,并添加了一个名为 student_id
的新属性和一个名为 study
的新方法。使用 super()
函数可以在子类中调用父类的方法。
多态
多态允许不同的对象对相同的方法进行不同的实现。示例:
1 |
|
在上面的示例中,我们定义了一个名为 introduce
的函数,它接受一个 Person
类的对象作为参数,并调用对象的 greet
方法。通过向 introduce
函数传递不同的对象,可以实现多态行为。