作者:@DarrenDanielDay
最后更新时间:2020/01/16
本文是一个用Markdown写的
Python
的语法和机制基础性教程。
本文的内容大部分是本人对官方文档的解读。
如有错误或不准确的地方,欢迎指出并联系本人。
本教程使用的Python版本是3.7.3。
如果你使用的是Python3.X,都可以参阅本教程。本教程的源码在这里
本教程主要用于解决一些初学者对语法和机制的疑惑。
当读者较好地理解并熟悉了本文中的各种概念后,读者对Python
语言本身将会有一个相对详细的认知,并能够轻松发现并解决其他初学者的类似低级错误。
若读者在学习后能灵活运用这些语法和机制,也不难写出简洁而逻辑清晰的高质量代码。 本文着重点在于语法与机制,代码示例基本不涉及第三方库,只涉及标准库,因此本教程并不能教会你使用Python做爬虫、人工智能、大数据、Web开发等。
库是学不完的,是会快速更新出新的,而语法和机制则是相对稳定的、不变的,是可以比较系统地学完的。因此,仔细学习语法和机制更有长远意义。
如果读者对语法和机制有初步的了解,读者可以将本文当作一本工具书,当遇到语法或机制上的疑惑时再来翻阅。不过,对于绝大多数(超过99.9%)的学习者(不管是不是初学者),都能在本文中找到完全没有接触过的Python
语法和机制知识(尽管本教程只有三千多行Markdown代码)。
我会按照这样的顺序来描述我对Python
的理解。
对象这个词是一个十分抽象的概念。
简单来说,对象就是一个“东西”
。
比如说一个数字,一个公式,一条信息,一张图片,一张表,一首歌曲,一个程序……在计算机的世界里,他们存在的方式不过是杂乱无章的0与1的组合罢了。
为了让他们有所区别,我们把他们隔离开来,不让这个0与1的组合与那个0与1的组合混起来,我们将每一个“个体”称为对象。
了解对象的概念,有助于你更好地理解面向对象的程序设计。
面向对象的设计,可以让你的代码思路更加清晰。
在Python
中,一切都是对象。
请注意这里的一切不是指
代码
中的一切,而是在程序
看来的一切。这一点和JavaScript
是类似的。Python
的所有对象都是object的实例,所有的类都是对象,所有的类都是object的子类。
面向对象有三大特性:
我认为封装这个概念很好地解释了什么叫对象。
封装也可以称为数据抽象
。
封装的主要思想是:隐藏对象的数据和方法的实现细节,对外仅提供一些接口进行访问。
这话看起来有些抽象。打个比方:
假设你买了一个台式电脑主机。主机里面有很多零件,包括一些存储设备,它们就好比是数据。主机也可以完成一些事情,比如读取磁盘,这些就好比是主机的方法。但是,你买来的主机一般都是装在一个外壳里面的。如果不把外壳拆开,你就只能通过主机上可以插入显示器的接口、可以插入USB的接口等来与主机进行交互。事实上,作为使用者的你也并不需要去关心这些数据是怎么存储的,也不关心读取磁盘的方法的细节,是什么硬件完成的。你只需要通过可用的接口来访问他们就可以了,例如在显示屏的接口插上一个显示屏来显示内部存储的数据(实际上还需要安装操作系统、文件系统等),或者在USB接口插上U盘来传输数据,等等。这里,你买的主机就是一个对象。它是一个整体,它的硬件(数据)和功能(方法)被封装在一个大箱子里。其他提到的如显示器、U盘,也是对象,它们也有自己的硬件和功能,也被装在一个外壳里。
继承也可以称为泛化
。
如果一个类B继承了一个类A,那么我们说类A是类B的父类,并且B是A的子类。
继承具有传递性。即:如果A继承了B,B继承了C,那么A也继承了C。A是B的子类,同时也是C的子类。
子类拥有父类的全部数据和方法,并且可以在此基础上进行扩展。
尽管继承的机制比较简单,但我们还是需要知道如何设计父类与子类。
假设有类动物
,它封装了数据“叫声”
以及方法“叫”
。狗
是一种类,猫
也是一种类,他们都是一种动物
,因此应该有“叫声”
的数据和“叫”
的方法。如果用继承的机制,我们就不必再去为猫
和狗
去重复指定它们有叫声
以及会叫
。
子类的实例也是父类的实例。
一个子类的实例的方法的具体实现可以和其父类的实例有所不同,是一种多态的表现。
换言之,子类可以重写 父类的方法,是多态的一种表现。
如上面的例子,
猫
和狗
都有叫声
,并且会叫
,但是叫声
不同。狗
的叫声
是“汪汪汪”,猫
的叫声
是“喵喵喵”。并且,狗``叫
的时候还会伴随着头部的抖动,而猫
不会。因此,虽然类狗
和类猫
都继承了类动物
,但我们希望他们在“叫”
这个方法上有不同的表现。因此我们可以通过重写它们父类的方法“叫”
来解决这个问题。
例如前面的USB接口。能在USB接口进行数据交换的不只是U盘,还有鼠标、键盘、移动硬盘、光驱等,只要是有(实现了)USB接口的设备,都可以通过USB接口接入主机进行数据交换。
Ellipsis表示未定的
对象。其类型为ellipsis(这同时也是类对象)。
Ellipsis 对象可以直接用字面值...
或者Ellipsis
得到。
所有的Ellipsis都是一样的(都是同一个对象的引用)。通过以下代码可以验证:
print(... is ...) # 输出结果为True
官方文档并没有对这个对象给出更多的解释。或许它只是一个特殊的符号。当然有些第三方库的代码里需要用到它,例如numpy
。这意味着它的存在和运算符@的存在是类似的。你想用这个特殊符号,可以拿去用,并让你的代码更加简洁。这和许多汉字被造出来本身无特殊含义,仅仅用于人名是一样的道理(你可以自行定义其作用)。
此外,如果懒得写
pass
来填补空的代码块,也可以使用Ellipsis作为单一表达式语句填补,即用...
代替pass
,尽管这并不是其原本的作用。
在高中我们学过,数学中的函数
是数集到数集的映射。
但是在编程语言中不是这个意思,尽管他们有些相似的地方。
一个函数接收一些参数,做一些事情,返回一些结果。
函数可以看成是一个大程序中的子程序。
一般地,一个函数由函数名称及参数列表、函数实现(函数体)以及一些注解构成。
参数,英文Parameter,是一个函数可能需要的,指明一些信息的对象。在函数体中,参数往往用变量来表示。 就像数学里的函数需要一个具体的自变量的值才能计算函数值一样,编程语言中的函数也需要一些参数才能运作。 和数学不同的是,编程语言中的函数的参数不一定是数,而且参数的个数也不一定是一个,可以是多个。至于具体是什么类型的参数,需要多少个参数,不同的函数会有所不同,也有的函数不接收参数(接收0个参数)。
函数体是函数的主要部分,在函数体内包含了函数做的事情,以及返回的信息。
在Python
的代码中,函数体是与函数声明连续并且多一个缩进的代码块。
这里有一个Python
初学者非常容易犯的错误:写了一个函数的定义却没有函数调用的代码,然后运行代码看不到任何运行结果。
本文中的函数调用,英文Call,指的是给一个函数传入适当的参数,让它工作起来。
有关Python
的函数调用机制,详见运算符()。
返回,英文Return,是函数在正常执行完毕后,向调用者提供运行的结果的行为,尽管函数运行的所有结果不一定都通过返回值来体现。
一些编程语言如C/C++
、Java
的函数也可以不返回任何值,即返回一个无法使用的void
类型的值。在这种情况下,返回就只是让函数执行结束罢了。
而Python
的函数一定有返回值,即使你不去显式地去执行返回语句。
返回值是函数正常执行完后向调用者提供的运行结果。
在Python
中,返回值是一个对象。
在函数中,用于说明执行到这里就应该结束函数调用,向调用者提供执行结果的语句。
在Python
中,返回语句应当使用关键字 return。
当一个函数被调用后,执行到了返回语句,那么函数就会向调用者返回结果,并结束函数的执行,回到调用函数的地方。
Python
中的函数也是对象,尽管这对于C/C++
、Java
这样的语言来说并不成立。
一个Python
函数,根据其定义的参数列表,接收相应个数的参数,可能是0个参数(没有参数),也可能是任意固定个数的参数,也可能是个数可变的参数。
之后,函数会利用这些参数(也可以不用)做一些事情,比如做一些运算,存储一些信息,创建一些窗口等等。只要是代码能实现的事情,函数都可能会做。
最后,函数会返回一个对象,作为函数的返回值。
一个Python
函数的定义大致是这样的格式:
def function_name(parameter_list): # 这里是函数的声明,包括了函数的名称以及参数列表。
...... # 这里是函数体。
return return_value # 这里是函数体,并且这一行是一个返回语句。
你可以在代码中的任何行自定义一个函数,尽管这样做很可能会带来不必要的麻烦。
一般地,Python
的函数体是连续的位于声明下方的具有比函数声明多一个缩进的代码块。
其中的返回语句:
return return_value
可以没有。如果这样做,当函数调用并执行到函数体的最后时,函数将会返回 None
。
当然,你也可以为函数添加一些注解(官方翻译:标注),使得你的编写思路更加清晰。虽然Python
解释器会去处理这些注解,但这不太会影响函数的执行。
def function_name(parameter1:int,parameter2:str,paramater3:list)->dict:
......
return return_value
参数列表是函数需要的参数的清单,是在函数的定义处定义的。Python
函数的参数将在函数体内作为局部变量使用。
各个参数都必须有自己的名字,命名规则参考变量名。参数列表中各个参数用逗号,
隔开。
参数列表的合理顺序是:位置参数,含默认值的位置参数,可变参数,仅键值参数,字典参数。
直接为参数取名即可。可以有0个或任意有限个,个数必须是确定的。
例如:
def func1():
pass
def func2(a):
pass
def func3(a,bcd,e,fff,ggggg):
pass
为参数取名后,在名称右边附上等号=
并在等号右边指定其默认值。参数列表中可以有0个或任意有限个,个数必须是确定的。函数调用时如果没有接收到,那么将会使用默认值。
值得注意的是:默认值只会初始化一次。也就是说,如果使用了默认值,那么它总是指向同一个对象。如果这个对象是可变的,那么当这个对象被改变了,使用的默认值也会受到影响。
例如:
def func4(a=1,b=""):
pass
为参数取名后,在名称左边附上星号*
。
这个变量名用于接收“多余”的通过位置传递获得的参数。它将多余的参数集中在一起,构成元组,并指向这个元组。
不建议将含默认值的位置参数和可变参数同时使用,因为这样做在函数被调用的时候,要么默认值失去意义,要么可变参数失去意义,尽管这不会在语法上产生问题。
仅键值参数在参数列表中的写法和位置参数一致,但是仅键值参数必须在可变参数的后面。 简单思考一下就会明白其中的道理:可变参数用于接收多余的位置传递的参数,那么在可变参数之后的参数自然不可能通过位置传递获得值。要想让他们获得值,只能通过键值传递或键值解包来传递。
值得注意的是,仅键值参数必须靠键值传递,因此不像位置参数那样,并没有“不带默认值的要在带默认值的前面”这一限制。当你看了参数传递以后,你会理解这一点。
为参数取名后,在名称左边附上两个星号**
。这个变量名将会与传入的无法赋值给位置参数的键值传递传入的参数构成的字典所绑定。\
以上的几种参数都可以混合使用,但是要注意遵循一些语法。
本人已知的参数列表语法包括以下:
def f1(p1,p2=1,*args,k1,k2=1,k3,**kwargs):
pass # 正确
def f2(p1,p2=1,k1):
pass # 语法错误,k1是位置参数,顺序不符合
def f3():
pass # 正确
def f4(*a1,*a2):
pass # 语法错误,不符合2
def f5(p1,*p1):
pass # 语法错误,不符合3
合理的参数传递顺序是:位置传递,一个星号*
的解包传递,键值传递,两个星号**
的解包传递。
前文所说的参数列表中使用含默认值的位置参数和可变参数同时使用的歧义就会在这里会产生。
通过逗号,
将被传递的对象隔开,按照顺序赋值给参数列表中的变量。如果位置参数的个数不对,将会引发异常。
通过[名称]=[值]
的方式传递。
如果位置参数(包括带默认值的)能够匹配到相同的[名称]
,那么这个[值]
将会被赋值给那个参数。
通过在表达式前面加一个星号*
将可迭代对象进行迭代,并将各次迭代的返回值用逗号分隔后替换
接收的位置传入。
通过在对象前面加两个星号**
将字典的键值对解释成[key]=[value]
的形式传入,就像键值传递一样。
本人已知的语法检查机制包括以下:
如果以上语法没有问题,大概再按这样的方式来进行传递。
位置传递
。将这个总的序列按其顺序将参数取出,并从左到右赋值给参数列表中的位置参数(包括含默认值的位置参数)。如果赋值完成后,还有参数剩余,则会尝试将多余参数的打包成一个元组,并将这个元组赋值给可变参数,若参数列表中没有可变参数,则引发异常。没有参数多余但参数列表中有可变参数的情况,可变参数会被赋值为空的元组。键值传递
。不断取出一个键值对,将键对应的值赋值给名称与键相等的位置参数、含默认值的位置参数、仅键值参数。如果这些参数已经被赋值,则引发异常。如果赋值完以后,还有多的键值对没有使用,则尝试将其打包成一个字典,并将这个字典 赋值给字典参数,若参数列表中没有字典参数,则引发异常。没有键值对多余但参数列表中有字典参数的情况,字典参数会被赋值为空的字典。说起类型,就还是得再提一提这件事:
计算机的世界其实是0和1的组合,至少现在还是这样。
那么,0与1的组合为何能够表示出不同的含义呢?我们需要的就是类型。使得这些0与1的组合能够有所区别的,是类型的作用。 更简单的理解是:类型就是说明对象是哪个类的实例。当我们讨论一个对象的类型时,往往使用类代替类型。当然,这个理解不适用于没有类机制的编程语言。
举一个简单的例子。学过C语言的人应该知道这样的一件事:'0'==48。为什么呢?因为字符'0'占一个字节的存储空间,所对应的二进制码是0x30,也就是十进制的48。因此,同样的二进制码0x30,你可以把它解释成字符'0',也可以解释成数字48。单从二进制码上来讲,'0'和48是相等的,而类型则规定了我们应该如何来解释这些二进制码。
当然,Python
的类型机制比这个例子要复杂的多,在这里就不(wo)深(bu)入(hui)了。
在Python
中,type是一个特殊的类,也是类对象,它是所有类的类型,也就是说,所有类对象都是type的实例。
type作为构造函数
时,接收一个对象,返回这个对象所属类型的类对象。
在学习中多使用type去获取对象的类型可以让你更加深刻地理解类、类型、对象、类对象之间的关系。
布尔类型也是不可变对象,是可哈希对象。
布尔类型是最简单的类型。它只有两种可能的值:True
或者False
。
True
代表真,False
代表假。
布尔类型是逻辑的基础。
True
和False
都是关键字。
这里的数指的是Python
中用于计算的数对象。
Python
中的数都是不可变对象,是可哈希对象。
在这里,整数的概念和数学中的整数
是一致的。
英文:Integer
Python
中的类名:int
以下是一些与十进制、二进制等进制有关的数学知识,并且可能颠覆你对数
的认识。如果你理解数制的概念,可以跳过。
数制是用于表示数值的方法。一个数制由基数和位权组成。 数字是数值在数制下的文字表现。不管是用哪种数制表示数字,只要其数值相等,我们就可以在这些数字之间划上等号。 数字是表象的,数值才是本质的。 数值是十分抽象的。为了说明这一点,请看下面的例子。
现在,下面的区域有一些&
:
&&&&&&&&&
请问有多少个&
?
有小学一年级以上的数学水平的人都能够很快地回答出:
这里有
9
个&
。
但是当你继续看下去,会发现问题没有这么简单。
为什么是9
个?不是$
个也不是%
个,而是9
个,或者是九
个。看到$
和%
你可能会看不懂我在说什么,这很正常,因为这两个符号是我随便挑的。
实际上,问题的本质是:9
只是一个符号,而我们人为地赋予它与前面那块区域中&
的个数相同的意义。
但是,9
只是一个符号而已,至于为什么这样写,那要问造出这个符号的人。
而9
,正是一个十进制数字。它只是一种数值在十进制下的表现形式。
然后我借助大家熟知的十进制来解释一下数制的机制。
一个数制中有一个基数和以及一些个数与基数相等的数码。一个该数制下的数字由这些数码组成(小数点是可选的)。
对于十进制,其基数应当和正常人手指的个数相等,对应的数码分别是0
,1
,2
,3
,4
,5
,6
,7
,8
,9
。
每个数码都对应了一些确定的数值。
十进制的这些数码对应的值的多少应当在小学一年级就有学过,此处略。
一个数制还有位权。位权是指一个数码在数字中的一个位置上的权。有关权的概念请自行查阅资料。
一般地,对于在A进制(A>1)下的数字,位权可以这样确定:
如果没有小数点,最右边的位置上权为1。如果有小数点,小数点左边的位置上权为1。
每向左一位,权变为A倍。每向右一位,权变成1/A。
对于十进制整数,从右向左的权依次是1,10,100,1000……
数字代表的数值是数字中所有数码乘以其位权的总和。
例如十进制数字12344
,按照前面的规则,权依次是10000,1000,100,10,1。因此,这个数字代表的数值就和这个表达式的值相等:1*10000+2*1000+3*100+4*10+4*1
尽管我这里使用了10000
、4
等十进制数字来代表数值,但这不影响含义的表达。
如果你完全理解了这些,那么你大概已经知道二进制是什么了。
二进制的基数是2,对应的数码是0
,1
。
二进制整数的权值从右往左依次是1,2,4,8,16,32……
二进制整数1011
表示的数值就是用十进制数字写的表达式1*8+0*4+1*2+1*1
的值,即十进制数字11
的值。
然后计算机科学中常用的八进制和十六进制我就可以这样简单带过了。
八进制的基数为8,数码是0
,1
,2
,3
,4
,5
,6
,7
。位权的规则和前文介绍的一致。
十六进制的基数为16,数码是0
,1
,2
,3
,4
,5
,6
,7
,8
,9
,A
,B
,C
,D
,E
,F
,其中A
,B
,C
,D
,E
,F
对应的值是十进制的10~15。字母也可以用小写的。位权的规则和前文介绍的一致。
尽管我花了大量篇幅来解释数制,但我不想在这里介绍不同进制的数字之间如何转换,因为这是纯粹的数学知识,并且Python
也提供了相应的库支持,而介绍数制只是为了补充概念。
请注意:转换前后的两个数字是相等的,它只是换了一种表示形式,因为进制转换不会改变数值。
在计算机科学中,整数一般都是以二进制补码存储的。
以经典的32位整数为例,总共有32个二进制位供存储值,每个二进制位都只可能是0或者1。我们姑且将这32个二进制位从左到右排列,越往左边,权值越高,这和数学里面的习惯是一样的。最高位是符号位,是特殊的位,可以用于表示符号,0表示正数,1表示负数。
每个二进制位的权值从右到左依次是1(2的0次方),2(2的1次方),4(2的2次方),8(2的3次方)……,1073741824(2的30次方),-2147483648(负2的31次方)。与众不同的只有最高的符号位,其权值是负值。
一个整数的32位二进制补码的手算方法:
例如,十进制的78对应的32位二进制补码是:
00000000000000000000000001001110
十进制的-78对应的32位二进制补码:
先把31位取反,得到
1111111111111111111111110110001
然后加1得到
1111111111111111111111110110010
最后添上符号位得到
11111111111111111111111110110010
同样我们可以得到十进制的-1对应的32位二进制补码是(非常特别,所有位都是1):
11111111111111111111111111111111
整数常量的几种表示方法如下:
# 十进制整数,由十进制数码和下划线"_"构成,不以"_"和"0"开头,不以"_"结尾,且"_"不可连续出现。
18640 # 18640
18_640 # 18640
18640_ # 语法错误
018640 # 语法错误
13__13 # 语法错误
_18640 # 语法上不错误,但这并不是代表整数常量18640。
# 二进制整数,以0b或者0B开头,后面的部分由"0","1","_"构成,不以"_"结尾,且"_"不可连续出现。
0b11111 # 31
0B1010 # 10
0b001_1 # 3
0b_111010 # 58
0b100_ # 语法错误
0b12 # 语法错误
# 八进制整数,以0o或者0O开头,后面的部分同二进制,但是多了数码2~7。
0o3417 # 1807
0O12471 # 5433
# 十六进制整数,以0x或者0X开头,后面的部分同二进制,但是多了数码2~9以及a~f和A~F。
0xa_78afD_F # 175681503
下划线是比较特别的存在,它可以用来使得数字常量更易读,正如一些较大的十进制数字每三位用逗号隔开一样。
值得注意的是,负号是一个运算符,它并不在整数常量的表示当中。
顾名思义,浮点数就是小数点的位置可以浮动的小数,其本质也是数值的表示方式。 英文:Floating Point
Python
里的类名:float
在Python
中,浮点数常量不像整数那样有那么多进制可以用,只有十进制。
浮点数常量有两种表示方式:浮点表示和指数表示(即科学计数法)
# 由整数部分、小数点以及小数部分组成。
# 整数部分以及小数部分和十进制整数类似,但是都可以以0开头。
# 整数部分是可选的,若无则视作0。
# 小数部分也是可选的,若无则视作0。需要注意的是:这样做得到的不是整数而是浮点数,尽管他们在值上相等。
# 整数部分和小数部分不可同时省略。
# 以下都是合法的浮点表示。
3.14
0123.1
1.
0_1.1_2_3
123.
0123.
.1
.0
# 指数表示由浮点数部分和指数部分构成。
# 浮点数部分和浮点数表示基本一致,但是也可以没有小数点。
# 指数部分由"E"或者"e"开头,接下来可选添加指数的符号"+"和"-",最后是可以以0开头的十进制整数。
# 以下都是合法的指数表示。
3.14e0
314e-2
1.29e+03
.9e-1_0
9_8.e-01
复数是什么,是纯粹的数学知识,在此不做介绍。
在这里主要介绍Python
中虚部的表示。
在Python
中,虚数单位用j
表示。将其附加在十进制整数或者浮点数的后面表示这个数乘上虚数单位。使用十进制整数时,可以以0开头。
# 以下都是合法的虚数。
3.14j
10.j
010j
.001j
1e100j
3.14e-10j
3.14_15_9j
字符串是字符的序列。它可以用于存储文字信息。
Python
中的字符串是不可变对象。
我们已经知道计算机的世界是0与1的组合。存储在计算机世界的数据也一样,是0与1的序列。而字符就好比是文本数据的0与1的序列中有意义的最小子序列。一个英文字母I
,一个数字9
,一个特殊符号⑨
,一个汉字九
等等都是字符。
一个字符根据其内容和编码格式,对应的0与1的序列会有所不同。不同的字符所占的字节数也不同,有的一个字节,有的两个字节,还有的占更多。关于编码格式,请自行查阅相关资料。
与C/C++
、Java
等语言不同,Python
的内置数据结构中没有“字符”这个类型,而是用只有单个字符的字符串来代替。
字符串的内容被一对双引号"
或者一对单引号'
括起来,也可被一对"""
或者'''
括起来。两者的区别在于后者内部可以包含换行符。
字符串的内容被解释成什么字符,由引号前面的字符串前缀决定。字符串前缀不区分大小写。
Python
目前(3.7.4版本)有三种字符串前缀:
用法及更多细节见官方文档:https://docs.python.org/zh-cn/3/reference/lexical_analysis.html#string-and-bytes-literals
顾名思义,转义
就是改变原来的意思。
不仅仅是Python
中有,其他大多数的语言中也存在。
转义字符通常是由反斜杠\
与其他字符组成的,其中反斜杠的作用就是改变其后面的字符的含义。
常用的转义字符:
转义字符 | 含义 |
---|---|
\\ |
字符\ |
\' |
字符' |
\" |
字符" |
\n |
换行符 |
\t |
水平制表符 |
\[ooo] |
以三位八进制整数[ooo] 为编码的字符
|
\x[xx] |
以两位十六进制整数[xx] 为编码的字符
|
用法及更多细节见官方文档:https://docs.python.org/zh-cn/3/reference/lexical_analysis.html#string-and-bytes-literals
字符串常量的拼接可以直接接续在前面一个的后面,但是换行接续需要用括号以消歧义。例如:
'one''two' # 等价于'onetwo'
"three"'four' # 等价于'threefour'
(
"ab" # 你可以在这些地方添加注释
"cd" # 并且这不会对这个字符串造成影响
'ef'
) # 等价于'abcdef'
准确地来说,这并不是拼接,而是字符串常量的表示方法,尽管这看上去好像是把多个字符串拼在了一起。
拼接也可以用字符串的加法(运算符+)。但是每次做加法都会创建新的字符串,因此,如果需要大量拼接但不需要中间结果的字符串,建议使用str.join(s:str,iterable:Iterable)
方法。
如果需要得到一个字符串的若干次复制后并拼接起来的字符串,可以使用字符串的乘法(运算符*)。
"复读"*3 # 得到"复读复读复读"
3*"复读" # 也可得到"复读复读复读"
如果需要将某些数据填入一个具有固定格式的字符串的适当位置,则用直接拆开并拼接的方式是很不合理的,并且那样的格式也不易于阅读。字符串的格式化很好地解决了这个问题。
字符串的格式化一般有两种方法,一种是通过字符串的运算符%(是一个历史遗留问题,不推荐),另一种是通过str.format(s:str,*args,**kw)
方法(推荐)。
如果你学过C语言,那么你应该可以很快掌握运算符%的使用方法,它的风格和C语言的printf函数很像。详见官方文档:https://docs.python.org/zh-cn/3/library/stdtypes.html#old-string-formatting
字符串的实例是可迭代对象,是可用索引访问的,它就好像由一些单个字符的字符串构成的列表(但字符串不是列表的实例),也是可以切片的。 有关索引的语法,详见列表。 有关切片,详见语法糖。
字符串的迭代就像C语言里面遍历char数组的每个char那样。字符串每次迭代 返回的是当前迭代位置的单个字符组成的字符串。
判断字符串src是否包含子字符串target可以用运算符in:
target in src
str.index(src:str, sub:str, start:int=0, end:int=-1)->int
str.find(src:str, sub:str, start:int=0, end:int=-1)->int
两者都返回在src中从start索引开始,end索引结尾的子串中sub字符串第一次出现的索引。 对于1,若不存在子串将会引发异常。对于2,若不存在返回-1。
使用方法:str.replace(s:str, old:str, new:str)->str
。
正则表达式可以判断一个字符串是否符合某种格式,并获取格式中相应的信息。例如一个我们知道电子邮件地址的格式,对于一个具体的邮箱地址我们想知道它的邮箱服务器域名。如果要用索引与切片、子串查找之类方法来判断和提取信息,会变得很麻烦。而正则表达式则能够高效完成这个任务。
正则表达式的详细使用方法请参考Python
标准库的re模块资料,在这里只做简单介绍。
正则式(其实就是正则表达式,只为与另一标题区分)是正则匹配中最核心的。
一些元字符在正则式中具有特殊含义。
元字符 | 匹配规则 |
---|---|
. |
匹配除换行符以外的任意单个字符 |
* |
匹配位于* 之前的模式0次或更多次 |
+ |
匹配位于+ 之前的模式1次或更多次 |
` | ` |
^ |
匹配行首,匹配以^ 后面的字符开头的字符串 |
$ |
匹配行尾,匹配以$ 之前的字符结束的字符串 |
? |
匹配位于? 之前的模式0次或1次 |
\ |
表示位于\ 之后的为转义字符 |
[] |
匹配位于[] 中的任意一个字符 |
- |
用在[] 之内用来表示范围 |
() |
将位于() 内的内容作为一个整体来对待,并存入groups |
{} |
按{} 中的次数进行匹配 |
此外,还有一些转义字符。
转义字符 | 含义 |
---|---|
\b |
匹配单词头或单词尾 |
\B |
与\b 含义相反 |
\d |
匹配任何数字,相当于[0-9] |
\D |
与\d 含义相反 |
\s |
匹配任何空白字符 |
\S |
与\s 含义相反 |
\w |
匹配任何字母以及下划线,相当于[a-zA-Z0-9_] |
\W |
与\w 意义相反 |
其余普通的字符均匹配自身。
如果你下载了本教程附的代码,你可以直接运行它以查看结果,并加深自己的理解。 举个例子,一个可行的匹配电子邮件的正则式是:
rex=r"(\w+)@(\w+(\.\w+)+)"
简单解释一下:
\w+
用于匹配至少一个字符的单词。\w
的含义),且至少一位(即+
的含义)@
匹配自身。(\.\w+)+
用于匹配若干个.com
、.net
等域名成分,域名至少有一个.xxx
。(\w+(\.\w+)+)
匹配的是域名整体。有了正则式以后,我们还需要将其做成一个re.compile对象。这个对象可以重复使用,用来匹配符合正则式的字符串,得到相应的match对象。
# rex借用上文的正则式
import re
compile_object=re.compile(rex) # 这样就得到了编译对象compile_object
有了编译对象以后,我们就可以开始让它去匹配要匹配的字符串。匹配成功则返回一个match对象,否则返回 None
。
# compile_object使用的是上文的
target1="someone@example.com"
target2="someone@notcorrect"
match_object1=compile_object.match(target1) # 匹配成功
match_object2=compile_object.match(target2) # 匹配失败,match_object2是None
我们可以根据re.Match.group
方法得到匹配出的分组的信息。
第1和第2组是我们想要的用户名和邮箱服务器域名。
name=match_object1.group(1)
host=match_object2.group(2)
print(f"用户名:{name}")
print(f"主机域名:{host}")
英文:List
在Python
中的类名:list
列表有点像数组。列表和数组不同的是:里面存放的对象的类型不一定要一样。
可以直接用中括号[]
将以逗号隔开的需要放入列表的对象(或表达式)序列括起来,得到一个列表的实例。你也可以通过迭代解包来得到这些序列。空的列表可以用[]
来表示。
例如:
list1=[1,"a",False]
可以得到一个列表list1。
也可以用类list(iterable:Iterable)进行构造。传入一个可迭代对象即可,返回一个以可迭代对象的迭代 序列生成的列表 实例。 例如:
list2=list(range(5))
就可以得到一个内容为[0,1,2,3,4]
的列表list2。
此外,还有推导式可以生成列表,以及一些函数可能会返回一些列表。
列表既然能把对象装进去,自然能够将对象取出来。这需要索引。 索引就好像目录里的页码,知道了在第几页就能把要的找出来。
列表索引是一个整数,它代表列表中某个元素的位置。 列表索引也是如此,但是要注意的是:程序员数数总是从0开始。
也就是说,列表首元素的索引是0。 用索引访问列表的某个元素,可以用运算符[]。
和其他大多数语言不同的是:Python
有负的索引。负的索引是从最后开始数的,列表中最后一个对象的索引是-1,每向前一个索引减一。
例如,使用刚刚的代码中的list1,以下索引访问将得到以下结果:
list1[0] # 得到1
list1[1] # 得到"a"
list1[-1] # 得到False
list1[0]='change' # 此时list1变成了['change','a',False]
有关列表的其他资料,请参考官方文档:https://docs.python.org/zh-cn/3/library/stdtypes.html?highlight=list#lists
英文:Tuple
在Python
中的类名:tuple
简单地说,元组就是不可更改每个索引对应的对象的引用,也不可以增加或减少对象的引用的列表。
创造元组的方法和列表差不多,但用的是圆括号()
和tuple(iterable:Iterable)。
元组是不可变对象,但这个“不可变”仅限对对象的引用不变,并不是说元组内的对象都是不可变对象。 因此,元组是可哈希对象。
元组比列表少了改变引用的能力,那为什么不都用列表?
这是因为不可改变引用意味着更加安全可靠。如果我们不需要去修改一个表的内容,那就把它做成不可变的就可以了。
能用元组不用列表的时候尽量使用元组。
英文:Set
在Python
中的类名:set
集合这种数据结构和数学意义上的集合一样,具有确定性、无序性、唯一性,但是只能存放可哈希对象,并且哈希值与判断集合内两个元素是否相等密切相关。
创建集合和创建列表的方式差不多,只不过不用方括号[]
而是花括号{}
。set(iterable:Iterable)
也是好的。由于集合具有唯一性,故序列中的重复元素将会被自动筛去。
英文:Dictionary
在Python
中的类名:dict
字典在其他语言中也叫Map(映射),其作用就是将一个可哈希对象(作为键)与一个任意的对象(作为值)建立映射关系(即键值对)。一个字典包含的是若干键值对。同一个字典的键是不可能重复的,一个键只能对应一个值。
就像查字典一样,给字典一个单词,字典就能够告诉你这个单词的解释。当然,如果字典里没有这个单词,就找不到了。
类似于列表,字典也可以用类似的方法构建。将键值对用逗号分开,并用花括号{}
括起来即可得到一个字典。可以使用键值解包来添加键值对 序列。
键值对的写法:键:值
可以通过{}
来创建一个空的字典。
字典索引的语法类似于列表索引,但是[]
中不一定是整数,可以是任何可哈希对象。如果直接用运算符[]进行访问且作为右值,则当键不存在的时候将会引发异常。作为左值的情况,当键不存在将会创建相应的键。
比较保守的做法是使用dict.get(self:dict, key:object, default:object=None)->object
这样当键不存在时会返回default参数,并且default参数的默认值是None
。从代码量上来说,用运算符[]更加省事,但是用get更加安全。
变量是用于指向对象、使用对象的一个媒介。有关“指向”的含义,参考赋值。
变量名是变量的名字。在绝大多数时候,我们用变量名来指代变量本身,或者是指代变量所指向的对象。
Python
的变量命名规则如下:
赋值的语法主要有两种:
[左值]
=[右值]
即可
左值可以是变量名,也可以是可以被赋值的引用(如列表索引和字典索引计算得到的)。
只要是符合命名规则的变量名,都是可以被赋值的。如果原先这个变量名不存在,那么这个变量会被创建。如果存在,那么它的指向就会改变。
右值可以是任意的对象。
a=1
cdd='户山香澄'
shopping_list=['鸡蛋','西红柿','白糖','菜油']
today={'status':'good','weather':'sunny'}
基本的是这样的:
[左值1],[左值2],...[左值n]=[右值1],[右值2],...[右值n]
例如:
a,b=0,1
差不多和这效果一致:
a=0
b=1
而右边也可以只有一个对象。如果这样做,当左边有多个对象需要被赋值时,效果相当于
a,b,c=[1,2,3]
a,b,c=*[1,2,3]
有时候也叫做“指向”。这个概念比较抽象,如果需要直接解释可能需要很多C语言的知识,本人在此用一个形象的例子来解释。
假设有一个人A,父母给他取名为“王小明”(如有姓名雷同,纯属巧合)。那么,父母会用“王小明”这个名字来指代他,这就好比“王小明”这个名字指向了他这个人。王小明在公司上班,同事们都叫他“小王”。“小王”这个名字自然是从“王小明”来的。用Python
代码来讲这就是:
王小明="某个人A"
小王=王小明
假设后来公司里来了个新人B,也姓王,父母给她取名叫“王小红”(如有姓名雷同,纯属巧合)。由于年龄还是“王小明”大,于是同事们用“小王”称呼“王小红”,而改用“老王”称呼“王小明”。这用Python
代码来讲就是:
王小红="某个人B"
小王=王小红
老王=王小明 # 你可以忽略这一句,因为这一句和本例要表达的逻辑关系不大。
显然根据现实中的逻辑,现在“小王”指代(指向)的应该是"某个人B",并且“王小明”指代(指向)的应该是"某个人A"。
那么,你现在应该也可以理解这段代码(初学者极易犯的错误)是为什么了:
a=1
b=a
b=2
print(a) # 在控制台输出a的值,结果是1
此外,你也应该能够理解这里的函数f为什么无法改变外部变量的值:
def f(n):
n=100
a=1
f(a)
print(a) # 在控制台输出a的值,结果是1
一般来说,一个变量的作用域是从它被定义开始,到同一级结构的结尾处结束。
但是Python
变量的作用域有所不同。Python
是解释型语言,因此,变量名的解析与定向不像C语言那样在编译时就可以通过代码结构确定。Python
的变量名解析是通过字典来完成的。你可以通过内置函数来获取信息。
内置函数globals()->dict返回调用时所有全局变量的变量名到对象的映射字典。 内置函数locals()->dict返回调用处的局部变量名到对象的映射字典。
命名空间是一个用于存储名称的空间。它主要用来对名称进行管理,以避免重复名称之间的冲突。
在Python
中,一般来说,命名空间可以由模块和类来产生。命名空间也是对象,而且往往是字典。
在默认情况下,一个模块的命名空间包含这个模块脚本内的所有非下划线开头的全局变量以及def
声明的函数。你也可以通过定义__all__
列表来重新定义为外部提供命名空间所包含的名称。
在默认情况下,一个类的命名空间包含比这个类所多一个缩进的代码块内的所有名称,包括通过赋值语句产生的变量和def
定义的函数名称。
在默认情况下,类命名空间内的所有名称将会成为类对象的属性。
局部变量一般是指在函数内部定义的变量。它不属于任何命名空间,只是一个临时的变量,只有属于同一个局部的代码能对它进行正常解析,变量名超出了这个局部就不再指向这个局部所指向的对象。
全局变量是相对于局部变量来说的。全局变量指向的对象应当可以在任何地方被任何对象所引用。
在Python
中,如果需要在局部使用全局变量指向的对象,可以直接将全局变量的变量名作为右值使用,但是要注意:局部变量的名称解析优先于全局变量。如果想在一个函数的局部改变全局变量的引用(将变量名作为左值进行赋值),则需要使用关键字global,但是本人不建议这样做。随意修改全局变量是很危险的,可能会导致与该变量相联系的部分受到很大的影响。
当一个脚本被作为模块导入时,脚本中的全局变量将属于该模块的命名空间。 当一个脚本被当作主模块执行时,里面的全局变量属于main模块。
成员变量就像是对象的数据和方法一样的存在的变量。他们的访问方式是通过成员访问运算符(运算符.)。
当在一个类 命名空间中创建变量时,这个变量将会成为类的成员变量。
当一个模块被以import xxx
的方式导入后,模块中的全局变量将会成为模块的成员。
运算符是一些特殊的符号,我们可以用它来完成一些对对象的操作。运算符和对象构成了运算的表达式。
运算符需要有操作的对象才能工作起来。这些对象就是运算符的操作数。
有些运算符只需要一个操作数,叫做单目运算符。 有些运算符需要两个操作数,叫做双目运算符。
Python
中的运算符本质其实也是函数调用,即Python
支持运算符重载。内置类型的这些函数已经被实现,并且无法修改。
官方文档定义的运算符包括以下:
+ - * ** / // % @
<< >> & | ^ ~
< > <= >= == !=
而在本文的解释中,运算符还包括这些:
() [] .
= += -= *= /= //= %=
@= &= |= ^= >>= <<= **=
虽然运算符很多,但是我们还是可以大致这样分开讨论:
运算符重载是重新定义运算符的作用。其本质其实是定义一些特殊的函数。
运算符重载可以使得代码更加简洁,同时也可能会使代码变得生涩难懂,例如为自定义类Student
(包含一个学生信息的封装类)重载运算符+就显得莫名其妙。但是有些类的运算符重载就很符合逻辑,例如第三方库numpy中的numpy.matrix
(矩阵类)就重载了运算符+。这样,矩阵的加法可以通过运算符+来进行,而不需要去记忆一些名称复杂的库函数来完成,并且这样写也可以让一些逻辑更加像数学中的写法。机制允许你对任何自定义的类进行运算符重载,但是需不需要这样做,以及这样做好不好,就难说了。
算术运算符一般用于对数的运算操作。
运算符+可以作为单目运算符和双目运算符。
作为单目运算符,是取正。
与取正运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__pos__(self) |
+ self |
self指代操作数自身。
以下是一些内置类型的取正的含义:
操作数 | 返回值 |
---|---|
数 | 数学意义上的正号,相当于乘以正一 |
作为双目运算符,是加法。
与加法运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__add__(self, other) |
self + other |
__radd__(self, other) |
other + self |
其中字母r
的含义大概就是right
(右边)。
尽管加法应当满足交换律,但是你很快会知道,交换律不一定成立,例如字符串的加法。
当一个类重载了函数__add__
后,这个类的实例就可以作为运算符+的左操作数。
当一个类重载了函数__radd__
后,这个类的实例就可以作为运算符+的右操作数。
在重载的函数的函数体中,应当对另一个操作数other的类型做类型检查。如果认为接收到的类型不支持,应当返回NotImplemented。
运算符+会先尝试调用左操作数的__add__
方法,如果返回了NotImplemented或者未定义,就再尝试右操作数的__radd__
。如果两者都返回了NotImplemented或未定义,那么将引发异常。
如果右操作数类型为左操作数类型的一个子类,且该子类提供了指定运算的反射方法(以r开头的,如
__radd__(self, other)
),则此方法会先于左操作数的非反射方法被调用。此行为可允许子类重载其祖先类的运算符。这对于所有具有反射方法的运算符都适用。
其他的需要两个操作数的运算符(双目运算符)机制与上述类似,后文将不再赘述。
以下是一些内置类型加法的含义:
左操作数 | 右操作数 | 返回值 |
---|---|---|
数 | 数 | 数学意义的加法结果 |
字符串 | 字符串 | 拼接后的字符串 |
列表 | 列表 | 拼接后的列表 |
元组 | 元组 | 拼接后的元组 |
一个重载的例子:
from functools import wraps
def show(func):
@wraps(func)
def shower(*args,**kw):
print(func.__name__)
return func(*args,**kw)
return shower
class LR:
def __init__(self,x):
self.x=x
@show
def __pos__(self):
return self
@show
def __add__(self,other):
if isinstance(other,LR):
return self.x+other.x
return self.x+other
@show
def __radd__(self,other):
if isinstance(other,LR):
return self.x+other.x
return self.x+other
l=LR(2)
r=LR(3)
print(l+1)
print(1+r)
print(l+r)
print(+r)
运算符-可以作为单目运算符和双目运算符。
作为单目运算符,是取负。
与取负运算有关的函数如下:
函数原型 | 对应运算 |
---|---|
__neg__(self) |
- self |
以下是一些内置类型的取负的含义:
操作数 | 返回值 |
---|---|
数 | 数学意义上的负号,相当于乘以负一 |
作为双目运算符,是减法。
与减法运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__sub__(self, other) |
self - other |
__rsub__(self, other) |
other - self |
以下是一些内置类型减法的含义:
左操作数 | 右操作数 | 返回值 |
---|---|---|
数 | 数 | 数学意义的减法结果 |
集合 | 集合 | 两者的差集 |
运算符*可以作为单目运算符和双目运算符。
准确来说,作为单目运算符,是解包的表达式,返回的是一个序列(并不是一个对象)。详情请参考解包传递相关内容。
作为双目运算符,是乘法。
与乘法运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__mul__(self, other) |
self * other |
__rmul__(self, other) |
other * self |
以下是一些内置类型乘法的含义:
左操作数 | 右操作数 | 返回值 |
---|---|---|
数 | 数 | 数学意义的乘法结果 |
列表 | 整数 | 列表复制整数倍后的列表,若为0或负整数,返回空列表 |
整数 | 列表 | 列表复制整数倍后的列表,若为0或负整数,返回空列表 |
元组 | 整数 | 类似于列表 |
整数 | 元组 | 类似于列表 |
字符串 | 整数 | 类似于列表 |
整数 | 字符串 | 类似于列表 |
运算符**可以作为单目运算符和双目运算符。
准确来说,作为单目运算符,是解包的表达式,对对象使用mapping协议。详情请参考解包传递相关内容。
作为双目运算符,是求幂。它与内置函数pow(base, index[, modulo])有一定的关系。
与乘法运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__pow__(self, other[, modulo]) |
self ** other(当传入第三个参数时,对应内置函数pow) |
__rpow__(self, other) |
other ** self |
重载了这两个函数之一的类也会使得内置函数pow接收到该类型的参数时产生关联。
以下是一些数求幂的结果:
左操作数 | 右操作数 | 返回值 |
---|---|---|
整数 | 整数 | 整数的幂(精确,不存在误差) |
非负实数 | 实数 | 实数幂(存在舍入误差) |
负实数 | 非整数 | 利用欧拉公式等求得的幂,辐角取主值(存在舍入误差) |
数 | 复数 | 利用欧拉公式等求得的幂,辐角取主值(存在舍入误差) |
复数 | 数 | 利用欧拉公式等求得的幂,辐角取主值(存在舍入误差) |
与除法运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__truediv__(self, other) |
self / other |
__rtruediv__(self, other) |
other / self |
运算符//是双目运算符。对于数,其含义是地板除法,得到的是对实数除法的结果用高斯函数取整的值,但是当有操作数是浮点数时,返回的总是浮点数。
例如:
1//2 # 得到0
7//3 # 得到2
(-7)//3 # 得到-3
3.8//1.3 # 得到2.0
与地板除法运算符有关的函数如下:
函数原型 | 对应运算 |
---|---|
__floordiv__(self, other) |
self // other |
__rfloordiv__(self, other) |
other // self |
运算符%是双目运算符。
对于数,其含义是取模。如果a
和b
都是实数,则a
%b
得到的大致等于a-((a//b)*b)
例如:
1%2 # 得到1
7%3 # 得到1
(-7)%3 # 得到2
3.8%1.3 # 得到1.1999999999999997(存在一定的舍入误差)
对于字符串作为左操作数的情况,将会用右操作数对左边的字符串进行格式化,返回 格式化后的字符串。格式化的风格和C语言的printf很像。
但是这样的字符串并不是很好,甚至官方文档里面也有吐槽:
注解: 此处介绍的格式化操作具有多种怪异特性,可能导致许多常见错误(例如无法正确显示元组和字典)。 使用较新的 格式化 字符串字面值,str.format() 接口或 模板字符串 有助于避免这样的错误。 这些替代方案中的每一种都更好地权衡并提供了简单、灵活以及可扩展性优势。
详细格式化的方法请参考官方文档:https://docs.python.org/zh-cn/3/library/stdtypes.html#old-string-formatting
与运算符%有关的函数如下:
函数原型 | 对应运算 |
---|---|
__mod__(self, other) |
self % other |
__rmod__(self, other) |
other % self |
当左操作数是字符串且占位符个数与获得的填充元素个数不一致时,将会引发异常。
本人已知的符号@在Python
中有两个作用,而大部分人只知道它和装饰器有关。
这个不为人知的作用就是矩阵乘法运算符。(这还是谷歌了英文内容才找到的,装逼神器)
与运算符@有关的函数如下:
函数原型 | 对应运算 |
---|---|
__matmul__(self, other) |
self @ other |
__rmatmul__(self, other) |
other @ self |
Python
第三方数学库numpy中就有矩阵类,重载了这个运算符。如果你没有安装numpy模块,以下的代码将无法正常运行。
import numpy
m1=[[1,0,0],[0,2,0],[0,0,3]]
m2=[[2,0,0],[0,2,0],[0,0,2]]
mat1=numpy.asmatrix(m1)
mat2=numpy.asmatrix(m2)
print(mat1)
print(mat2)
print(mat1@mat2)
逻辑运算符用于逻辑的“运算”。逻辑的运算总是围绕着布尔类型的。
关于逻辑,你需要知道三种基本逻辑。
a
或b
:为False当且仅当a
和b
都为Falsea
与b
:为True当且仅当a
和b
都为Truea
:为True当且仅当a
为False。简单来说,可以化成下面的真值表:
a |
b |
a 或b |
a 与b |
非a |
---|---|---|---|---|
True |
True |
True |
True |
False |
True |
False |
True |
False |
False |
False |
True |
True |
False |
True |
False |
False |
False |
False |
True |
在Python
中,逻辑运算符或、与、非都是以关键字的形式存在的,分别是or
、and
、not
,但大多数语言都是以符号形式存在的,例如C语言的或与非分别是||
、&&
、!
。在本文中,我们姑且将这三个关键字视作运算符。
有趣的是,在Python
中传给这些逻辑运算符的对象不一定要是布尔类型的,这一点和Javascript很像。那么如何判断非布尔类型的实例的逻辑呢?答案是通过内置函数bool(x:object)
。
如果希望非布尔类型的对象能够拥有逻辑值,可以通过重写 函数:__bool__(self)->bool
来实现。在默认情况下,一个自定义类的实例的布尔值总是True。
下面是一些内置类型的特殊量的布尔值:
对象 | 布尔值 |
---|---|
空列表 | False |
空元组 | False |
空字典 | False |
空字符串 | False |
整数0 | False |
浮点数0.0 | False |
复数0j | False |
非空字符串,如'0' | True |
非空列表,如[0] | True |
简单而言,空的或者为0的内置类型的实例布尔值都是False,不为空的内置类型实例布尔值为True。
运算符not将会返回与操作数布尔值相反的布尔值,并且一定返回布尔值。
运算符and将会返回第一个布尔值为False的对象。如果两者的布尔值都是True,那么返回后一个对象。
例如:
a=[] and True
b=True and ""
c=[] and {}
d=1 and "abc"
print(a,b,c,d) # 结果:[] [] abc
# b是空字符串,所以看不到
运算符or将会返回第一个布尔值为True的对象。如果两者的布尔值都是False,那么返回后一个对象。
例如:
a=[] or True
b=True or ""
c=[] or {}
d=1 or "abc"
print(a,b,c,d) # True True {} 1
如果你理解了运算符相当于函数体,而函数在返回后就会结束执行,那么应该可以很快理解什么是逻辑短路。
如果运算符and运算符的第一个操作数布尔值为False,那么第二个操作数会被短路,如果是表达式,则不会被计算。 如果or运算符的第一个操作数布尔值为True,那么第二个操作数会被短路,如果是表达式,则不会被计算。
虽然逻辑运算的优先级比较低,但是当局部可以决定整体的逻辑时,逻辑运算的过程会马上结束,不去管无法改变最终逻辑结果的部分,即使那部分可能会带来一些别的工作。这就好比NBA总决赛7局4胜,打到4:2就不打第7局一样。这不是计算机在偷懒,也不是什么奇葩的机制,相反,机智的程序员会很好地利用这一点。
下面的代码可以加深你的理解。
def t():
print('调用函数t()')
return True
def f():
print('调用函数f()')
return False
def test(n):
print(f"第{n}次测试")
test(1)
a=f() and t()
test(2)
b=f() and f()
test(3)
c=t() and t()
test(4)
d=t() and f()
print(a,b,c,d)
test(5)
a=f() or t()
test(6)
b=f() or f()
test(7)
c=t() or t()
test(8)
d=t() or f()
print(a,b,c,d)
比较运算符主要是用于比较大小,因此他们都是双目运算符。它们应当返回一个布尔值。
比较两个不同类型的对象的大小,不是很推荐,除非他们所属的类有着一定的继承关系。
Python
官方文档没有将这些归为运算符,而是归为分隔符
。
比较运算符包括以下:
即运算符<。
对应的可重写 函数为
__lt__(self, other)
lt
大概是less than的缩写。
一般来说,若运算符<的左操作数比右操作数小,将返回True,否则返回False。
对于实数,其大小就是数学意义上的大小。 对于字符串,其大小就是字典序大小。 对于列表和元组,其大小比较的规则类似于字典序,将按照顺序对包含的对象进行比较。 字典不可以比较大小,但是可以比较是否相等,两个字典相等当且仅当所有的键值对相等。 集合可以比较大小,但是比较逻辑比较奇怪,不建议使用。当一个集合是另一个集合的真子集的时候,真子集更小。当两个集合的元素完全相同时两个集合相等。其他情况都是既不大于也不小于也不相等,但是不会引发异常。
对于自定义的类,默认是不支持比较的,需要显式地重写 函数__lt__(self, other)
以使用运算符<进行比较。
一般来说,我们最好保证运算符<有传递性
:即如果a < b
成立并且b < c
成立,那么应该有a < c
成立。
需要注意的是,重载了运算符<后就不必再重载运算符>,除非有这样的必要,因为大于的逻辑和小于应当是对称的,要判断
a > b
是否成立只需要判断b < a
是否成立就可以了。
如果类A重写了__lt__(self, other)
函数,那么对于类A的实例a1
和a2
,当需要对表达式a1 > a2
进行求值的时候,会尝试调用A.__lt__(a2, a1)
。类似的,如果重载了运算符>的类B实例b1
和b2
需要对表达式b1 < b2
求值时也会尝试调用B.__gt__(b2, b1)
一样。
由重载了运算符<的自定义类的实例构成的列表可以正常地被排序。
即运算符>。
对应的可重写 函数是
__gt__(self,other)
。
gt
大概是greater than的缩写。其他的特性与运算符<基本一致。
即运算符<=。
对应的可重写 函数是
__le__(self,other)
。
le
大概是less和equal的缩写。
一般来说,若运算符<=的左操作数比右操作数小或者相等,则返回True,否则返回False。这里的大小
和运算符<中的一致。
除了不能作为排序的依据,以及不建议使得运算符>=和运算符<=逻辑完全相反以外,其他的特性与运算符<基本一致。与运算符<=是一对。
需要注意:运算符<=与运算符<没有隐式的联系。如果一个类只重载运算符<,不会导致运算符<=也被重载。如果希望能够产生这些方便的隐式的联系,请参考官方文档的functools.total_ordering:https://docs.python.org/zh-cn/3/library/functools.html#functools.total_ordering
即运算符>=。
对应的可重写 函数是
__ge__(self,other)
。
ge
大概是greater和equal的缩写。除了不能作为排序的依据,以及不建议使得运算符>=和运算符<=逻辑完全相反以外,其他的特性与运算符<基本一致。与运算符<=是一对。
即运算符==。
对应的可重写 函数是
__eq__(self,other)
。
eq
大概是equal的缩写。
一般地,当左操作数和右操作数相等时返回True,否则返回False。
对于内置类型,这里的相等
和运算符<中的一致。
需要注意的是:如果一个类 重写了这个函数,将会隐式地将__hash__
设置成None
,使得该类的实例成为不可哈希对象,除非__hash__
也被显式地重写。
在不重写__eq__
的默认情况下,自定义类的运算符==将判断两个对象是否是同一个对象,即判断其所指向的内存中的位置是否相等。
即运算符!=。
对应的可重写 函数是
__ne__(self,other)
。
ne
大概是not eqaual的缩写。
在不重写这个函数的时候,其逻辑与运算符==相反。重写这个函数不会导致__hash__
被隐式地设置成None
。
位运算,是计算机科学中的一种快速而强大的运算功能。
计算机内部表示整数一般都是用二进制表示的,并不会用十进制数码的字符串来表示,那样做运算效率太低。既然是二进制数表示的,那么就有许多二进制位了,每一个位只可能是0或者1,而位运算,就是对这些二进制位进行操作。当然,对于不同的Python
对象,位运算符有不同的含义,以上只是对数来说的。有关整数在计算机的内部表示,请参考计算机中的整数表示。
左移运算符。即运算符<<。
函数原型 | 对应运算 |
---|---|
__lshift__(self, other) |
self << other |
__rlshift__(self, other) |
other << self |
对于整数,左移运算就是将左操作数的所有二进制位向高位移动右操作数次,低位用0补齐。在一定程度上,一个整数左移a位和乘上2的a次方差不多。其他的内置类型都不支持这个运算符。
在C++
的代码中,经常可以看到cout << a
之类的表达式。其实这就是重载了运算符<<,使得对象可以向流传递信息。在Python
中没有这样的机制,当然在Python
中你可以自己定义一些类并重载这些运算符来模仿这种行为。
右移运算符。即运算符>>。
函数原型 | 对应运算 |
---|---|
__rshift__(self, other) |
self >> other |
__rrshift__(self, other) |
other >> self |
对于整数,右移运算(算数右移)就是将左操作数的所有二进制位向低位移动右操作数次,高位用符号位补齐,移出最低位的部分将会被忽略。在一定程度上,一个整数右移a位和整除2的a次方差不多。其他的内置类型都不支持这个运算符。
位与运算符。即运算符&。
对于整数,位与运算是指将两个操作数的各个二进制位对应进行and运算(1代表True,0代表False,高位不存在时用符号位补齐),将得到的各个结果放到对应的位上得到结果整数。
例如:
0b1100 & 0b1010 # 得到0b1000(十进制的8),即12 & 10将会得到8
函数原型 | 对应运算 |
---|---|
__and__(self, other) |
self & other |
__rand__(self, other) |
other & self |
下面是一些内置类型进行这个运算的含义。
左操作数 | 右操作数 | 返回值 |
---|---|---|
整数 | 整数 | 两个数位与运算后得到的整数 |
布尔类型 | 布尔类型 | 相当于左操作数 and 右操作数 |
布尔类型 | 整数 | 将True转换成1,False转换成0再进行整数的位与运算 |
整数 | 布尔类型 | 将True转换成1,False转换成0再进行整数的位与运算 |
集合 | 集合 | 两者的交集 |
位或运算符。即运算符|。
对于整数,位与运算是指将两个操作数的各个二进制位对应进行or运算(1代表True,0代表False,高位不存在时用符号位补齐),将得到的各个结果放到对应的位上得到结果整数。
例如:
0b1100 | 0b1010 # 得到0b1110(十进制的14),即12 | 10将会得到14
函数原型 | 对应运算 |
---|---|
__or__(self, other) |
self |
__ror__(self, other) |
other |
下面是一些内置类型进行这个运算的含义。
左操作数 | 右操作数 | 返回值 |
---|---|---|
整数 | 整数 | 两个数位或运算后得到的整数 |
布尔类型 | 布尔类型 | 相当于左操作数 or 右操作数 |
布尔类型 | 整数 | 将True转换成1,False转换成0再进行整数的位或运算 |
整数 | 布尔类型 | 将True转换成1,False转换成0再进行整数的位或运算 |
集合 | 集合 | 两者的并集 |
整数的位与、位或运算的一个作用:位或为一个整数的某个二进制位置位,位与判断一个整数的某个二进制位是否被置位。这在用于控制复杂的模式的时候会很好用,例如re模块中正则表达式的匹配模式,就可以通过传入不同的标志(Flags)来完成匹配模式的传递。如果我们用一个整数来表示所有的模式,我们就可以用位或运算来将模式叠加,用位与来判断是否添加了这个模式。
位异或运算符。即运算符^。
布尔类型中异或(xor)的逻辑:当两者同为True或同为False时,返回False,当两者不同时,返回True。
真值表如下:
a | b | a xor b |
---|---|---|
True |
True |
False |
True |
False |
True |
False |
True |
True |
False |
False |
False |
注意:Python
中xor不像and
、or
那样,它不是一个关键字!
对于整数,位异或运算是指将两个操作数的各个二进制位对应进行xor运算(1代表True,0代表False,高位不存在用符号位补齐),将得到的各个结果放到对应的位上得到结果整数。
例如:
0b1100 ^ 0b1010 # 得到0b0110(十进制的6),即12 ^ 10将会得到6
函数原型 | 对应运算 |
---|---|
__xor__(self, other) |
self ^ other |
__rxor__(self, other) |
other ^ self |
下面是一些内置类型进行这个运算的含义。
左操作数 | 右操作数 | 返回值 |
---|---|---|
整数 | 整数 | 两个数位与运算后得到的整数 |
布尔类型 | 布尔类型 | 相当于左操作数 and 右操作数 |
布尔类型 | 整数 | 将True转换成1,False转换成0再进行整数的位异或运算 |
整数 | 布尔类型 | 将True转换成1,False转换成0再进行整数的位异或运算 |
集合 | 集合 | 两者的对称差集,相当于`(a |
实际上,对于整数,a ^ b
也总是和(a | b) - (a & b)
等价。
按位取反运算符。即运算符~。单目运算符。
操作数 | 返回值 |
---|---|
整数 | 整数按位取反的结果 |
布尔类型 | ~True 得到-2,~False 得到-1,和~1 ,~0 一致 |
不同于C/C++
、Java
、JavaScript
等语言,Python
的赋值运算符不能用于构成表达式,只能用于构成赋值语句或键值传递。
在
Javascript
中,有一种简化代码的手段是利用赋值语句的返回值。例如下面的Typescript代码:
namespace package1 {
export function func():string {
return ""
}
}
package1.func()
经过
tsc
编译成JavaScript
后的代码是这样的:
var package1;
(function (package1) {
function func() {
return "";
}
package1.func = func;
})(package1 || (package1 = {})); // 注意这一行,利用了赋值语句的返回值
package1.func();
有兴趣的读者可以研究一下这样利用赋值语句返回值的美妙之处。当然,
Python
并不允许你使用赋值语句的返回值。
由于Python
变量创建机制的问题,赋值运算符在Python
中的机制和其他语言不太一样,他们没有返回值。但是,在重载他们的时候,我们应当在重写的函数体中返回一个对象的引用。
即运算符=。
最基础的赋值运算符,将一个对象的引用与一个变量名绑定,不可重载。可以用在任何合法的代码空间中,可以在默认参数初始化的地方使用,也可以在需要键值传递的地方使用。
此外,关于运算符=有一个语法糖,即可以为同一个对象一次性绑定多个变量名:
a=b=c=d="hello"
这样做,将会使得a
、b
、c
、d
四个变量同时指向字符串"hello"。
但是这在C/C++
、Java
这类语言里面是这样的(假设已经将四个变量声明):
结合顺序从右到左,先执行d="hello",返回"hello"。
然后执行c='hello',返回"hello"。
然后执行b='hello',返回"hello"。
然后执行a='hello',返回"hello"。
最后一个返回值没有被利用,但这无关紧要。
在此以运算符+=为例,其余的赋值运算符都与之类似。
运算符+=意为“加后赋值”,将右操作数与左操作数相加后的结果赋值给左操作数。左操作数必须能够作为左值。
以下的两段代码的逻辑差不多。
a=2019
a+=1
a=2019
a=a+1
但是其实比这要复杂,下面会展开介绍。
在展开介绍之前,我要说些题外话:
在C/C++
、Java
等语言中,int类型的整数可能由一个简单而高效的寄存器来存储,或者在内存中就只有固定大小的字节数供存储,一般来说只有4个字节。而Python
的int没有长度限制,只要内存还能放,就能加长。
在C/C++
、Java
等语言中,int类型的运算符+=就是直接把右操作数加到左操作数上,改变左操作数的值,返回改变后的值。这和一般的运算符接收一些操作数,返回一个操作数有些不一样,它改变了操作数,而之前我们讲到的运算符(除了运算符=)基本上都不会改变操作数。而Python
里的整数是不可变对象,不可以被改变,可能就是因为整数长度无限制,如果可以随意改变,一会儿很长,一会儿很短,那么内存空间的释放回收将会变得很麻烦。因此,要实现经过+=运算以后,改变变量所引用的对象的值是不可能的,办法是:改变变量的引用。
运算符+=对应的可重写 函数是
__iadd__(self, other)
其中i的含义大概是immediately(立即)。
x+=y
会先尝试调用x=x.__iadd__(y)
。如果__iadd__
没有被实现或者返回了NotImplemented,那么将会尝试执行x=x+y。因此,运算符x+=y的含义并不与x=x+y完全相同。
其余的赋值运算符机制与此类似,此处只是列出其对应的可重写 函数,不再重复。
函数原型 | 运算符 |
---|---|
__isub__(self, other) |
-= |
__imul__(self, other) |
*= |
__imatmul__(self, other) |
@= |
__itruediv__(self, other) |
/= |
__ifloordiv__(self, other) |
//= |
__imod__(self, other) |
%= |
__ipow__(self, other[, modulo]) |
**= |
__ilshift__(self, other) |
<<= |
__irshift__(self, other) |
>>= |
__iand__(self, other) |
&= |
__ixor__(self, other) |
^= |
__ior__(self, other) |
` |
剩下的运算符还有以下几个:
即运算符[]。
运算符[]主要用于进行索引访问,返回 索引对应对象(可返回可作为左值的引用)。并且,使用的方法和之前提到的运算符也不同,形式如a[b]
。
对于字符串、列表、元组这些具有序列的,单个元素的索引是其在序列中的编号。它们应当是整数。在计算机的世界,计数通常是从0开始数的。
可重写 函数有两个,对应的功能也不同,一个用于赋值(所谓的“返回 引用”),一个用于获取内容。
函数原型 | 对应运算 |
---|---|
__getitem__(self, index) |
self[index](只能做右值) |
__setitem__(self, index, value) |
self[index]=value |
重写了__getitem__
并且没有将__iter__
设置成None
的类的实例,将成为可迭代对象。
运算符()有很多地方可以用到。
一个作用是结合,使得被包括的部分成为整体,并调整运算顺序及结构。这和数学意义上的括号很像。这个作用不可以重载。
另一个作用就是函数调用。
函数原型 | 对应运算 |
---|---|
__call__(self[, args...]) |
self([args...]) |
参数列表只需要保证至少有第一个self即可,后面的可以根据需要添加。一个类只要重写了__call__
方法,其实例就可以被调用,即与运算符()作用。
运算符,用于构成元组、参数传递中的分隔的作用。在大多数序列中,多一个结尾的逗号是没有关系的,例如[1,2,3,]
是合法的列表。当构成元组不会引起歧义时,也可以不用括号,例如这样的赋值语句也是可以的:
t=1,'b',[3]
在C/C++
中,逗号,
是一个不折不扣的运算符,它按照从左到右的顺序执行表达式的计算,并返回最后一个表达式的值。只不过,其优先级非常低,并且也很少有特殊的用途。
运算符.即成员访问运算符。其作用主要是成员(包括成员变量和成员函数)以及命名空间访问。
关于Python
的成员访问的机制,说起来有些复杂。Python
的很多复杂的机制都是方便程序员的,这对没有任何基础的外行十分不友好。在这里我只介绍最简单的对象属性访问机制,其余如果有兴趣可以自行查阅官方文档了解:https://docs.python.org/zh-cn/3/reference/datamodel.html#customizing-module-attribute-access。
运算符.的语法约束是对象.属性名称
。对象可以是任何对象,但是当对象是整数常量时需要加一个空格,例如13 .__hash__()
。属性名称可以是任意合法的变量名。
函数原型 | 对应运算 | 注释 |
---|---|---|
__getattribute__(self, name:str) |
self.name |
|
__getattr__(self, name:str) |
self.name |
仅当属性访问以AttributeError异常失败时 |
__setattr__(self, name:str, value) |
self.name=value |
仅当为属性赋值时 |
__delattr__(self, name:str) |
del self.name |
仅当销毁对象属性时调用 |
这里的self.name的name将会作为字符串传给参数列表里的name。
在默认情况(不重写以上任何一个函数的自定义类型)下,一个类AClass的实例obj的运算符.大致如下:
__dict__
,这个字典总是可以直接用obj.__dict__
访问,并且这个字典不可被赋值成字典以外的类型的对象。如果尝试使用del关键字进行删除,那么只是会清空字典内容。这个字典可以修改内容,和普通的字典基本一样。__add__
等)的属性名称加上前缀_类名
,即_AClass
,再进行后一步的操作,例如调用obj.__hide=1
,将会转换成obj._AClass__hide=1
来做,而在类外部的代码块访问成员时,不会这样处理。这实际上就悄悄地屏蔽了外部对加前置双下划线的属性的直接访问,虽然外部还是可以通过__dict__
或者_类名
+原属姓名(在本例中即_AClass__hide
)来访问这些属性。这是一种常用的私有成员机制。
__dict__
,来进行获取值或赋值值。如果成员访问表达式作为左值,就相当于直接对字典进行操作。例如obj.a1="hello"
相当于obj.__dict__["a1"]="hello"
。如果作为右值,并且存在对应的键,也会正常返回。__dict__
的键中,并且这个成员访问表达式作为右值使用,那么将会尝试往类的__dict__
查找。例如a=obj.x
,其中x
是obj.__dict__
中不存在的属性。如果类的__dict__
中也不存在这个键,那么将会引发异常。在Python
中,in是一个关键字。
运算符in是双目运算符。
对于内置的类型,当左操作数存在于右操作数容器中时,返回True,否则返回False。
可重写 函数:
__contains__(self, other)
。
左操作数 | 右操作数 | 返回值 |
---|---|---|
任意对象 | 列表/元组/集合 | 左操作数在右边的容器中返回True,否则返回 False |
任意对象 | 字典 | 左操作数在字典的键中返回True,否则返回 False |
字符串 | 字符串 | 左字符串是右字符串子串返回True,否则返回 False |
在Python
中,is是一个关键字,可用于判断两个对象是否是同一个对象。
当且仅当左操作数和右操作数是同一个对象(内存中的地址相同)返回True,否则返回False。不可重载。
关键字一览 常用关键字的语句
你也可以通过模块keyword来查看它们。
False
None
True
and
as
assert
async
await
break
class
continue
def
del
elif
else
except
finally
for
from
global
if
import
in
is
lambda
nonlocal
not
or
pass
raise
return
try
while
with
yield
None
的含义是NoneType类型的常量值。
参考运算符and。
用于断言。
如果表达式的布尔值为False,并且模式是debug模式,将会引发异常。
def f(n):
assert isinstance(n,int)
return "我是复读姬"*n
f(3) # 正常
f('a') # 引发AssertionError异常
暂无
暂无
用于构建自定义类。
用法:
class 类名1(父类1, 父类2, ...):
代码块1
代码块1将被执行,其中使用def
定义的函数将被绑定到类的命名空间中。
用于跳过本次循环的剩余代码块,到下一次循环。
用于自定义函数。
使用def
声明的函数的名称将被绑定到当前脚本所执行位置所处的的命名空间。
例如,在脚本的全局声明将被绑定到该脚本所属模块的命名空间,在class中使用def
定义函数将会绑定到class的命名空间(即成为这个class的类对象的属性),在def
定义的函数的函数体内使用def
声明将仅在调用函数时绑定到一个临时的函数命名空间。
def func1():
print('模块命名空间内的函数func1')
def func2():
print('func1的临时函数命名空间内的func2')
func2()
func1()
class Class1:
def func3(self):
print(f'self={self}')
print('类Class1的类明明空间内的func3')
Class1.func3(None)
Class1().func3()
else:
if
的简化,但是与其有微妙的不同(两者不可以完全替换,会产生一些语法问题)。
可以借此关键字实现switch语句的简单转换。
详见if语句。
详见for语句。
for也可以用于推导式。
一般用于import语句。在协程中也有使用。
语法:global
变量名
详见if语句。
if还可以用于三元表达式和推导式。
三元表达式:
表达式1 if 条件表达式 else 表达式2
此表达式当条件表达式
的布尔值为True时将计算并返回表达式1
,否则将计算并返回表达式2
。与C语言里面的三元运算符a ? b : c
有点相似。
需要注意的是,表达式1
和表达式2
的值只有一个会被计算,就像if语句中if后和else后的两个语句块只有一个会被执行那样。三元表达式本意是为了简化功能简单的if语句,同时其本身也是一个表达式。
详见模块。
详见运算符in。
详见运算符is。
用于构成lambda表达式。
暂无
详见运算符not。
详见运算符or。
用于填补语句块结构的空缺。它什么也不做。一般单独成行。
用于引发异常。
详见返回语句。
详见while语句。
详见with语句。
详见生成器。
if语句的结构如下:
if 表达式1:
代码块1
elif 表达式2:
代码块2
elif 表达式3:
代码块3
# 可以有任意个elif结构
else:
代码块n
# 可以有0~1个else结构
运行时,将先计算表达式1的布尔值。若表达式1的布尔值为True,代码块1将被执行。代码块1执行后,整个if结构执行完毕。(无论后面有多少elif和else)
如果表达式1的布尔值为False,代码块1将被跳过,并计算表达式2的布尔值。若表达式2的布尔值为True,代码块2将被执行。代码块2执行后,整个if结构执行完毕。(无论后面有多少elif和else)
若表达式2的布尔值也为False,则以此类推向后推进,一直到最后一个elif的表达式。若该表达式的值的布尔值也为False,则代码块n将被执行。
简单地说,if语句必须以if开头,中间夹杂着若干个可有可无的elif,else也可有可无,但只能有一个并且放最后。每个if、elif、else都需要跟上一个代码块。if语句只会执行结构中第一个表达式布尔值为True对应的代码块,若全为False则执行else的代码块(如果有的话)。
if语句主要用于控制条件跳转,使得一些代码只在特定的情况下被执行。
代码块的所有行比其对应的if、elif、else多一个缩进。
代码块的第一行可以写在冒号的后面,但是本人不建议这样做。
while语句的结构如下:
while 表达式1:
代码块1
else:
代码块2
# 可以有0~1个else结构
while语句用于循环。
首先计算表达式1的布尔值,如果为True,则执行代码块1。在代码块1中如果遇到了没有被其他循环套住的continue,则会跳过代码块1中的剩余代码。使得代码块1运行完毕。如果碰到了没有被其他循环套住的break,则会跳出整个while语句。
当代码块1执行完毕,将重新计算表达式1的布尔值。如果为True,则会执行代码块1,与上面介绍的一样,如此循环往复。
如果在计算表达式1的布尔值时得到了False,则else部分的代码块2将被执行。当代码块2执行完毕后,while语句结束。
while语句是循环的基础。主要的有三点:
以下两个例子将会帮助你理解while语句。
n=0
while n<5:
print(n)
n+=1
else:
print(n*10)
# 结果:
# 0
# 1
# 2
# 3
# 4
# 50
n=0
while n<10:
n+=1
if n%2==0:
continue
if n==7:
break
print(n)
# 结果:
# 1
# 3
# 5
用法:
try:
代码块0
# except结构可以有任意多个,捕获的类型是可选的。as结构为捕获的异常对象命名,使用时必须有前面的类型,是可选的。
except 类型1 as 变量名1:
代码块1
except 类型2:
代码块2
except:
代码块3
# else结构仅当存在except结构时才可以存在,可以有0~1个
else:
代码块n1
# finally结构可以有0~1个
finally:
代码块n2
执行try语句从下面的1开始。
try结构。一定有代码块0,执行代码块0。
except结构。
else结构。
finally结构。
重新引发异常。
在try/except/else/finally中使用return、break、continue是不建议的。这样很容易导致结构混乱。如果你想了解有关在try/except/else/finally中使用return、break、continue的详细机制,请参考官方文档:https://docs.python.org/zh-cn/3/reference/compound_stmts.html#the-try-statement
所谓遍历,就是将一个容器内的对象一个一个全部取出来,分别对它们进行一些操作。
for语句的结构如下:
for 变量名 in 可迭代对象:
代码块1
else:
代码块2
# 可以有0~1个else结构
当可迭代对象实现了__iter__
方法 返回一个迭代器,则for循环的逻辑大致和以下的语句一致:
临时迭代器=iter(可迭代对象)
临时状态=True
while 临时状态:
try:
变量名=next(临时迭代器)
except StopIteration:
临时状态=False
else:
代码块1
# 如果没有else和代码块2,此部分将被省略。
else:
代码块2
当可迭代对象没有实现__iter__
方法而是实现了__getitem__
方法,并且__iter__
没有被赋值成None
,则for循环的逻辑大致和以下的语句一致:
临时索引=0
状态=True
while 状态:
try:
变量名=可迭代对象[临时索引]
except IndexError:
状态=False
else:
代码块1
临时索引+=1
# 如果没有else和代码块2,此部分将被省略。
else:
代码块2
对于列表 实例作为可迭代对象,两个逻辑是一样的。你可以通过以下例子来加深对for循环的理解。
ls=[1,'abc',{'hello', 'world'}]
# part1
print("part1:")
for item in ls:
print(item)
# part2
print("part2:")
临时迭代器=iter(ls)
状态=True
while 状态:
try:
item=next(临时迭代器)
except StopIteration:
状态=False
else:
print(item)
# part3
print("part3:")
临时索引=0
状态=True
while 状态:
try:
item=ls[临时索引]
except IndexError:
状态=False
else:
print(item)
临时索引+=1
with语句的语法如下:
# as结构为表达式命名,可选。
with 表达式 as 变量名或变量名列表:
语句块
其逻辑大致等价于:
import sys
临时上下文管理器对象=表达式
临时异常类型,临时异常对象,临时回溯对象=None,None,None
变量名或变量名列表=临时上下文管理器对象.__enter__()
try:
语句块
except:
临时异常类型,临时异常对象,临时回溯对象=sys.exc_info()
finally:
if (临时异常类型,临时异常对象,临时回溯对象) != (None, None, None)
and not 临时上下文管理器对象.__exit__(临时异常类型,临时异常对象,临时回溯对象):
raise 临时异常对象
如果没有实现上下文管理所需要的两个特殊函数__enter__
和__exit__
,则会引起问题。如果在try语句块之前就引发了异常,那么异常将会直接视作由with语句引发。从finally结构中的逻辑,我们可以知道如果希望上下文管理器对象屏蔽异常,应当使得它调用__exit__
时返回一个布尔值为真的对象。不过请注意,不要让__exit__
引发传入的异常,这是我们重写它时需要确保的。
模块是一套具有相关的功能的代码集或程序集,它们往往负责一些功能。在Python
的代码中,模块也是对象。
英文:Package
包是特殊的模块。包是一些模块的容器,当然这些被容纳的模块也可以是包。尽管模块和一个Python
代码文件很像,包和文件夹很像,但是他们是不同的概念,这是因为模块不一定要来自于文件系统。在本文的介绍中,我们着重于来自代码文件的自定义模块。
直接来自于代码文件的模块可以是一个文件夹(包),也可以是一个Python
代码文件(普通模块),其文件夹名和文件名应当符合变量名的命名标准。一般地,我们不去用双下划线开头命名这些文件夹和代码文件,因为这些名称具有一定的特殊性。
一个文件夹如果包含了名为__init__.py
的代码文件,则此__init__.py
文件将被视作这个包的模块代码,否则这个文件夹将只能作为一个命名空间 包使用,没有模块代码。
包可以是不连续的,一个包可以由多个位于不同的位置的包拼接成,这和C++
中的namespace以及Java
中的包是相似的。
一个模块的直接名称就像变量名一样。如果这个模块来自于文件系统,那么它的名称和这个文件夹或文件的名字(不含扩展名)一样。例如示例代码中的文件夹package1对应模块的直接名称是package1,module1.py对应模块的直接名称是module1。
一个模块的完整名称是从根部的包开始,用以点号.
分隔的包的直接名称,这表示其由外向内所属的一层一层的包,最后再加上该模块的直接名称。这一点和Java
的完整类名基本上是一致的,只不过Java
的根部的包不会改变,而Python
的则会随着脚本的位置改变。
当需要在某个脚本中导入一个来自于自己编写的代码文件的模块时,这个脚本所在的目录就是根部的包。
例如在示例代码中,若以samplecode文件夹为根目录:
Python
代码文件./package1/package2/module2.py在package2文件夹内,是一个普通的模块,属于包package2。因此,在脚本./importexample1.py中,包package1的完整名称是package1
,包package2的完整名称是package1.package2
,模块module2的完整名称是package1.package2.module2
。
Python
的import语句主要有三种形式:
import 完整模块名 as 变量名
from 完整模块名 import 对象 as 变量名
from 完整模块名 import *
对于套在多层包中的模块,这些模块将被按顺序依次加载(详见importexample1),如果其中的某些模块已经被加载,则会跳过它们。例如在示例代码中要加载模块package1.package2.module1
,则会依次尝试加载包package1
、包package1.package2
和普通模块package1.package2.module1
。每一次加载都会将加载出的模块放入上一层模块的模块 命名空间中(详见importexample2)。
加载一个包或者普通模块将会执行这个模块的模块代码,以进行这些模块或包的初始化,其中,包的模块代码是这个包文件夹中的__init__.py
,普通模块的模块代码是其自身。在执行这些代码时,如果碰到了import语句(即在一个import语句执行模块代码时碰到了别的import语句),也会重复一样的过程,但是这时的import语句将以最外层的import语句所在目录作为根目录(详见importexample3中涉及到的包和模块)。因此,如果没有管理好作为入口脚本的位置以及调用自定义模块中import语句导入的自定义模块名,这些自定义模块之间的导入将可能会令人困惑地失败(例如将示例代码中的module4.py作为入口脚本将会引发异常)。此外,特殊变量__name__
的值将会是从import语句所在脚本的目录来看的完整模块名(详见importexample1),而在作为入口的脚本中,__name__
的值总是__main__
。
这里还涉及一个名称解析的问题。在默认情况下,当文件夹名和代码文件名重复时,将认为这个名字指代的是文件夹。上述的例子(importexample1)中加载package1.package2
时,虽然文件夹/package1
中含有代码文件/package1/package2.py
,但是被加载的是/package1/package2/__init__.py
。此外,脚本所在的目录优先于一些库的目录的搜索,这一点要求我们在为模块的文件命名时要格外小心。如果不小心加载了和库名称一样的自定义模块,将可能导致无法通过import语句正常导入相关的库(详见importexample4)。
接下来根据不同的语句形式,将加载好的模块 对象 赋值给变量。
如果要使用库或者调用自定义模块的内容,就应该把import语句放在执行任何其他代码之前,这是一种习惯。
Python
没有强制要求import语句的位置在首部,但是应当养成良好的习惯。
使用内置函数__import__(name, globals=None, locals=None, fromlist=(), level=0)->module
来导入,返回值是一个模块 对象。
因为使用比较复杂,详情请参考官方文档:https://docs.python.org/zh-cn/3/library/importlib.html#importlib.__import__
模块也是对象,使用运算符.进行成员访问即可得到模块中的对象。如果直接从模块导入了对象,那么可以直接使用变量。
示例代码见importexample1对f的调用。
Python
标准库的功能就十分强大,详情参考官方文档:https://docs.python.org/zh-cn/3/library/index.html
Python
的一大优势是第三方库非常丰富,而且用起来也比较优雅。有关第三方库,你需要了解一下pip。pip官网:https://pypi.org/project/pip/
语法糖是让代码更加简洁,编写更加快捷的机制。下面介绍一些本人所知的Python
语法糖。
Python
的逻辑运算符中,比较大小的表达式有一个很好用的语法糖:
你可以这样写:
a,b,c,d=1,2,3,4
if a < b < c < d:
print('a < b < c < d 成立')
实际上是解释器将其解释成了:
if a < b and b < c and c < d:
print('a < b < c < d 成立')
当然,其中的小于号可以是<
、>
、<=
、>=
中的任何一个,可以重复任意次,甚至还可以是一些很奇怪的表达式:
if 1 < 3 > 2:
print(1<3>2)
英文:Slice
Python
中的类名:slice
切片本身似乎是Python
特有的。
切片是一种很简单的对象,它其实就是由三个任意的对象组成的,分别叫做start,end,step。顾名思义,start是始,end是末,step是步长。
1个:赋值给end,其余为None
2个:赋值给start和end,step为None
3个:赋值给start、end和step\
例如表达式slice(1,9)
可以得到一个start=1,end=9,step=None
的slice对象。
slice的构造函数的赋值方式和range对象有些像,只不过一个参数时range的start是0不是None
。
对于列表及字符串这类具有序列的对象,一个切片 对象可以作为其索引使用,但是此时start、end、step必须都是整数、None
或者是实现了__index__(self):->int
的对象,并且返回的是一个子序列的拷贝(即子列表和子字符串,对这个拷贝的修改不会影响到原来的序列)。
返回的规则如下:
如果不是整数或者None
,会先调用该对象的__index__(self):->int
来决定对象所代表的整数,得到start、end、step。
而Python
切片的语法糖是运算符[]内部可以通过冒号:
来构造切片。
例如:
ls=[1, 2, 3, 4, 5, 6]
print(ls[start:end]) # 相当于ls[slice(start, end)]
print(ls[start:end:step]) # 相当于ls[slice(start, end, step)]
start,end,step的位置都可以省略,那样将被视作None
。
迭代的原义是一种重复运算的过程,往往需要利用上一次的运算结果对下一次运算做辅助。在本文中,迭代指的是有序生成一组对象,且每次只能生成一个对象的过程。
英文:Iterable Object
可以不断进行一定的迭代操作,得到一个对象的序列的对象就是可迭代对象。迭代操作可以是基于索引的,例如列表,其索引一个一个叠加上去,就可以得到不同位置的元素,产生一个对象的序列。
还可以像for语句那样,先用内置函数iter
获取对象的一个迭代器,然后不断用内置函数next
对迭代器进行迭代操作,得到每次迭代得到的对象,以生成对象的序列。能够这样操作的对象也是可迭代对象。
请注意本文中的可迭代对象和collections.abc.Iterable
的实例是不完全一样的,确切地说,本文中的可迭代对象比这个范围还要大一些。
所用的框框分别是:[]、()、{}、{}。
需要注意的是用括号()不能直接得到元组。可以通过tuple(生成器)
来得到元组。
框框内部的格式如下:
表达式1 for 变量1 in 可迭代对象1 for 变量2 in 可迭代对象2 ... if 表达式2
for 变量 in 可迭代对象
至少出现一次,多次出现相当于嵌套的for循环。
例如一个100以内奇数的列表可以这样写:
[i for i in range(100) if i%2]
例如10以内的数的平方的列表可以这样写:
[i*i for i in range(10)]
实际上这和for循环的机制差不多,只不过是将每次迭代中使得表达式2布尔值为True的序列保留,并将对应计算出的表达式1的对象保存罢了
而字典,则表达式1必须是键值对的形式。例如小九九字典可以这样写:
{f'{a}*{b}':a * b for a in range(1,10) for b in range(1,10) if a < b}
而通过推导式得到的生成器则不直接将迭代进行完,而是得到一个可以被内置函数next
不断迭代 生成器 对象。
英文:Iterator
这里迭代器的含义和collections.abc.Iterator
的含义基本一致。一个对象如果实现了__iter__(self)
和__next__(self)
这两个特殊方法,就是一个迭代器 对象。通常,__iter__(self)
返回自身。迭代器也是可迭代对象。
迭代器的好处是:它占用空间比较小。因为迭代器每次迭代 返回一个对象,其本身不一定存储了这些数据,而是通过计算得到。用迭代器来“保存”一些只用一次的较长的临时的序列是不错的选择,强行用列表、元组等存储会带来很大的内存空间和垃圾回收开销的浪费。
很多内置函数都返回一个迭代器,可以很方便地不写循环语句就完成迭代操作。
本人已知的有以下:
函数原型 | 返回 迭代器 |
---|---|
map(func:function, iterable:Iterable) | iterable经function映射后的对象 序列的迭代器 |
filter(func:function, iterable:Iterable) | iterable经function映射后布尔值为True的序列的迭代器 |
顾名思义,map就是映射器,filter就是过滤器。一些简单的迭代逻辑不必写个for循环去实现它。
英文:Generator
生成器也是一种迭代器,但是它的数据空间往往更小。一种生成器的构成方式是推导式。
另一种方式是在定义函数时,在函数体内使用yield关键字来使得函数 返回一个生成器。
一旦一个函数体内有yield关键字,当调用这个函数时,函数会返回一个生成器。这时函数体内的代码不会被执行,而是保存参数暂用。
假设将生成器 赋值给了一个变量ge,那么当调用next(ge)时,将会使得函数体开始运作,直到遇到一个yield语句。
yield 表达式
将表达式的值作为迭代的返回值。之后,将会暂停函数的执行,回到调用next的地方。
当再次调用next(ge)时,将会使得函数体的执行从刚暂停的地方继续开始,重复以上。
当函数真正返回或者产生异常时,生成器的迭代终止。如果异常是StopIteration,则应当认为迭代是正常终止(或已经终止)的。
def fibo(n):
a,b=0,1
while b <= n:
yield b
a, b=b, a + b
for i in fibo(100):
print(i)
英文:Decorator
装饰器其实就两件事情:
@decorator1
def function1(...):
...
# 差不多等价于
def function1(...):
...
# def也可以是class
function1=decorator1(funcnion1)
此外,如果有多个被@装饰的情况,结合顺序是由下至上。即:
@a
@b
def function(...):
...
# 差不多等价于
def function(...):
...
function=a(b(function))
装饰器的作用是:使得函数或者类被装饰,可能是为它附加一些操作。下面的例子可以让你更好地理解装饰器的执行过程:
def message(func):
print(f'{func}的装饰器初始化')
def w(*args, **kw):
positional=','.join(str(i) for i in args)
keyword=','.join(f'{k}={v}' for k, v in kw.items())
argls=f"{positional}{',' if positional != '' else ''}{keyword}"
print(f'调用函数 {func.__name__}({argls})')
rs=func(*args, **kw)
print(f'调用函数 {func.__name__}({argls}) 结束')
return rs
return w
class A:
def __init__(self,data):
print(f'对象{self}初始化')
self.data=data
@message
def echo(self):
print(f'复述{self.data}')
a=A('你好')
a.echo()
英文:Class
类是对一些具有相同特征的对象的共性的抽象。
例如“显示屏”是类,“你正在看的显示屏”则是一个“显示屏”的实例。
例如“狗”是类,“我家楼下的小黄狗”则是一个“狗”的实例。
本文曾经提到过,在Python
中,类也是对象。这些对象被称作类对象。
当一个Python
类通过class
关键字被定义后,其类名就成为了这个类的类对象。
类对象利用自身的__init__
方法(即使没有显式重写__init__
方法,也会因默认继承了object
而拥有这个方法)默认实现了其__call__
方法,这使得类对象同时也是一个可以被当作函数来调用的构造函数
。构造函数
被调用后,返回一个该类的实例。
英文:Data/Attribute
别名:属性
英文:Method
英文:Interface
本文的接口指的是一套功能或者交互的标准。
接口只是声明:实现这个接口的对象就意味着有哪些方法。
接口本身不应该有相应的实现。
英文:Subclass
子类是两个类之间的关系。
如果类A继承了类B,那么类A是类B的子类。
英文:Superclass
父类是两个类之间的关系。
如果类A继承了类B,那么类B是类A的父类。
英文:Instance
实例是一个类的具体的对象。
有时候,我们可以将实例看成是和对象一样的含义(但不建议这样做)。
英文:Override
别名:覆写
英文:Expression
表达式可以与值对象或表达式通过运算符结合成新的表达式。这个过程和数学上的复合运算类似。
运算符的结合存在优先级。优先级详见其他文档的介绍:https://www.runoob.com/python/python-operators.html#ysf8
该网站文中遗漏的运算符.、运算符[]、运算符()优先级高于文中列举出的全部。
在此对表达式简单举例:
1 # 常量值对象
1 + 2 # 前文与运算符+以及2结合成的表达式
1 + 2 + 3 # 前文表达式“1 + 2”与运算符+结合成的表达式
"1,2,3" # 常量值对象,类型为str(字符串)
"1,2,3".split # 前文与运算符.以及名称“split”结合成的表达式,类型为函数
"1,2,3".split(",") # 前文与运算符()以及常量值对象","结合成的表达式,类型为list(列表)
"1,2,3".split(",")[-1] # 前文与运算符[]以及常量值对象-1结合成的表达式,类型为int(整数)
print("1,2,3".split(",")[-1]) # print函数对象与运算符()以及前文结合成的表达式,类型为None
从上例可见,表达式的链可以无限变长。如果使用推导式和生成器,你甚至可以把带有循环结果的复杂代码压缩成一个表达式。
例如:
[(print(i), print(i*i),print(i*i*i)) for i in range(10)]
这是一个表达式,最外层是列表推导式,但是却完成了下面代码的逻辑:
for i in range(10):
print(i)
print(i*i)
print(i*i*i)
不建议过度使用过长的表达式压缩代码。这只是装逼神器。
英文:Exception
异常由程序中的一些不正当的操作引起。如果处理不当,将可能使得程序被迫错误地终止。
英文:Key
本文中的键指的是字典中被用于映射的一方。映射到的一方叫做值。如果将字典比作函数,那么键就是自变量,值就是函数值。一个键只能映射到一个值,而一个值可以被多个键映射到。
英文:Immutable Object
顾名思义,不可变对象就是不可以被改变内容的对象,他们一旦被创建完成,就永远是不变的。
英文:Mutable Object
顾名思义,可变对象就是可以改变内容的对象。由于可变性的存在,它们一般是不可哈希对象。
英文:Hash
哈希其实是一类算法,作用是将一些对象映射到一个整数,这个整数的值叫做哈希值。不过这些整数可能会重复,即产生哈希冲突。一个良好的哈希算法产生的哈希冲突较少。
英文:Hashable Object
英文:Unhashable Object
英文:Index
如果我们把可用索引查找的容器对象比作书,那么索引就像页码一样。有了索引,我们可以快速找到容器里的对象,就像有了页码,我们能够快速地翻到某个特定的位置。
英文:Lvalue
英文:Rvalue
单独的右值也可以作为一个合法的语句。
英文:Indentation
缩进是Python
中表示结构的方法。一个具有缩进的结构被视作为上一个缩进比它少的结构的子结构。一般而言,一个缩进是4个空格。下面的示例可能对理解这个有一定的帮助。
无缩进的行1
一个缩进的行2
一个缩进的行3
两个缩进的行4
两个缩进的行5
两个缩进的行6
一个缩进的行7
无缩进的行8
一个缩进的行9
无缩进的行10
英文:Block
顾名思义,代码块就是代码的块。一般而言,在Python
中具有相同的最小缩进个数的连续(忽略空行)的代码行属于同一个代码块,这个最小的缩进的个数是这个代码块的缩进个数。一般我们讲代码块都是指行尽可能地多的代码块。所有行都在一个代码块A内,且缩进个数更多的代码块B是代码块A的子代码块。
例如上面的示例中,行1-10属于同一个代码块,缩进个数为0。行2-7属于同一个代码块,缩进个数是1,且是行1-10的代码块的子代码块。行4-6属于同一个代码块,缩进个数为2,且是行2-7的代码块的子代码块。行9是一个代码块,是行1-10的代码块的子代码块。但是,行2-7和行9不是同一个代码块,因为不连续。如果你学过C/C++
或者Java
,那么它的结构看起来应该是这样(同一对花括号{}
内是同一个代码块):
{
无缩进的行1
{
一个缩进的行2
一个缩进的行3
{
两个缩进的行4
两个缩进的行5
两个缩进的行6
}
一个缩进的行7
}
无缩进的行8
{
一个缩进的行9
}
无缩进的行10
}
英文:Sequence
序列是按照一定顺序排列的某些东西。例如高考分数的排名名单、节目单的出场顺序、星期一的课程表/作息表,都是具体的序列。
英文:Annotation
别名:注解
标注仅用于函数的参数列表和返回值声明,以及对左值的声明。对函数的标注的主要作用是表示函数的参数和返回值的类型,对左值的标注则表示对右值的类型要求。你可以通过读取函数 对象的__annotations__
属性获取函数对象的所有标注(是一个字典),这个字典的键是函数的参数名,以及一个特殊的return
,值就是这个参数(return
对应返回值)的标注。
本文中多处使用了标注来表示库 函数的参数的合法类型以及返回值的类型。
标注的语法比较简单。你可以为你自定义函数的任何参数(或返回值)添加标注,并且这些标注都是可选的,在绝大多数(99%以上)的Python
教程中,标注都是被忽略的(因为写起来简单)。下面通过一个简单的例子来说明语法:
a:int = 1
ls:list = [1, 2, 3]
ls[0]:int = 7
ls[1]:int = "a" # 运行不会报错,但代码令人费解
def func(p1: int, p2: 1 + 2 = 9, *args:"just a string", **kw: {"key":"value"})->list:
pass
注意:此处的p2: 1 + 2 = 9
同时包含了对p2进行的标注(表达式1 + 2
)和p2的默认值(= 9
)。
可以注意到,左值和参数的标注用冒号:
分隔,返回值的标注用->
分隔,并且写在被标注的左值/参数/返回值的后面。
标注的内容可以是任何合法的表达式。
此处的合法是指当前上下文中可以正常计算表达式的值,不会因为含有无法解析的变量名或者任何错误的语法而无法计算。在
Python
中,未定义的变量名是合法(即符合语法)的表达式,但运行时产生异常。
此时,如果计算func.__annotations__
,结果将会是:
{'p1': <class 'int'>, 'p2': 3, 'args': 'just a string', 'kw': {'key': 'value'}, 'return': <class 'list'>}
标注如果为需要计算的表达式(如例子中参数p2的标注),这个表达式将仅在函数被声明时计算一次。
如果程序没有通过任何反射方法(有关反射的概念,请自行查阅资料了解)去读取函数的标注,那么这些标注对程序的逻辑影响仅限于声明时对这些表达式的计算。
如果你使用Visual Studio
或者Visual Studio Code
作为Python
的编辑器,为左值或自定义函数添加合适的标注将获得相应的代码提示
(即编辑器能够借助标注作出类型推断
)。借助标注,我们可以在Python
这样的脚本语言中使用安全、准确的类型推断
与代码提示
,还能提高代码的可读性,何乐而不为?
如果想要利用标注一些复杂类型来获得相应的代码提示
,例如一个将字符串映射到学生类(一个自定义类),可以使用库typing
内的类型以及对象。
利用标注在Visual Studio Code
中获得代码提示的简单举例如下:
from typing import *
class Student:
def __init__(self,name:str,number:int): # 此标注可以使得self.name和self.number类型得到推断
super().__init__()
self.name = name
self.number = number
def sayHello(self)->None:
print(f"{self.name} says hello")
def get_number(self)->int: # 此标注可使得后文的get_number调用返回值被推断
return self.number
students:Dict[str,Student] = dict() # 此标注使得后面的students["Adam"]能够被推断
adam = Student("Adam",1)
students["Adam"] = adam
adam.name = "Adam Smith" # 属性类型推断已生效
students["Adam"].sayHello() # 索引类型推断已生效,students["Adam"]被推断为Student类型
num = adam.get_number() # 函数调用类型推断已生效,num被推断为int类型
请注意标注并不能影响函数调用以及赋值操作。即使函数调用或赋值时实际传入的对象不符合标注声明的类型(例如前例的ls[1]应当是int类型)或者值(例如前例的p2应当被传入值3),
Python
解释器也不会为此做出任何类型/值检查。
此外,请不要尝试在标注中使用过于复杂的表达式,例如推导式等,尽管Python
并没有禁止你这样做,但这会对代码的可读性造成一定的麻烦。