Python自动化运维快速入门
上QQ阅读APP看书,第一时间看更新

1.5 Python基础语法

1.5.1 数字运算

编程是将问题数据化的一个过程,数据离不开数字,Python的数字运算规则与我们学习的四则运算规则是一样的,即使不使用Python来编写复杂的程序,也可以将其当作一个强大的计算器。打开Python,试运行以下命令:

提示

在不同的机器上浮点运算的结果可能会不一样。

在整数除法中,除法(/)总是返回一个浮点数,如果只想得到整数的结果,就可以使用运算符//。整数除法返回浮点型,整数和浮点数混合运算的结果也是浮点型。

Python可以使用**操作来进行幂运算。

在交互模式中,最后被输出的表达式结果被赋值给变量_,这样能使后续计算更方便。例如:

>>> tax = 12.5 / 100
>>> price = 100.50
>>> price * tax
12.5625
>>> price + _
113.0625
>>> round(_, 2)
113.06

Python数字类型转换:


int(x)将x转换为一个整数。

float(x)将x转换为一个浮点数。

complex(x)将x转换为一个复数,实数部分为x,虚数部分为0。

complex(x, y)将x和y转换为一个复数,实数部分为x,虚数部分为y。x和y是数字表达式。


常用的数学函数可参见表1-2。

表1-2 常用的数学函数

1.5.2 字符串

1. 认识简单字符串

Python中的字符串有几种表达方式,可以使用单引号、双引号或三引号(三个单引号或三个双引号)括起来。例如:

>>> 'abc'
'abc'
>>> "abc"
'abc'
>>> '''a\
... b\
... c'''   #使用 反斜线(\)来续行
'abc'
>>> '''abc'''
'abc'

如果想要字符串含有单引号、双引号该怎么处理呢?有两种方法:一是使用反斜杠转义引号;二是使用与字符串中单引号、双引号不同的引号来定义字符串。例如:

使用\n换行或使用三引号。例如:

如果需要避免转义,则可以使用原始字符串,即在字符串的前面加上r。例如:

>>> s = r"This is a rather long string containing\n\
... several lines of text much as you would do in C."
>>> print(s)
This is a rather long string containing\n\
several lines of text much as you would do in C.

字符串可以使用+运算符连接在一起,或者使用*运算符重复字符串。例如:

>>> word = 'Help' + ' '+ 'ME'
>>> print(word)
Help ME
>>> word="word "*5
>>> print(word)
word word word word word

2. 字符串的索引

字符串可以被索引,就像C语言中的数组一样,字符串的第一个字符的索引为0,一个字符就是长度为一的字符串。与Icon编程语言类似,子字符串可以使用分切符来指定:用冒号分隔的两个索引,第一个索引默认为0,第二个索引默认为最后一个位置,s[:]表示整个字符串,s[2:3]表示从第3个字符开始,到第4个字符结束,不含第4个字符。不同于C字符串的是,Python字符串不能被改变。向一个索引位置赋值会导致错误,例如:

3. 字符串的遍历

遍历字符串有三种方式:一是使用enumerate函数,其返回字符串的索引及相应的字符;二是直接使用for循环;三是通过字符索引来遍历。例如:

有一个方法可以帮我们记住分切索引的工作方式,想象索引是指向字符之间,第一个字符左边的数字是0,接着有n个字符的字符串最后一个字符的右边是索引n。例如:

如s[1:3]代表bc,s[-2:-1]代表f。

4. 字符串的格式化

Python支持格式化字符串的输出。尽管这样可能会用到非常复杂的表达式,但最基本的用法是将一个值插入到一个有字符串格式符%s的字符串中。

>>> print ("我叫 %s 今年 %d 岁!" % ('小明', 10))#使用%
我叫 小明 今年 10 岁!
>>> print ("我叫 {} 今年 {} 岁!" .format('小明', 10))#使用字符串的format方法
我叫 小明 今年 10 岁!
>>> print ("我叫 {0} 今年 {1} 岁!" .format('小明', 10,20))#使用索引,整数20未用到
我叫 小明 今年 10 岁!

