今天端午节,为了老婆需要,抽时间整理一下Python的基础语法和一些使用技巧,以及和其他语言的一些差别,方便快速的了解和使用Python;对于使用过C++,Python,Go,Lua的人,相信可以更好的去解释Python的一些设计思想和基础关键点;
本文是基于2014年10月底整理的「Python核心编程」的相关笔记进行重新梳理,由于Python官方已于2020停止了对2.7版本的支持,所以本文的梳理是针对Python 3.X版本;本文主要参考官方Python 3.10.4 手册,教程和之前整理的笔记,下面书归正传。
0x1. What is Python?
Overview
官方Overview对于Python的解释:Python是一个简洁且功能强大的面向对象的语言,相对于Perl,Ruby,Scheme,Java;是不是感觉啥也没说,下面简单的概况一下Python的特点:
Python是一门解释性的脚本语言,他和Java一样,也可以称之为字节编译性语言,因为他们在Running前都会被解析器编译成中间形式的字节码,在对应的虚拟机上执行;这和C/C++,Go等直接编译成机器字节码的编译性语言是有运行速度差异的;
Python是一门强类型和动态类型的语言;
强类型一般定义:不允许错误类型的参数参与计算;借由类型推导,很多语言显示出了弱类型特性,但是同时保留了强类型语言的类型检查和保护;例如C/C++,Java;弱类型的语言比较典型的是VB,JS;
动态类型:变量名和类型不用事先绑定,在运行期进行类型检查;这个带来的结果是效率低;
这里要说明的是:对于计算机语言的各个类型的定义,并没有统一的标准,编程语言专家 Benjamin C. Pierce对此的看法是近乎无用;但是对于我们理解一门语言,还是有一定的帮助的;
Python是一门含有GC的语言,基于引用计数和环检测相结合的自动内存管理;
Python是一种胶水语言,官方解析器CPython是有C语言编写的,所以Python很容易通过C/C++进行新功能的添加,Python可以很容易衔接各个组件来完成任务;
Python 2.x和3.x
根据维基的历史介绍,吉多·范罗苏姆在1989年圣诞期间,为了打发时间,决定开发一种脚本语言,借鉴ABC语言的特性,以替代Shell和C来对Unix系统的进行管理;Python名字由来:作者喜欢的BBC电视节目蒙提·派森的飞行马戏团之名命名之。
1991年2月,范罗苏姆发布了最初代码,版本号标记为:「0.9.0」;1994年1月发布了「1.0」版本;2000年10月16号发布了Python「2.0」,2008年12月3日发布了Python「3.0」;
这里为什么要说2.x和3.x版本呢,这是因为Python这个奇葩在语言的设计上为了解决设计之初的缺陷和复杂度,践行了「欲练此功,必先自宫」自己捅了自己一刀,在2008年发布的Python「3.0」,对语言做了较大修订而不能完全后向兼容,导致2.x和3.x两个不兼容的版本在后来的十多年中同时存在,也让其维护成本和使用成本增加;
Python「2.x」和「3.x」的差异可以参考3.0的发布说明;后面介绍相关基础和语法的时候会进行说明;
0x2. How
0x2.1. 解析器
Python的解析器作为Python语言执行的载体,可以很方便的进行安装;需要说明的是:为了兼容Python2,Python3安装后默认的命令是python3
,所以在终端直接输入python3
就可以启动解析器;当然为了方便都会将python
重新符号链接到python3
;
解析器的使用方式类似Unix Shell,主要有两种方式:
- 交互模式:以交互式的方式读取输入的指令,执行,输出结果;
- 文件模式:以文件名为参数,读取并执行文件中的脚本命令;
当然Python解析器还支持python -c command [arg] ...
和python -m module [arg] ...
来执行语句和模块;
如下是通过交互模式,进入Python解析器输出”Hello World”:
1 | % python3 |
0x2.2. 标识符
Python2的标识符只能是:A-Z,a-z,0-9,_,且不能以数字开头,大小写敏感,在此规则基础上Python3的标识符引入了更多的可用的Unicode字符:Lu, Ll, Lt, Lm, Lo, Nl等码段,具体可以参考官方手册;如下:
1 | 1 walker= |
Python3引入更多Unicode字符的做法,也是很多语言都支持的更广泛的标识符命名,例如C, C++, Java, Go;按照rosettacode的列表,有79种语言支持Unicode字符的标识符命令,不过里面没有C++,所以里面的数据应该是缺失的;
0x2.3. 关键字
Python3中的以下标识符作为关键字,不可用于普通的标识符命名,如下:
1 | False await else import pass |
这里需要说明的是,Python最新的「3.10」版本引入了软关键字的概念,就是为了保持兼容,又希望引入新的关键字来支持新的语法,提出了这个概念;例如Python「3.10」版本引入了结构模式匹配的功能(类似我们常见的switch case语法,但是更复杂),为此引入了关键字match, case, _
;这几个关键字在模式匹配语句中作为关键字保留,但是在其他地方仍然作为一个标识符使用;
0x2.4. 代码结构
Python中需要注意的是代码结构:
- 行缩进
Python中代码的层级结构是通过缩进来管理的,而不是像其他语言,通过{}
包裹或者关键字if then end
等包裹来组织代码结构;需要注意的是:缩进可以使用制表符和空格符,但是不要混用,因为不同平台不同编辑器默认的制表符长度不一,可能会触发缩进异常导致层次出现错误,触发TabError的异常;Python解析器在解析源码是遇到制表符会被替换为1到8个空格,保证行首的空格总数是8的倍数;
- 行结尾
Python的代码行结尾不需要什么分隔符;例如C/C++每行结尾需要;
来进行结尾;Python不需要,但是写了也可以,不会有语法错误,因为;
在Python中可以用来在一行中书写多个表达式,用来进行表达式分割,允许其后面不接其他表达式;
至于换行符,和其他大多数语言一样,可使用任意标准平台行终止序列 - Unix ASCII 字符 LF (换行)、 Windows ASCII 字符序列 CR LF (回车换行)、或老式 Macintosh ASCII 字符 CR (回车)。不管在哪个平台,这些形式均可等价使用。
- 注释:单行注释:符号
#
;多行注释:可以通过三引号字符串的方式来进行多行注释;
0x2.5. 基础数据类型
Python3支持的基础数据类型中,数值类型包含三种:整数,浮点数和虚数,如下:
- 整数(int)
1 | integer ::= decinteger | bininteger | octinteger | hexinteger |
整数的字面值定义需要符合以上语法规范,这里有几点说明:
- 整数字面值的长度没有限制,能一直大到占满可用内存。
- 数值之间可以用下划线’_’来进行分割,目的是为了进行分组数字,更易读,是不是太贴心了,赞!。
- 十进制非0数字不能以0开头;非0开头的数字用来标识其他进制的数据,如上语法格式;
1 | 100_0000_0000 |
- 浮点数(float):浮点数的精度问题可以参考官方文档解释;浮点数的简单使用如下:
1 | 77e2 |
- 虚数(complex):忽略之;
Python3支持的基础数据类型中,字符串的相关语法格式如下:
- 字符串(str)
1 | stringliteral ::= [stringprefix](shortstring | longstring) |
字符串的字面值的语法格式如上,其中主要说明的是:
- 字符串可以用三种引号包裹:单引号
'str'
,双引号"str"
,三引号'''str'''
和"""str"""
;支持那么多种干啥呀!前面两种方式是等价的,用于短字符串,不能跨行,而三引号的方式包裹的字符串,用于定义长字符串,可以跨多行,其中的换行等格式都会原样保留;这三种方式定义的字符串,如果里面含有转义的字符,都会被识别出来,进行转义;如下: - 字符串前面前缀
r
或R
,称之为原始字符串;即字符串中的所有转义符号都会进行保留,不会进行任何转义; - 字符串前面前缀
u
或U
,称之为Unicode字符串;Python3默认源码文件就是UTF-8编码,所以此前缀是默认不需要的,但是为了简化 Python 2.x 和 3.x 并行代码库的维护工作,在Python「3.3」中又加回了该前缀; - 字符串前面前缀
f
或F
,称之为格式字符串;详见官方语法说明;
如下是上述字符串说明的简单测试:
1 | print('line1\nline2') |
需要注意的一点,对于内置的基础数据类型:
整数,浮点数,字符串都是具有固定值的对象,是不可以修改的;如果对这几种值绑定的名字进行修改,会创建一个新的对象,原有对象是没有发生任何改变的;
如果你理解Python中一切皆是对象,这点就很容易理解;这里首先说明很重要的一点:Python的参数传递都是引用传递而不是值传递;
0x2.6. 内置数据结构
Python支持了相对丰富的内置数据结构,这里主要介绍三类:
- sequence序列类型:可以使用整数索引高效的进行元素访问;同时还支持切片
:
操作来获取序列的子序列;包括的数据结构有:list,str,tuple,bytes,bytearray;注意:序列类型的数据结构下标都是从0开始的; - mapping映射类型:可以使用任意对象通过hashtable值映射到任意对象来进行元素访问;目前只包括:dict;
- set集合类型:目前有两种内置集合类型:
set
和frozenset
;
0x2.6.1. sequence序列类型
序列类型本身根据是否可修改,又分为两类:
- 可变序列:list,bytearray;
- 不可变序列:str,tuple,bytes;
列表(list)
列表,顾名思义和我们大多数语言的数组类似,用来组合一串值的可变数组,Python的列表很重要的一点:可以支持不同类型的元素;列表的创建可以通过如下三种方式:
- 使用
[]
或者[A,B,C,...]
来创建列表,列表中的元素用逗号分隔; - 用列表生成器
[x for x in iterable]
; - 用list类的构造器
list()
或者list(iterable)
来构造;
1 | 1,2,3,4,5] # 直接通过[]来创建列表 a=[ |
列表和其他sequence序列类型一样,都可以通过索引和切片操作来访问其中的元素,序列类型的数据结构下标都是从0开始的;
切片操作的语法:[开始下标索引 : 结束下标索引 [:步长]]
,其中不包含结束下标,步长可以省略,默认为1;这里要知道:切片操作生成的是一个浅拷贝序列,如果原序列的元素都是基础数据类型,那么生成的新序列和原序列没有任何关系,否则是有关联的;如下:
1 | 0] # 访问列表第一个元素 a[ |
列表是可变的序列,列表的拷贝其实是引用传递,并不会生成一个新的列表;如下:
1 | # 生成一份引用,并没有发生拷贝 b=a |
对于list
列表的数据类型,支持的方法可以执行help进行查看;
在这里首先简单的介绍一下sequence序列数据类型的通用操作,如下表,引用官方文档:
运算 | 结果: | 备注 |
---|---|---|
x in s |
如果 s 中的某项等于 x 则结果为 True ,否则为 False |
对于str类型,可以用来校验子串是否存在 |
x not in s |
如果 s 中的某项等于 x 则结果为 False ,否则为 True |
对于str类型,可以用来校验子串是否不在str中 |
s + t |
s 与 t 相拼接 | 拼接不可变序列总是会生成新的对象 |
s * n 或 n * s |
相当于 s 与自身进行 n 次拼接 | 小于0的n值会被当作0来处理,生成一个与s同类型的空序列 |
s[i] |
s 的第 i 项,起始为 0 | 如果 i 或 j 为负值,则索引顺序是相对于序列 s 的末尾: 索引号会被替换为 len(s) + i 或 len(s) + j 。 但要注意 -0 仍然为 0 |
s[i:j] |
s 从 i 到 j 的切片 | 所有满足 i <= k < j 的索引号 k 的项组成的序列。 如果 i 或 j 大于 len(s) ,则使用 len(s) 。 如果 i 被省略或为 None ,则使用 0 。 如果 j 被省略或为 None ,则使用 len(s) 。 如果 i 大于等于 j,则切片为空。 |
s[i:j:k] |
s 从 i 到 j 步长为 k 的切片 | k 不可为零。 如果 k 为 None ,则当作 1 处理。 |
len(s) |
s 的长度 | |
min(s) |
s 的最小项 | |
max(s) |
s 的最大项 | |
s.index(x[, i[, j]]) |
x 在 s 中首次出现项的索引号(索引号在 i 或其后且在 j 之前) | 当 x 在 s 中找不到时 index 会引发 ValueError |
s.count(x) |
x 在 s 中出现的总次数 |
上面的序列通用操作对可变序列和不可变序列都是通用的;那么针对可变序列支持的通用操作,如下表,引用自官方文档:
运算 | 结果: | 备注 |
---|---|---|
s[i] = x |
将 s 的第 i 项替换为 x | |
s[i:j] = t |
将 s 从 i 到 j 的切片替换为可迭代对象 t 的内容 | |
del s[i:j] |
等同于 s[i:j] = [] |
|
s[i:j:k] = t |
将 s[i:j:k] 的元素替换为 t 的元素 |
当k不等于1的时候,t必须和被替换的切片长度保持一致; |
del s[i:j:k] |
从列表中移除 s[i:j:k] 的元素 |
|
s.append(x) |
将 x 添加到序列的末尾 (等同于 s[len(s):len(s)] = [x] ) |
|
s.clear() |
从 s 中移除所有项 (等同于 del s[:] ) |
为了与不可用切片操作的可变容器的接口保持一致;例如dict 和set |
s.copy() |
创建 s 的浅拷贝 (等同于 s[:] ) |
为了与不可用切片操作的可变容器的接口保持一致;例如dict 和set |
s.extend(t) 或 s += t |
用 t 的内容扩展 s (基本上等同于 s[len(s):len(s)] = t ) |
|
s *= n |
使用 s 的内容重复 n 次来对其进行更新 | n 值为一个整数,或是一个实现了 __index__() 的对象。 n 值为零或负数将清空序列。 序列中的项不会被拷贝;它们会被多次引用 |
s.insert(i, x) |
在由 i 给出的索引位置将 x 插入 s (等同于 s[i:i] = [x] ) |
|
s.pop() 或 s.pop(i) |
提取在 i 位置上的项,并将其从 s 中移除 | 可选参数 i 默认为 -1 ,因此在默认情况下会移除并返回最后一项。 |
s.remove(x) |
删除 s 中第一个 s[i] 等于 x 的项目。 |
当在 s 中找不到 x 时 remove() 操作会引发 ValueError 。 |
s.reverse() |
就地将列表中的元素逆序。 |
元组(tuple)
元组与列表很像,但使用场景不同,用途也不同。元组是 immutable (不可变的),一般用来包含异质元素序列。列表是 mutable (可变的),列表元素一般为同质类型,可迭代访问。当然tuple和list对存入元素的类型是没有限制的,只是习惯用法上的有些差异;
元组的初始化有两种方式:
- 通过由逗号组成的值构建;最外层的圆括号可以忽略;
- 通过类构造器
tuple()
或者tuple(iterable)
进行构建;
如下:
1 | 1,2,3,4,5,6] a=[ |
上一节的列表中列出的序列类型的通用操作在元组都是可以正常使用,这里不再赘述了;
字符串(str)
前面在基础数据类型里,已经介绍过字符串一些基本知识,但是Python对于字符串的归类其实是属于sequence序列数据类型;和C++的string容器一样;其实Python对所有类型在实现上都是封装成了一个对象;既然Python的str
是序列数据类型,那么它同样支持list
一节描述的对于sequence序列数据类型的通用操作;
此外,由于str
的特殊性,str
还额外支持很多字符串相关的操作,具体可以在解析器中help(str)
进行查看,这里罗列一些比较常见的方法:
1 | # 前缀和后缀子串判断 |
0x2.6.2. mapping映射类型(dict)
Python也提供了我们常用的数据结构,哈希表,Python中称之为dict
也叫字典;目前Python中只支持此一种映射类型的数据结构;Python中的dict
的key只能是不可变类型的数据,包括:数字,字符串,元组(元素也只能是不可变的数字,字符串和元组),集合(frozenset);
我们知道dict
是键值对的集合,且key是唯一的,Python中的dict
的创建有两种方式:
- 通过花括号
{}
来创建一个字典,当然{keyA:valueA, keyB:valueB}
可以用逗号分隔,来初始化字典里面的键值对元素; - 使用字典推导式:
{x: x ** 2 for x in range(10)}
- 使用类型构造器:
dict()
,dict([('foo', 100), ('bar', 200)])
,dict(foo=100, bar=200)
;
如下是测试这几种创建dict
的方法:
1 | dict(one=1, two=2, three=3) a = |
字典数据结构支持的基本增删改查操作如下:
1 | 'one'] # 访问key为'one'的值 a[ |
如下是dict
支持的基本操作,详细的可参考help(dict)
说明,也可以参考「Python标准库」,这里不过多罗列:
list(d)
:返回字典 d 中使用的所有键的列表;len(d)
:返回字典 d 中的键值对数;d[keu]
:返回value,如果映射中不存在 key 则会引发KeyError
。如果字典的子类定义了方法__missing__()
并且 key 不存在,则d[key]
操作将调用该方法并附带键 key 作为参数;get(key[, default])
:如果 key 存在于字典中则返回 key 的值,否则返回 default。 如果 default 未给出则默认为None
,因而此方法绝不会引发KeyError
。pop(key[, default])
:如果 key 存在于字典中则将其移除并返回其值,否则返回 default。 如果 default 未给出且 key 不存在于字典中,则会引发KeyError
。key in d
:如果 d 中存在键 key 则返回True
,否则返回False
;key not in d
:如果 d 中不存在键 key 则返回True
,否则返回False
;clear()
:移除字典中的所有元素;copy()
:返回原字典的浅拷贝;iter(b)
:返回以字典的键为元素的迭代器;keys()
:返回由字典键组成的一个新视图。 关于字典视图对象可以参考官方文档;所生成的视图一个动态视图,这意味着当字典改变时,视图也会相应改变。这和Python2里面有很大差异,Python2中返回的是一个由key组成的列表,可以看出,Python3对于性能的还是比较在意的;values()
:返回由字典值组成的一个新视图。items()
:返回由字典项 ((键, 值)
对) 组成的一个新视图。popitems()
:从字典中移除并返回一个(键, 值)
对。 键值对会按 LIFO 的顺序被返回;在「3.7」之前的版本中,popitem()
会返回一个任意的键/值对。d|other
:「3.7」新增功能,合并 d 和 other 中的键和值来创建一个新的字典,两者必须都是字典。当 d 和 other 有相同键时, other 的值优先;d|=other
:「3.7」新增功能,用 other 的键和值更新字典 d ,other 可以是 mapping 或 iterable 的键值对。当 d 和 other 有相同键时, other 的值优先。
0x2.6.3. 集合类型(set)
集合是数学上的一个概念,Python中集合是由不重复元素组成的无序容器。基本用法包括成员检测、消除重复元素。集合对象支持合集、交集、差集、对称差分等数学运算。Python的集合类型支持两种数据结构:
set
:可变集合,其内容可以使用add()
和remove()
这样的方法来改变;frozenset
:不可变集合,由于它创建后不可变,因此它可以被用作字典的键或其他集合的元素;
集合的创建方式包含一下三种:
- 使用花括号内以逗号分隔元素的方式:
{'jack', 'sjoerd'}
; - 使用集合推导式:
{c for c in 'abracadabra' if c not in 'abc'}
- 使用类型构造器:
set()
,set('foobar')
,set(['a', 'b', 'foo'])
,frozenset(['a', 'b', 'foo'])
,frozenset
只能通过构造器的方式来创建;
对于集合类型set()
和 frozenset()
支持的常用操作如下,也可以参考「Python标准库」;
len(s)
:返回集合 s 中的元素数量;x in s
:检测 x 是否为 s 中的成员。x not in s
:检测 x 是否不是 s 中的成员。isdisjoint(other)
:如果集合中没有与 other 共有的元素则返回True
。 当且仅当两个集合的交集为空集合时,两者为不相交集合。issubset(other)
或set <= other
:检测是否集合中的每个元素都在 other 之中。issuperset(other)
或set >= other
:检测是否 other 中的每个元素都在集合之中。copy()
:返回原集合的浅拷贝;
0x2.7. 运算符和表达式
0x2.7.1. 运算符
Python中的运算符沿用了C的符号,以下是Python中支持的运算符列表:
1 | + - * ** / // % @ |
其中有一部分运算符被Python归类为分隔符,如下,下面分隔符后半部分是增强赋值操作符;
1 | ( ) [ ] { } |
参考Python教程,下表对 Python 中运算符的优先顺序进行了总结,从最高优先级(最先绑定)到最低优先级(最后绑定)。 相同单元格内的运算符具有相同优先级。 除非句法显式地给出,否则运算符均指二元运算。 相同单元格内的运算符从左至右分组(除了幂运算是从右至左分组);
运算符 | 描述 |
---|---|
(expressions...) ,[expressions...] , {key: value...} , {expressions...} |
绑定或加圆括号的表达式,列表显示,字典显示,集合显示 |
x[index] , x[index:index] , x(arguments...) , x.attribute |
抽取,切片,调用,属性引用 |
await x |
await 表达式 |
** |
乘方 |
+x , -x , ~x |
正,负,按位非 NOT |
* , @ , / , // , % |
乘,矩阵乘,除,整除,取余 |
+ , - |
加和减 |
<< , >> |
移位 |
& |
按位与 AND |
^ |
按位异或 XOR |
` | ` |
in , not in , is , is not , < , <= , > , >= , != , == |
比较运算,包括成员检测和标识号检测 |
not x |
布尔逻辑非 NOT |
and |
布尔逻辑与 AND |
or |
布尔逻辑或 OR |
if – else |
条件表达式 |
lambda |
lambda 表达式 |
:= |
赋值表达式,Pyhon「3.8」新增 |
关于运算符有几个以下符号的简要说明:
%
符号也被用于字符串格式化;在此场合下会使用和取余同样的优先级;
1 | "%s_%s" % ("Hello", "World") + "~" |
is
和is not
运算符用于检测对象的标识号:当且仅当 x 和 y 是同一对象时x is y
为真。 一个对象的标识号可使用id()
函数来确定。x is not y
会产生相反的逻辑值。:=
:赋值表达式,是Pyhon「3.8」新增的功能,后面表达式部分会介绍;
0x2.7.2. 算术表达式
Python的算术运算,不同的内置数据类型不能进行算术运算,特殊指出,以下两种情况会发生类型的自动转换:
- 如果任一参数为复数,另一参数会被转换为复数;
- 如果任一参数为浮点数,另一参数会被转换为浮点数;
其中数据类型的算法运算参考项目数据类型的说明,这里不在赘述;
0x2.7.3. 逻辑表达式
Python的逻辑运算的表达式格式如下:
1 | or_test ::= and_test | or_test "or" and_test |
在执行逻辑运算的时候,哪些值为被认为是真,哪些值会被认为是假呢?按照标准语法介绍:值被判定为假的情况:False,None,所有类型的数字0,空字符串,空容器(包括字符串、元组、列表、字典、集合与冻结集合)时;其他情况的值皆被认为是真;注意,这和C/C++中逻辑表达式返回结果是布尔值不同,Python的逻辑表达式返回的不一定是布尔值;
逻辑运算的判断如下:
not x
:求x求值,如果为假,则返回True
,否则返回False
;x and y
:对x求值,如果判定为真,则返回y的求值结果,否则返回x;x or y
:对x求值,如果判定为真,则返回x,否则返回y的求值结果;
如下测试:
1 | not "walkerdu" |
0x2.7.4. 赋值表达式
在Python「3.8」之前,并没有赋值表达式,通过=
赋值运算符来表示的是一个赋值语句,他只能单独一行,不能和其他表达式混用;
1 | 1 + (a=2) |
Python「3.8」引入了赋值表达式,即通过海象运算符:=
组成的表达式,允许在表达式中使用赋值表达式来组织语句,如下:
1 | 1 + (a:=2) |
赋值表达式可以在需要的时候让代码更加的简洁;如下,更多如果简洁的书写代码,可以参考赋值表达式的提案:PEP-0572;
1 | diff = x - x_base |
0x2.7.5. 条件表达式
条件表达式,常称为“三元运算符”,在所有 Python 运算中具有最低的优先级。条件表达式语法如下:
1 | conditional_expression ::= or_test ["if" or_test "else" expression] |
表达式 x if C else y
首先是对条件 C 而非 x 求值。 如果 C 为真,x 将被求值并返回其值;否则将对 y 求值并返回其值。如下测试:
1 | "hello" if 2 > 1 else 'no hello' |
0x2.7.6. lambda表达式
lambda 表达式被用于创建匿名函数。 语法格式如下:
1 | lambda_expr ::= "lambda" [parameter_list] ":" expression |
表达式 lambda parameters: expression
会产生一个函数对象 。 该未命名对象的行为类似于用以下方式定义的函数,所以lambda表达式的参数列表并不需要像函数定义一样,将参数括起来,显得更加简洁;
1 | def <lambda>(parameters): |
请参阅 函数定义 了解有关参数列表的句法。 请注意通过 lambda 表达式创建的函数不能包含语句或标注。如下测试:
1 | lambda lv, rv : lv + rv add = |
0x2.8. 简单语句
0x2.9. 复合语句
Python中复合语句就是包含其他语句的语句,即所谓的语句块;一条复合语句由一个或多个子句组成,每个子句包含一个句头和句体;
- 子句头:以一个作为唯一标识的关键字开始并以一个冒号结束;
- 子句体:由一个子句控制的一组语句;
子句体和子句头的结构关系:
- 可以紧接着在子句头之后,同在一行,一行中的多条简单语句用分号分隔;
- 当然最常见的就是子句体在子句头后新缩进一行或多行,这种形式才可以嵌套其他复合语句;
如下简单的复合语句的测试:
1 | if 2 > 1 : print("Hello World") |
0x2.9.1. if语句
Python中if条件语句的语法格式如下:
1 | if_stmt ::= "if" assignment_expression ":" suite |
它通过对表达式求值,直到找到一个真值。计算结果的值哪些被认为是真,哪些值会被认为是假,和逻辑表达式的判定结果是一致的,这里再重述一遍,按照标准语法介绍:值被判定为假的情况:False,None,所有类型的数字0,空字符串,空容器(包括字符串、元组、列表、字典、集合与冻结集合)时;其他情况的值皆被认为是真;
如下简单if语句测试:
1 | import time |
0x2.9.2 while语句
Python中while循环语句的语法格式如下:
1 | while_stmt ::= "while" assignment_expression ":" suite |
表达式的值真假的判定,和逻辑表达式的判定结果是一致的,这里不再赘述;这里有一个可选的else语法,就是当while条件判断为假时,执行else子句;在while语句体内执行break退出循环是不会执行else语句的;如下测试语句:
1 | 1 a = |
0x2.9.3. for语句
Python中的for循环语句语法格式如下:
1 | for_stmt ::= "for" target_list "in" expression_list ":" suite |
for语句用于对序列(例如字符串、元组或列表)或其他可迭代对象中的元素进行迭代;
表达式列表会被求值一次;它应该产生一个可迭代对象。 系统将为 expression_list
的结果创建一个迭代器,然后将为迭代器所提供的每一项执行一次子句体,具体次序与迭代器的返回顺序一致。 当所有项被耗尽时 (这会在序列为空或迭代器引发 StopIteration
异常时立刻发生),else
子句的子句体如果存在将会被执行,并终止循环。和while语句一样,如果循环体内由break触发退出,不会执行else语句;如下简单测试:
1 | for idx in range(0,10): |
0x2.A. 函数
OK,前面讲了那么多基础知识,那么现在进入Python函数,函数作为功能封装的基本单元,是程序设计语言的核心,下面进入正题;
下面是函数定义的简单版语法格式,删除了关于parameter_list
的说明,后面会详细说明参数列表的情况;
1 | funcdef ::= [decorators] "def" funcname "(" [parameter_list] ")" ["->" expression] ":" suite |
decorators
是Python中函数的装饰器,一个函数定义可以被一个或多个 decorator 表达式所包装。当函数被调用时对装饰器表达式求值。 求值的过程是以该函数对象作为唯一参数调用表达式,求值结果必须是一个可调用对象,一般是返回修饰的函数对象。 多个装饰器会以嵌套方式被应用,后面会详细单独介绍装饰器;
如下是一个简单函数测试:
1 | def test(a, b): |
Python的函数定义支持比较复杂的可变数量的参数,下面详细介绍函数调用时的参数列表使用;
- 位置参数
默认情况下,定义的函数普通参数,就是位置参数,调用时需要按照正确的参数顺序依次传入进行调用,如下:
1 | def Print(arg1, arg2): |
- 关键字参数
Python允许在调用函数时,以不按参数顺序传入参数值,调用时,通过指定函数的参数名以及对应的值的方式来调用,如下:
1 | def Print(arg1, arg2): |
- 默认参数
和C/C++,Go一样,Python支持在函数定义的参数结尾指定参数的默认值,在进行函数调用的时候可以不指定默认参数的值,使用函数定义的默认值;如下简单测试:
1 | def Bob(a, b = 1, c = 2): |
Python的参数默认值只会在代码段第一次调用的时候计算一次;所以如果默认值是列表、字典或类实例等可变对象时,会产生意想不到的结果哦,如下:
1 | def f(a, L=[]): |
不想在后续调用之间共享默认值时,需要以以下方式使用:
1 | def f(a, L=None): |
- 可变长度参数
Python提供了任意长度的可变长度参数列表,参数以元祖的形式最终出入函数,可变参数的定义:参数名字前面加上*
标识,且需要放到参数列表最后;如下:
1 | def Alice(arg1, arg2, *args): |
- 可变关键字参数
在可变长度参数的基础上,Python提供了可变关键字参数,即可以传入任意数量的关键字参数;可变关键字参数的定义:参数名前面加上**
标识,且放到参数列表最后,最终可变关键字参数会以字典的形式打包传入函数内,如下:
1 | def Alice(arg1, arg2, **args_dict): |
- 参数说明
前面已经介绍了,函数参数可以是默认位置参数,关键字参数,默认参数,可变长度参数,可变关键字参数;为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。
Python提供了通过/
和*
两个符号,来声明函数的参数是怎么样的一种形式;如下特殊参数函数定义的语法格式:
1 | def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): |
如下测试代码:
1 | def combined_example(pos_only, /, standard, *, kwd_only): |
- 特殊参数位置
当位置参数,默认参数,可变长度参数,可变关键字参数同时在函数的参数列表中出现的时候,需要遵循以下参数顺序:
- 可变关键字参数必须放在最后;
- 可变长度参数必须在位置参数后面,可变关键字参数前面;
- 默认参数必须放在位置参数后面,可变关键字参数前面;
1 | def Alice(arg1, arg2, arg3=3, *args_tuple, **args_dict): |
- 解包实参列表
函数调用要求独立的位置参数,但实参在列表,元组,字典里时,需要执行相反的操作,将实参进行反向解包操作,然后按照格式调用函数;Python可以用过*
和**
符号,将实参的序列或者字典进行解包,然后调用函数;
如下:
1 | 3, 6] args = [ |
0x2.B. 面向对象
类把数据与功能绑定在一起。Pythond的类类似于C++和Modula-3类的结合体,支持OOP的所有标准特性:类的继承机制支持多个基类、派生的类能覆盖基类的方法、类的方法能调用基类中的同名方法。对象可包含任意数量和类型的数据。和模块一样,类也支持 Python 动态特性:在运行时创建,创建后还可以修改。
如果用 C++ 术语来描述的话,类成员(包括数据成员)通常为 public (例外的情况见下文 私有变量),所有成员函数都是 virtual。与 C++ 一样,算术运算符、下标等具有特殊语法的内置运算符都可以为类实例而重新定义。
Python类的定义格式如下:
1 | class ClassName(Base1, Base2, Base3): |
类的定义是有数据和方法组成的,在Python中数据和方法都称为属性;类Python中属性又分为如下两种:
- 类的属性:包括:数据属性或则方法属性,可以通过句点属性标识法来访问。
- 实例的属性:和类的属性的唯一区别是,实例属性成员是属于每个实例的,而类属性是属于类的,不依赖任何实例。
0x2.A.1 数据属性
数据属性类似C++里的成员变量;数据属性分为两类:
- 动态数据属性(实例变量, 实例数据属性):不需要在构造器中,或其他地方预先声明或者赋值。这样的数据属性是属于实例的。
- 静态数据属性(类变量,类数据属性):在类的定义中进行初始化(类的顶层结构定义中), 在类的实例中对类的静态数据进行的修改不会影响到类的该静态数据的值。
如下是对于数据属性的测试:
1 | class Bob(): |
由上面的测试可知,在方法fun()中对data_mem进行的修改,没有影响的类的静态数据的值。再由下面的测试可知, 在fun()中对data_mem进行的修改实际上生成了一个新的实例的数据属性,该数据属性存在于test实例的名字空间中,属于该实例,和类Test无关。
1 | class Bob(): |
类的数据属性是和类绑定的,类似于C++中的static成员变量, 一般类的数据属性设计上都是希望只读的,如果通过类的属性引用方式进行修改,那么该数据属性就永远被修改了,后面所有访问都是修改后的值,而通过实例来修改都是生成一个新的同名数据属性,通过实例的引用访问都是读取该实例的数据属性,而不是类的数据属性, 同名的属性,实例中会优先访问实例的属性,而不是类的;
但是这里需要注意: 如果类的数据属性是mutable的对象,例如,列表,字典的时候,共享的结果会导致所有的实例共享同一个数据属性;如下,引用官方手册:
1 | class Dog: |
真确的类设计应该是使用实例数据属性,如下:
1 | class Dog: |
0x2.A.2 方法属性
Python的方法属性可以分为三类:
- 普通的方法属性:只能被类的实例调用,即 普通的方法属性是和实例绑定的;
- 静态方法:和全局方法的定义很类似,就是不需要传入self参数,但是在函数的定义后,要在类中标明该函数是静态的方法:或者使用装饰器语法。类的静态方法可以理解为C++中的static方法,它是 和类绑定的;
- 类方法:和普通的方法属性的区别在于:普通的方法属性需要传入self参数,而类方法不是把实例作为第一个参数,而是 把类作为参数传入,类参数不需要特殊命名,但一般都用cls作为变量名。类方法也是和 和类绑定的;
如下是静态方法的两种定义方式:
1 | class Bob(object): |
如下是类方法的两种定义方式:
1 | class Alice(object): |
类方法和静态方法很类似,都是 和类绑定的;区别在于: 类方法可以修改类的状态,而静态方法不可获取和修改类的状态;
0x2.A.4 类继承
Python类继承的基本语法格式如下,前面也说了Python的类和C++的类比较像:
1 | class DerivedClassName(Base1, Base2, Base3): |
派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。
派生类可能会重写其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,所以调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。 (对 C++ 程序员的提示:Python 中所有的方法实际上都是 virtual
方法。)
如下:
1 | class Parent(object): |
0x2.A.5 属性控制
Python并不提供严格意义的权限控制, 仅从一个对象内部访问的“私有”实例变量在 Python 中并不存在;Python上代码规范层面的约定:带有一个下划线的名称 (例如 _spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。但是 不具有语法层面的约束力;
1 | class Bob(): |
但是,面向对象的语言设计,还是 会存在类对于私有成员的使用场景,Python为了在为了避免名字冲突,例如避免父类的属性名称和子类的属性名称冲突,Python设计了 名字改写的功能:类中任何形式为__XXX
的标识符( 至少带有两个前缀下划线,至多一个后缀下划线)的属性,都会被替换为_classname__XXX
,其中classname
为去除了前缀下划线的当前类名称;名称改写有助于让子类重载方法而不破坏基类内方法调用。如下测试,可以通过改名后的名字进行私有成员访问,所以 Python在语法层面上并不存在权限控制,名字改写的目的也是为了防止名字冲突而已:
1 | Bob._Bob__mem_b |
如下,是私有成员属性的设计:
1 | class Parent(object): |
0x2.C. Python数据模型
理解Python的数据模型,对于里面Python是如何设计和工作的十分重要;
0x2.C.1 基本定制
Python针对所有对象都支持了基本定制功能,这样可以提供更加丰富的对象定制和操作;简述如下,可以参考官方手册:
object.__new__(*cls*[, *...*])
__new()__
必须是一个静态方法,它是在类的实例化的时候最先调用的函数,用于构建类的实例,__new()__
结束后会返回一个合法的实例。__new()__
调用结束后,解析器会继续调用__init()__
函数,并把__new()__
返回的实例作为self参数传给__init()__
函数。
典型的实现会附带适宜的参数使用 super().__new__(cls[, ...])
,通过超类的 __new__()
方法来创建一个类的新实例,然后根据需要修改新创建的实例再将其返回。
__new__()
的目的 主要是允许不可变类型的子类 (例如 int, str 或 tuple) 定制实例创建过程。它也常会在自定义元类中被重载以便定制类创建过程。
object.__init__(self[, ...])
在实例 (通过 __new__()
) 被创建之后,返回调用者之前调用。其参数与传递给类构造器表达式的参数相同。一个基类如果有 __init__()
方法,则其所派生的类如果也有__init__()
方法,就必须显式地调用它以确保实例基类部分的正确初始化;例如: super().__init__([args...])
.
因为对象是由__new__()
和 __init__()
协作构造完成的 (由 __new__()
创建,并由 __init__()
定制),所以 __init__()
返回的值只能是 None,否则会在运行时引发 TypeError。
object.__del__(self)
在实例将被销毁时调用。 这还被称为终结器或析构器(不适当)。 如果一个基类具有__del__()
方法,则其所派生的类如果也有__del__()
方法,就必须显式地调用它以确保实例基类部分的正确清除。
当解释器退出时不会确保为仍然存在的对象调用 __del__()
方法。
del x
并不直接调用x.__del__()
, 前者会将 x 的引用计数减一,而后者仅会在 x 的引用计数变为零时被调用。
object.__repr__(*self*)
由 repr()
内置函数调用以输出一个对象的“官方”字符串表示。如果可能,这应类似一个有效的 Python 表达式,能被用来重建具有相同取值的对象(只要有适当的环境)。如果这不可能,则应返回形式如 <...some useful description...>
的字符串。返回值必须是一个字符串对象。如果一个类定义了 __repr__()
但未定义 __str__()
,则在需要该类的实例的“非正式”字符串表示时也会使用 __repr__()
。
object.__str__(*self*)
通过 str(object)
以及内置函数 format()
和 print()
调用以生成一个对象的“非正式”或格式良好的字符串表示。返回值必须为一个 字符串 对象。
此方法与 object.__repr__()
的不同点在于 __str__()
并不预期返回一个有效的 Python 表达式:可以使用更方便或更准确的描述信息。
内置类型 object
所定义的默认实现会调用 object.__repr__()
。
还有很多定制属性,如下是比较方法属性,具体可以参考可以参考官方手册;
object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)
0x2.C.2 特殊属性
Python针对不同层级对象,提供了很多不同的内建特殊属性,来获取相关对象的特殊描述信息,但是官方介绍:它们的定义在未来可能会改变,如下是不同类型对象的特殊属性列表:
- 自定义函数对象的特殊属性
针对所以自定义的函数对象,有如下特殊属性可以使用:
属性 | 含意 | |
---|---|---|
__doc__ |
该函数的文档字符串,没有则为 None ;不会被子类继承。 |
可写 |
__name__ |
该函数的名称。 | 可写 |
__qualname__ |
该函数的 qualified name。3.3 新版功能. | 可写 |
__module__ |
该函数所属模块的名称,没有则为 None 。 |
可写 |
__defaults__ |
由具有默认值的参数的默认参数值组成的元组,如无任何参数具有默认值则为 None 。 |
可写 |
__code__ |
表示编译后的函数体的代码对象。 | 可写 |
__globals__ |
对存放该函数中全局变量的字典的引用 — 函数所属模块的全局命名空间。 | 只读 |
__dict__ |
命名空间支持的函数属性。 | 可写 |
__closure__ |
None 或包含该函数可用变量的绑定的单元的元组。有关 cell_contents 属性的详情见下。 |
只读 |
__annotations__ |
包含形参标注的字典。 字典的键是形参名,而如果提供了 'return' 则是用于返回值标注。 有关如何使用此属性的更多信息,请参阅 对象注解属性的最佳实践。 |
可写 |
__kwdefaults__ |
仅包含关键字参数默认值的字典。 | 可写 |
- 自定义的类的特殊属性
属性 | 含意 | |
---|---|---|
__doc__ |
该函数的文档字符串,没有则为 None ;不会被子类继承。 |
可写 |
__name__ |
类的名称 | 可写 |
__dict__ |
包含类命名空间的字典 | 可写 |
__module__ |
类定义所在模块的名称,没有则为 None 。 |
可写 |
__bases__ |
包含基类的元组,按它们在基类列表中的出现先后排序。 | 可写 |
__code__ |
表示编译后的函数体的代码对象。 | 可写 |
__annotations__ |
包含在类体执行期间收集的 变量标注 的字典。 有关使用 __annotations__ 的最佳实践,请参阅 对象注解属性的最佳实践。 |
0x2.D. Python包管理
Python通过包管理来进行模块间的引用,模块间通过import
关键字进行导入使用;这里简单说一下使用,具体参考「Python教程-导入系统」;
import
的语法格式如下:
1 | import_stmt ::= "import" module ["as" identifier] ("," module ["as" identifier])* |
具体如下示例:
1 | import foo # foo imported and bound locally |
实际使用时不要使用:from module import \*
方式进行导入:原因如下:
污染了当前的名称空间,还有可能覆盖当前名称空间的名字(如果在中间导入)。
只从模块导入名字副作用:被导入的名字会成为局部名称空间的一部分。
0x3. 结语
端午节开始的整理,到今天,效率有点低,整理起来才发现,Python语言的基础太多了,后面有时间再针对一些特性进行详细学习和真理吧,例如包管理,容器等;