需要在字符中使用特殊字符时,Python用反斜杠(\)转义字符,如表1-3所示。

表1-3 转义字符

5. 字符串的内建函数

Python字符串的内建函数可参见表1-4。

表1-4 字符串的内建函数

(续表)

1.5.3 列表与元组

列表是Python常用的数据类型,也是最基本的数据结构。Python的列表是由方括号“[]”[]括起,使用“,”分隔的序列,序列中的数据类型不要求一致,序列的索引从0开始。


【示例1-1】创建一个列表,只要把逗号分隔的不同数据项使用方括号括起来即可。

>>> list1 = ['Google', 'Huawei', 1997, 2000];
>>> list2 = [1, 2, 3, 4, 5 ];
>>> list3 = ["a", "b", "c", "d"];
>>> list4=["all of them",list1,list2,list3]
>>> print ("list1[0]: ", list1[0])
list1[0]:  Google
>>> print ("list2[1:5]: ", list2[1:5])
list2[1:5]:  [2, 3, 4, 5]
>>> print(list4)
['all of them', ['Google', 'Huawei', 1997, 2000], [1, 2, 3, 4, 5], ['a', 'b', 'c',
'd']]
>>> print(list4[1][1])
Huawei


【示例1-2】更新一个列表,可以对列表的数据项进行修改,也可以使用append()方法添加列表项。


【示例1-3】删除列表中的某个元素。

>>> list = ['Google', 'Huawei', 1997, 2000]
>>> del list[0]
>>> print(list)
['Huawei', 1997, 2000]

列表还有一些其他操作,如列表对+和*的操作符与字符串相似,+号用于组合列表,*号用于重复列表。

列表的常用方法可参见表1-5。

表1-5 列表的常用方法

元组与列表类似,用“()”括起,“,”分隔的序列,不同于列表的是,元组是只读的,无法被修改,在定义时其元素必须确定下来,也可以像列表一样使用索引来访问。


【示例1-4】元组的应用。

注意,元组元素不变是指元组每个元素指向永远不变,如果元组的某个元素是一个列表,那么这个列表的元素是可以被改变的,但元组指向这个列表永远不变。


【示例1-5】元组的某个元素是列表。

如果希望元组中的每个元素无法被修改,就必须保证元组的每一个元素本身也不能变,如数字、字符串、元组等不可变数据类型。

1.5.4 字典

一提到字典,我们就会想到中华字典、英语词典等,通过给定的单词(key)查找其含义(value)。在字典里,要查找的单词(key)是唯一的,但不同的单词其含义(value)可能相同。Python里的字典就是键值对(key-value)组成的集合,且可存储任意类型对象。定义一个字典非常简单:使用一对花括号{}括起,键值对之间使用“,”分隔。例如:

字典值可以是任何的Python对象,既可以是标准对象,也可以是用户自定义的对象,但键不行。两个重要的点需要记住:

(1)不允许同一个键出现两次。创建时如果同一个键被赋值两次,后一个值就会被记住。


【示例1-6】不允许同一个键出现两次。

>>> dict = { 'hello':'你好','world':'世界','hello':'world'} #键hello的值被更新为
world
>>> dict
{'hello': 'world', 'world': '世界'}

(2)因为键必须不可变,所以可以用数字、字符串或元组充当,用列表则不行,即键必须为不可变数据类型。


【示例1-7】键必须为不可变数据类型。

>>> d = { 'a':1,'b':2, ['a']:'abc'}  #键是列表,会报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'


【示例1-8】遍历字典。


【示例1-9】修改字典。

>>> d = { 'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6 }
>>> d['b']='b'
>>> d
{'a': 1, 'b': 'b', 'c': 3, 'd': 4, 'e': 5, 'f': 6}


【示例1-10】删除字典元素。可以删除单一的元素,也可以一次性删除所有元素,清空字典,显式地删除一个字典用del命令。

Python字典的内置方法可参见表1-6。

表1-6 字典的常用方法

1.5.5 集合

集合set是一个无序不重复元素集,基本功能包括关系测试和消除重复元素。集合对象还支持union(联合)、intersection(交)、difference(差)和sysmmetric difference(对称差集)等数学运算。

在Python中可以使用“x in set”来判断x是否在集合中,使用“len(set) ”来获取集合元素个数,使用“for x in set”来遍历集合中的元素。但由于集合不记录元素位置,因此集合不支持获取元素位置和切片等操作。


【示例1-11】集合的定义和常见用法。


【示例1-12】使用集去重元素。

>>> a = [11,22,33,44,11,22]
>>> b = set(a)
>>> b
set([33, 11, 44, 22])

集合的基本操作可参见表1-7。

表1-7 集合的基本操作

提示

union()、intersection()、difference()和symmetric_difference()的非运算符(non-operator就是形如s.union()这样的)版本将会接受任何可迭代对象(iterable)作为参数。相反,它们的运算符版本(&^+-|)要求参数必须是集合对象。

1.5.6 函数

在中学数学中我们知道y=f(x)代表着函数,x是自变量,y是函数f(x)的值。在程序中,自变量x可以代表任意的数据类型,可以是字符串、列表、字典、对象,可以是我们认为的任何东西。


【示例1-13】以简单的数据计算函数为例,定义函数fun(a,b,h)来计算上底为a,下底为b,高为h的梯形面积。

函数的目的是封装,提高应用的模块性及代码的重复利用率。将常用的处理过程写成函数,在需要时调用它,可以屏蔽实现细节,减少代码量,增加程序可读性。


【示例1-14】假如多个梯形的面积需要计算,那么:

上例中的调用方法fun(3,4,5)并不直观,为了增加可读性,这里我们稍做调整。

在调用此函数传递参数时使用参数关键字,这样参数的位置可以任意放置而不影响运算结果,增加程序可读性。假如待计算的梯形默认高度均为5,就可以定义带默认值参数的函数。

提示

带有默认值的参数必须位于不含默认值参数的后面。

关于函数是否会改变传入变量的值有以下两种情况。


(1)对不可变数据类型的参数,函数无法改变其值,如Python标准数据类型中的字符串、数字、元组。

(2)对可变数据类型的参数,函数可以改变其值,如Python标准数据类型中的列表、字典、集合。


【示例1-15】举例说明。

1.5.7 条件控制与循环语句

1. 条件控制

Python的条件控制是通过一条或多条语句的执行结果(True或False)来决定执行的代码块。条件控制的流程如图1.26所示。

图1.26 条件控制的流程

if语句的一般形式如下:

解释:如果条件1为真,则执行语句1;如果条件1不为真,条件2为真,则执行语句2;如果条件1、条件2都不为真,则执行语句3。其中elif和else语句不是必需的。


【示例1-16】将下列代码保存为lx_if.py。

在命令窗口执行python lx_if.py后得到如下结果。

99 excellent
80 fine
70 pass
60 pass
59 bad

if语句还可以用来实现问题表达式。例如:有整数变量a、b、c,如果a<b,那么c=a,否则c=b。我们可以用一行代码实现:

2. 循环语句

Python有两种方式来实现循环:while语句和for语句。

while语句的结构如下:

当条件判断为真时执行语句1,当条件判断为假时执行语句2,其实只要不是死循环,语句2就一定会被执行。因此,while语句的结构也可以如下:

while语句的流程如图1.27所示。

图1.27 while语句的流程


【示例1-17】将下面的代码保存为lx_while.py。

在命令窗口中执行python lx_while.py,并尝试输入一些字符,结果如下。

please input something,'q' for quit.-> hello
your input is hello
please input something,'q' for quit.-> python
your input is python
please input something,'q' for quit.-> q
your input is q
You're out of circulation.

Python for循环可以遍历任何序列的项目,如一个列表或一个字符串。for循环的一般格式如下:

for <variable> in <sequence>:
<statements>
else:
<statements>


【示例1-18】计算1~1000的所有整数的和。

循环中的break语句和continue语句:从英文字面意思来理解即可,break就是中断,跳出当前的循环,不再继续执行循环内的所有语句;continue就是继续,程序运行至continue处时,不再执行continue后的循环语句,立即进行下一次循环判断。下面通过一个例子来了解两者的区别。


【示例1-19】break语句和continue语句的比较(lx_break_continue.py)。

在命令行中运行python lx_break_continue.py将得到如下结果。

break--------------
aaa 0
bbb 1
aaa 1
continue--------------
aaa 0
bbb 1
aaa 1
aaa 2
bbb 3
aaa 3
bbb 4
aaa 4
bbb 5

我们看到break直接跳出了循环,而continue只是跳过了其中一步(输出bbb 2的那一步)。

1.5.8 可迭代对象、迭代器和生成器

迭代是Python最强大的功能之一,是访问集合元素的一种方式。迭代器是一个可以记住遍历位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问结束。迭代器只能往前不会后退。迭代器有两个基本的方法:iter()和next()。字符串、列表或元组对象都可用于创建迭代器。

首先来了解一下可迭代对象、迭代器和生成器的概念。


(1)可迭代对象:如果一个对象拥有__iter__方法,这个对象就是一个可迭代对象。在Python中,我们经常使用for来对某个对象进行遍历,此时被遍历的对象就是可迭代对象,常见的有列表、元组、字典。for循环开始时自动调用可迭代对象的__iter__方法获取一个迭代器,for循环时自动调用迭代器的next方法获取下一个元素,当调用可迭代器对象的next方法引发StopIteration异常时,结束for循环。

(2)迭代器:如果一个对象拥有__iter__方法和__next__方法,这个对象就是一个迭代器。

(3)生成器:生成器是一类特殊的迭代器,就是在需要时才产生结果,而不是立即产生结果。这样可以同时节省CPU和内存。有两种方法可以实现生成器:


生成器函数。使用def定义函数,使用yield而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间挂起函数的状态,以便下次从它离开的地方继续执行。

生成器表达式。类似于列表推导,只不过是把一对大括号[]变换为一对小括号()。但是生成器表达式是按需产生一个生成器结果对象,要想拿到每一个元素,就需要循环遍历。


三者之间的关系如图1.28所示。

图1.28 可迭代对象、迭代器和生成器的关系

可迭代对象包含迭代器、序列、字典;生成器是一种特殊的迭代器,下面分别举例说明。


【示例1-20】创建一个迭代器对象(lx_iterator.py)。

因为类MyListIterator实现了__iter__方法和__next__方法,所以它是一个迭代器对象。由于__iter__方法本返的是迭代器(本身),因此它也是可迭代对象。迭代器必然是一个可迭代对象。

下面使用三种方法遍历迭代器MyListIterator。

输出结果如下:

使用for循环来遍历迭代器
0
1
2
3
4
使用next来遍历迭代器
0
1
2
3
4
同时使用next和for来遍历迭代器
先使用两次next
0
1
再使用for,会从第三个元素2开始输出
2
3
4

从结果可以看出,for循环实际上就是调用了迭代器的__next__方法,当捕捉到MyListIterator异常时自动结束for循环。


【示例1-21】创建一个可迭代对象。

因为对象MyList实现了__iter__方法返回了迭代器类的实例,所以它是一个可迭代对象。遍历操作可使用for循环,不可使用next()。for循环实质上还是调用MyListIterator的__next__方法。

输出结果如下:

使用for循环来遍历可迭代对象my_list
0
1
2
3
4
使用next来遍历可迭代对象my_list
print(next(my_list))
TypeError: 'MyList' object is not an iterator

从运行结果知道,可迭代对象如果没有__next__方法,则无法通过next()进行遍历。


【示例1-22】创建一个生成器,像定义一般函数一样,只不过使用yield返回中间结果。生成器是一种特殊的迭代器,自动实现了迭代器协议,即__iter__方法和next方法,不需要再手动实现两个方法。创建生成器:

遍历生成器:

运行结果如下:

for 循环遍历生成器myList
0
1
2
3
4
next遍历生成器myList
0
1
2
3
4

具有yield关键字的函数都是生成器,yield可以理解为return,返回后面的值给调用者。不同的是return返回后,函数会释放,而生成器则不会。在直接调用next方法或用for语句进行下一次迭代时,生成器会从yield下一句开始执行,直至遇到下一个yield。

1.5.9 对象赋值、浅复制、深复制

Python中对象的赋值,复制(深/浅复制)之间是有差异的,如果使用时不注意,就可能导致程序崩溃或严重bug。下面就通过简单的例子来介绍这些概念之间的差别。


【示例1-23】对象赋值操作(testFuzhi.py)。

输出结果如图1.29所示。

图1.29 对象赋值操作

下面来分析代码:首先第3行创建了一个名为object1的变量,这个变量指向一个list对象,第5行将object1赋给object2,然后打印它们及它们指向的对象在内存中的地址(通过id函数)。第18和19行修改object1,然后分别打印object1与object2在内存中的地址。从运行结果来看,无论是object1还是object2,它们都向同一个内存地址,即指向的都是同一个对象,也就是说“object1 is object2 and object1[i] is object2[i] ”,对object1的操作同样会反应到object2上,打印object1和object2的结果始终是显示一致的。


【示例1-24】浅复制操作(testCopy.py)。

运行结果如图1.30所示。

图1.30 浅复制操作

代码说明:与testFuzhi.py不同的是,第2行导入copy模块,第5行调用copy模块的copy函数来为object2进行赋值,也就是浅复制操作。从运行结果来看,object1与object2指向内存中的不同位置,它们属于两个不同的对象,但列表内部仍指向同一个位置。修改了object1[0]= "Wilber"后,object1对象的第一个元素指向了新的字符串常量"Wilber",而object2仍指向"Will"。执行object1[2].append("CSS")时object1[2]的地址并未改变,object1与object2的第三个元素仍指向此子列表。

总结一下浅复制:通过copy模块中的浅复制函数copy()对object1指向的对象进行浅复制,然后浅复制生成的新对象赋值给object2变量。浅复制会创建一个新的对象,这个例子中"object1 is not object2",但是对于对象中的元素,浅复制就只会使用原始元素的引用(内存地址),也就是说,"wilber[i] is will[i]"。当对object1进行修改时由于list的第一个元素是不可变类型,因此object1对应的list的第一个元素会使用一个新的对象,但是list的第三个元素是一个可变类型,修改操作不会产生新的对象,object1的修改结果会就相应地反应到object2上。


【示例1-25】深复制操作(testDeepCopy.py)。

运行结果如图1.31所示。

图1.31 深复制操作

从运行结果来看,这个非常容易理解,就是创建了一个与之前对象完全独立的对象。通过copy模块中的深复制函数deepcopy()对object1指向的对象进行深复制,然后深复制生成的新对象赋值给object2变量。与浅复制类似,深复制也会创建一个新的对象,这个例子中"object1 is not object2",但是对于对象中的元素,深复制都会重新生成一份(有特殊情况,下面会说明),而不是简单地使用原始元素的引用(内存地址)。也就是说," object1[i] is not object2[i]"。

复制有一些特殊情况:


对于原子数据类型(如数字、字符串、只含不可变数据类型的元组)没有复制一说,赋值操作相当于产生一个新的对象,对原对象的修改不影响新对象。简言之,赋值操作与浅复制和深复制的效果是一样的。

如果元组变量只包含原子类型对象,深复制就不会重新生成对象,这其实是Python解释器内部的一种优化机制,对于只包含原子类型对象的元组,如果它们的值相等,就在内存中保留一份,类似的还有小整数从-5~256。在内存中只保留一份,可节省内存,提高访问速度,如图1.32所示。

图1.32 元组的深复制