Python教程3 - 复习:内置对象

这篇文章是对我们之前所学知识的复习,也教授了一些新的知识,并且提供了一些习题来巩固大家所学的知识。由于我们已经大致掌握了Python命令式编程的设施,这里我就把很多细节补全,并且不再是按照人的学习顺序组织。这篇文章的内容适合不仅适合那些Python新手,也适合已经熟练运用Python的人巩固知识或作为参考查询。

# 1 内置对象

我们可能经常会用到对象这个词。按照Python的官网上的说法,所谓对象就是一个有唯一ID、类型的东西。对象的ID和类型是不会改变的,而根据对象的值能否改变,对象可以分为可变的(mutable)不可变的(immutable),这是由它的类型决定的。有些容器如元组,虽然它的内容可能是个可变对象,但我们依旧认为它是个不可变对象。Python中的一切实体都是对象,或者说一切可被赋值给变量的都是对象。而变量是一种到对象的引用,它将一个对象绑定到了名字。当没有任何方法能够获取到某个对象时,那个对象就会被销毁。

Python的类型之间是有父子关系的。子类型会拥有父类型的所有方法。所有的类型都是object类型的子类型。比如int类型就是object类型的子类型,这种父子关系我们称为继承(inheritance),因此我们说int类型继承自object类型。这里我们还需要引入一个名词叫实例(instance),当我们说对象a是类型A的实例的时候,其实就在说对象a的类型是A或者A的子类型(包括直接或间接的)。比如1int的实例,'1'str的实例,而1'1'都是object的实例,甚至Python中一切的对象都是object的实例。

面向对象启蒙

面向对象编程启发自大自然。我们就以大自然中的动物为例,每个动物都有一些属性,比如性别,同时它们也有一些方法,比如繁殖。将这些属性和方法聚集在一起,这就是面向对象的第一个组成部分——封装(encapsulation)。而人类和鸟类都继承自动物,因此人类和鸟类都有繁殖这个方法,同时又有性别这个属性,这就是继承(inheritance),面向对象编程的第二个重要组成部分。另一方面,子类型除了能继承父类型的所有方法之外,还能重新定义父类型的方法。比如人类的繁殖是胎生,而鸟类的繁殖就是卵生。这样同样是动物,却对同一个方法有不同的具体行为,这就是多态(polymorphism)。多态是面向对象编程的第三个重要组成部分。

最后,我和正在阅读这些文字的你都是人类和动物的实例(instance),是一种对象(object)

下图是一些内置类型的继承关系,其中方框是内置类型,椭圆来自collection.abc,每个椭圆的内容都提供一些方法。以后如果有机会,我会再来介绍每个节点是什么含义,你可以点击此处查看相关文档。

graphviz image

接下来介绍下面2个运算符和4个函数:

  • id(a)函数:获取对象的ID,这个ID是个整数,而且在对象存在的期间是唯一的(不与其他对象一样);
  • type(a)函数:获取对象的类型;
  • a is ba is not b:判断ab是不是同一对象,如果id(a) == id(b),则就认为是同一对象;
  • isinstance(a, A):判断对象a是否是类型A的实例,注意这个运算符与type(a) is A还是有区别的比如isinstance(1, object)是真,但是type(1)int不是object
  • issubclass(A, B):判断类型A是否是类型B的子类型。

下面是例子。

>>> id(1)  # 这个数字是会改变的
139698536331168
>>> type(1) is int  # 最好不要在此处使用 ==
True
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b  # 对于相同值的可变对象,它们也可能不是同一对象,拥有不同的ID
False
>>> a is not b  # 上面式子的否定
True
>>> isinstance(1, int)
True
>>> isinstance('1', int)
False
>>> isinstance(1, object)
True
>>> isinstance('1', object)
True
>>> issubclass(int, object)
True

注意

请不要把判断对象相同运算符isis not和判断值相等运算符==!=搞混。

提示

Python中经常有一些函数,其实也是类型,如strint等,因为这个类型可以接受参数来实例化出对象,这种函数我们称为构造函数(constructor)

我们将在以后学习自定义的对象和类型,但其实内置对象已经足够我们使用。我们先从复习内置对象开始。

# 1.1 整数

Python内置支持无限位数的整数。它是一种不可变对象。

# 1.1.1 整数字面量

整数的字面量可以是以下几种情况:

  • 十进制:非0开头的十进制数,如127
  • 二进制:0b0B开头的二进制数,如0b1111111
  • 八进制:0o0O开头的八进制数,如0o177
  • 十六进制:0x0X开头的十六进制数,使用afAF表示10到15,如0x7f

其中十进制肯定是最常用的,而有时十六进制也很常用,因为2位16进制可以表示一个字节。

整数字面量的进制标识(类似0x)和数字以及数字和数字之间都可以加入下线符_,如1_10x_ff,而11_1__1都是非法的。增加这个语法主要是为了方便很长的数字被阅读,不过这个语法也用得很少。

# 1.1.2 整数与字符串的转换

int是整数的构造函数,它可以从浮点数和字符串中构造出整数:

  • 对于参数为浮点数的情况,它会截断(正数是向下取整);
  • 对于参数为字符串的情况,它有第二个参数名叫base可以指定进制数,必须是2到36(因为数字+字母总共36个,只能表示36进制),默认为10进制,如果你指定0,则它会从开头的进制标识来推断是几进制。
>>> int(0.9)
0
>>> int(-0.9)
0
>>> int('177')
177
>>> int('177', 8)
127
>>> int('0o177', base=0)
127

除了将某进制的字符串转成整型,还可以将整型转成指定进制的字符串。这里Python提供了以下函数:

  • str:这其实是个通用的构造函数,可以将任意对象转换成字符串,对于整数它会转成10进制。
  • bin:将整数转成2进制字符串,包含前缀0b
  • oct:将整数转成8进制字符串,包含前桌0o
  • hex:将整数转成16进制字符串,包含前缀0x

下面给一些例子:

>>> str(127)
'127'
>>> bin(127)
'0b1111111'
>>> oct(127)
'0o177'
>>> hex(127)
'0x7f'
>>> hex(127)[2:]  # 如果你不想要进制标识,可以用切片语法去除掉
'7f'

# 1.2 浮点数

浮点数能存储精度为15位有效数字左右的实数。它是一种不可变对象。

# 1.2.1 浮点数字面量

浮点数的字面量格式大致如下:

formula

它表示的数字其实就是:formula。这里整数、小数和指数可以是任意十进制数字,数字和数字之间可以插入_(但同样不常用)。浮点数的字面量可以省略一些部分,但必须满足如下规则:

  • 整数和小数可以省略,但不可以同时省略:1.e-2.5e6都是合法的浮点数,但是.e-2都不是合法的。
  • 小数部分和指数部分可以省略,但不可以同时省略:1e21.0都是合法的浮点数,但1不是,实际上它是整数。

为了确保某些字面量是浮点类型,我们经常会加上.0,比如1.00.0

# 1.2.2 浮点数与字符串的转换

float是浮点数的构造函数,它可以从整数和字符串中构造出浮点数。同样地,你可以通过str函数将浮点数转成字符串。

# 1.2.3 浮点数的精度问题

浮点数是有误差的,因此用等于去判断两个浮点数的值是否是相等的是有风险的:

>>> 0.1 + 0.1 + 0.1 == 0.3
False

上一讲中,我介绍了一种用两数只差小于某个很小的值来判断两个浮点数是否相等的方法。实际上,我后来查阅了一些文档,发现在Python 3.5中新增了用于判断两个实数是否相近的函数,使用方法如下:

>>> import math
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
True

# 1.2.4 特殊的浮点数值

这里我们使用import语句导入了一个包,我们会在之后的课程里讲解这些。

注意浮点数的零有正零和负零之分,但是正零和负零是相等的:

>>> +0
0
>>> -0    # 整数只有一种0
0
>>> +0.0
0.0
>>> -0.0  # 浮点数有两种0
-0.0
>>> +0.0 == -0.0  # 但这两种0是相等的。
True

此外浮点数还有3个特殊的值:+inf-infnan,分别表示为正无穷、负无穷和不是一个数(not a number)。对于大到超出浮点数表示范围的数,就成了正负无穷。在很多语言包括Python的一些数学库(如numpy)中,浮点数除以0也会得到无穷,这是CPU指令约定成俗的,但是在Python中,它会额外检查除数,遇到0就报除零错误。

>>> 1e1000
inf
>>> float('inf')
inf
>>> import numpy
>>> numpy.float64(1) / 0.0
<stdin>:1: RuntimeWarning: divide by zero encountered in double_scalars
inf
>>> 1.0 / 0.0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: float division by zero

如果出现了无穷相除、0相除(Python),或者无穷乘以0这种极限中的不定式的情况,就会产生nan值,nan很特殊,它是唯一一个不等于自身的浮点数。

>>> inf = float('inf')
>>> inf / inf
nan
>>> nan = float('nan')
>>> nan == nan
False

# 1.3 字符串和Bytes

字符串类型用于存储文本数据,而Bytes用于存储二进制数据,它是种不可变对象,也是一种序列。

# 1.3.1 字符串和Bytes的字面量

字符串和Bytes字面量都可以用一对单引号或双引号括起来,区别在于单引号括住的字符串中的双引号可以不必转义,同样的双引号括住的字符串中的单引号可以不必转义。此外字符串和Bytes也可以用三重引号括起,区别在于单重引号不能包含不加转义的换行和对应的引号,而三重引号则可以。

>>> 'It\'s me.'
"It's me."
>>> "It's me."
"It's me."
>>> '''And then,
... that's you'''
"And then,\nthat's you"

字符串和Bytes字面量形式上的差别在于Bytes字面量有前缀bB。字符串和Bytes字面量还可以有前缀rR表示原始字符串(raw string)。前缀的大小写是忽略的,其顺序也是无所谓的。原始字符串中的\代表它本身,不再处理转义序列。原始字符串经常出现在Windows路径或者正则表达式中。此外字符串还允许有fF前缀表示格式化字符串,我们将在以后的章节介绍这种。

# 1.3.2 转义

下表是可以出现在字符串和bytes字面量中的转义字符列表:

转义序列 含义
\紧跟换行 \和换行被忽略
\\ 反斜杠符 (\)
\' 单引号 (')
\" 双引号 (")
\a ASCII 响铃 (Bell,BEL)
\b ASCII 退格 (Backspace,BS)
\f ASCII 换页 (Formfeed,FF)
\n ASCII 换行 (Linefeed,LF)
\r ASCII 回车 (Carriage Return,CR)
\t ASCII 水平制表符 (Horizontal Tab,TAB)
\v ASCII 垂直制表符 (Vertical Tab,VT)
\ooo 8进制值为ooo的字符,o代表8进制数位,最多可以有3个数位
\xhh 16进制值为hh的字符,h代表16进制数位,必须2个数位

除此之外,字符串还支持了额外的转义,列在了下表中,bytes不支持这些转义:

转义序列 含义
\N{name} 名字为name的 Unicode 字符
\uxxxx 16进制值为 xxxx 的16位字符,x表16进制数位,必须4个数位
\Uxxxxxxxx 16进制值为 xxxxxxxx 的32位字符,x表16进制数位,必须8个数位

有一些小细节这里提示一下,原始字符串也可以转义引号,但反斜杠符仍会被保留,如r"\"""\\\"";原始字符串也不能以反斜杠符结尾。

上面的两张表不必死记硬背,记住个大概就好,其中\t(水平制表)、\r(回车)、\n(换行)和\xhh这4种转义序列用得比较多。其中\r,我们在涉及文件输入输出时再细细介绍,这里我们给出一些例子:

>>> print('1\t2\n3\t4')
1       2
3       4
>>> '\x0a' == '\n' # 换行符ASCII码为10,十六进制为0a
True

# 1.3.3 字符与整数的转换

每个字符都是拥有一个编码的,字符编码中Unicode是ASCII的拓展,前者包含了我们用到的所有字符,包括中日韩表意文字、emoji等等,而后者只有128个字符,这个字符对应的编码称为码位(code point)。我们有两个函数对字符和码位互相转换:

  • 通过ord()函数我们能获得长度为1的字符串或bytes对应的码位;
  • 通过chr()函数我们能获得码位对应的长度为1的字符串。
>>> ord('我')
25105
>>> chr(25105)
'我'

# 1.3.4 编码与解码

文本在进行编码后就成了二进制数据,而二进制数据解码之后也就变回了文本。Python提供了负责这两种转化的方法(这里方法是指某个对象拥有的函数):

  • str.encode(encoding='utf-8'):将文本编码成二进制数据;
  • bytes.decode(encoding='utf-8'):将二进制数据解码为文本。

编码默认是utf-8。对于中国大陆而言,由于Windows大都采用国标码记录中文,也就是gbk,所以gbk也会比较常见。想看完整的编码列表可以点击此处。下面给一些例子:

>>> text = '你好世界'
>>> binary = text.encode()
>>> binary
b'\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c'
>>> binary.decode()
'你好世界'

# 1.3.5 将任何对象转换成字符串

Python提供了两个函数将对象转换为字符串:

  • str()函数:将对象转成一个适合打印的形式;
  • repr()函数:将对象转成一个包含很多信息的形式,如果可能,得到的字符串将是可以产生这个对象的Python代码。

可能即使这么说还是不够形象,我们给一个字符串的例子,对于字符串而言str()函数就是返回它本身:

>>> print(str('1\t2'))
1       2
>>> print(repr('1\t2'))
'1\t2'

其实print()函数对于非字符串的对象会调用str()函数转换成字符串后再打印。另外我们可以看到,当Python的交互式模式输入的是表达式的时候,Python会调用repr函数打印这个表达式的值。

原理

实际上str()会调用对象的__str__()方法,而repr()会调用对象的__repr__()方法。这些方法是所有对象都有的。

>>> print("1\t2".__str__())
1       2
>>> print("1\t2".__repr__())
'1\t2'

# 1.3.6 作为可迭代对象和序列

关于可迭代对象和序列的介绍可以见1.5.3 可迭代对象的操作1.5.4 序列的操作。作为不可变的序列,字符串和Bytes只支持获取长度,根据索引或切片获取元素等操作,不支持对索引或切片赋值,也不支持删除索引或切片等操作。

# 1.3.7 常见的字符串方法

完整的字符串方法列表可以点击此处查看。

判断是否包含子串:

  • str.startswith(prefix):判断字符串是否以prefix开头;
  • str.endswith(suffix):判断字符串是否以suffix结尾;
  • str.find(sub):寻找字符串中包含子串sub的起始索引,如果找不到返回-1
>>> '1234'.startswith('12')
True
>>> '1234'.endswith('12')
False
>>> '1234'.find('23')
1
>>> '1234'.find('32')
-1

对字符串做一些变换(返回新的字符串):

  • str.lower():将所有的字母转成小写;
  • str.uppper():将所有的字母转成大写;
  • str.strip():删除前导和后继空白符。
>>> 'abc'.upper()
'ABC'
>>> '  a\tb\t '.strip()
'a\tb'

一些判断字符类型的函数:

  • str.isalpha():是否都是英文字母且长度至少为1;
  • str.isdigit():是否都是数字且长度至少为1;
  • str.isspace():是否是空白字符且长度至少为1。

分割与合并:

  • str.split(sep=None):以sep为分隔符分割字符串成列表,如果sepNone,则以空白符分割;
  • str.join(iterable):将可迭代对象以str为分隔符连接成字符串。
>>> items = '1\t 2\n3'.split()
>>> items
['1', '2', '3']
>>> ','.join(items)
'1,2,3'

# 1.4 布尔

布尔对象只有两种值:真和假,分别用两个关键字TrueFalse表示,它们是布尔类型仅有的两个实例。布尔类型实际上和整数类型比较类似,在绝大多是情况下,True和1有同样的表现,而False和0有同样的表现。bool是布尔类型的构造函数,可以将任意类型转为布尔类型。对于if语句和while语句中的条件表达式,其值会被转换为bool类型。

# 1.4.1 其他类型转换到布尔类型

通常而言,我们认为以下的值是假的:

  • 被定义成假的常量:NoneFalse
  • 值为0的数值类型:如00.0等;
  • 空的容器:''()[]{}

其他情况被认为是真值。

原理

实际上,bool()会调用对象的__bool__()方法。如果这个方法没有定义,它会调用对象的__len__()方法看它是否非零,如果这个方法也没有,那这个对象就被认为是真的。

# 1.5 列表和元组

列表和元组都是序列。其中列表是可变对象,而元组是不可变对象。

# 1.5.1 列表和元组的字面量

注意

官方文档里面并没有采用列表字面量和元组字面量的词汇,所以我的说法可能是不标准的。官方采用列表显示(list display)指我这里说的列表字面量。而元组字面量我没有找到专业的名词。

使用中括号[]括起,逗号分隔的表达式列表就能表示一个列表,最后一个表达式的尾部可以有一个可选的逗号。如果你采用多行来完成列表字面量,很推荐你在最后一个表达式的尾部加上逗号,这样以后你再添加元素会很方便。

>>> list1 = [1, 2, 3, 4]
>>> list2 = [
...     'one',
...     'two',
...     'three',
...     'four',
... ]
>>> empty = []
>>> mono = ['a']

类似地,使用圆括号()括起,逗号分隔的表达式就能表示一个元组,最后一个表达式尾部可以有一个可选的逗号。当只有一个元素的时候,这个逗号是必须的,如果不添加逗号,就成了一个加了括号改变优先级的表达式。

>>> (1, 2, 3)
(1, 2, 3)
>>> (1)  # 错误的单元素元组表示方法
1
>>> (1,)
(1,)
>>> ()   # 空元组
()

值得一提的是,列表和元组的构造是支持解包语法的,这和我们之前说过的函数解包语法是类似的:

>>> a = [3, 4]
>>> b = [1, 2, *a, *(5, 6), 7]
>>> b
[1, 2, 3, 4, 5, 6, 7]

此外,元组的括号在不引起歧义(如赋值语句的右侧)的时候是可以省略的。下方代码中,b, c = 3, 4利用了赋值语句左侧如果是多个目标,则右侧的可迭代对象会被展开挨个赋值给目标;最后一行是很Pythonic的交换两个变量。

>>> a = 1, 2
>>> a
(1, 2)
>>> b, c = 3, 4
>>> b
3
>>> c
4
>>> b, c = c, b  # 交换两个变量

提示

Pythonic是对代码的一种要求,就是代码不仅是语法正确的,而且是遵循了Python的习俗的,容易被人理解的。

原理

Python哪些地方元组可以省略括号?就我看到的而言有如下地方:

  • 表达式语句(支持解包);
  • 赋值语句右侧(支持解包);
  • 复合赋值语句右侧(不支持解包);
  • yield右侧(不支持解包);
  • 下标运算符内(不支持解包);
  • return右侧(不支持解包)

其中yield语句我们还没学过。

# 1.5.2 可迭代对象到列表和元组的转换

可迭代对象转成列表和元组可以直接使用列表和元组的构造函数:

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> tuple(range(10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

当你有多个可迭代对象,而你希望把它们拼接在一起,那么解包语法也是一个不错的选择,但注意这么做会把可迭代对象的所有值取出,占耗大量的内存,如果你希望得到一个惰性的迭代器,可以使用itertools.chain()

>>> [*range(5), *range(3)]
[0, 1, 2, 3, 4, 0, 1, 2]
>>> import itertools
>>> list(itertools.chain(range(5), range(3)))
[0, 1, 2, 3, 4, 0, 1, 2]

# 1.5.3 可迭代对象的操作

可迭代对象是指能够一次返回一个元素的对象。可迭代对象包括序列如liststrtuplerange,也包括映射dict,还有enumerate等等。

通过调用内置函数iter()可以获取可迭代对象的迭代器。所谓迭代器,是能够通过内置函数next()不断获取下一个元素的对象,直到抛出StopIteration异常终止(异常我们以后会介绍)。迭代器本身也是可迭代对象,调用iter()会获得自己,这使得迭代器能出现在可迭代对象出现的地方。一般而言一个迭代器只能使用一遍。如果你需要多遍遍历,你需要用iter()获取多个全新的迭代器。我们可以看看示例代码:

>>> iterable = [1, 2]
>>> iterator = iter(iterable)
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> iterator = iter(iterable)
>>> iterator is iter(iterator)
True

原理

这部分内容可能对于初学者过于深奥。请谨慎食用。

iter()函数会尝试调用可迭代对象的iterable.__iter__()方法获得迭代器;如果这个方法不存在,它会试图创建一个迭代器,这个迭代器会,从0开始用整数调用iterable.__getitem__()(基本等价于下标)获取元素直到出现IndexError异常。

next()函数会调用迭代器的iterator.__next__()方法。

以下我们给出可迭代对象支持的操作,除了下面列出的操作,可迭代对象还支持解包和用for语句遍历:

操作 结果 来源
iter(iterable) 返回iterable的迭代器 Iterable要求的方法
min(iterable) iterable最小的元素 Iterable提供的方法
max(iterable) iterable最大的元素
sorted(iterable) 返回一个迭代器,是排好序的iterable,排序是stable的
sum(iterable, start=0) start开始左往右对所有数据求和
all(iterable) iterable所有元素是否都是真
any(iterable) iterable是否存在某个元素是真
enumerate(iterable, start=0) 返回一个迭代器,这个迭代器返回的元素是一个由序号(从start开始编号)和原可迭代对象的值组成的二元组,经常被用于for循环
map(function, iterable) 返回一个迭代器,这个迭代器返回的元素是传给function得到True的元素
filter(function, iterable) 返回一个迭代器,这个迭代器返回的是传给iterable元素给function得到的结果
zip(*iterables) 返回一个迭代器,这个迭代器依次同时取出所有可迭代对象的元素,组成一个元组返回,经常用于for循环中

下面给出一些例子:

>>> a = list(range(5, 10))  # 产生一个5到9的列表
>>> a
[5, 6, 7, 8, 9]
>>> import random
>>> random.shuffle(a)  # 随机打乱这个列表
>>> a
[5, 9, 7, 6, 8]
>>> min(a)
5
>>> max(a)
9
>>> sum(a)
35
>>> list(enumerate(a))  # 由于enumerate返回的是迭代器,所以需要list来转换为列表,下同
[(0, 5), (1, 9), (2, 7), (3, 6), (4, 8)]
>>> for i, v in enumerate(a):  # 这个是enumerate经常被使用的方式
...   print(str(i) + ': ' + str(v))
...
0: 5
1: 9
2: 7
3: 6
4: 8
>>> list(filter(lambda x: x > 6, a))  # 筛选出列表中大于6的元素
[9, 7, 8]
>>> list(map(lambda x: x * 2, a))  # 将列表中所有元素乘以2
[10, 18, 14, 12, 16]
>>> all(map(lambda x: x > 5, a))  # 测试是否所有元素都大于5
False
>>> any(map(lambda x: x > 5, a))  # 测试是否存在元素大于5
True
>>> list(zip(range(5, 10), range(10, 5, -1)))
[(5, 10), (6, 9), (7, 8), (8, 7), (9, 6)]

# 1.5.4 序列的操作

所谓序列就是那些能够通过整数索引元素s[i]、并能通过len()函数获取长度的对象,所有的序列对象一定是可迭代对象(在先前的继承图中你可以看到Sequence继承自Iterable)。可变的序列是普通序列的子类型,除继承得到的方法之外,更进一步支持了对索引赋值s[i] = value、删除索引del s[i]和插入元素s.insert(index, value)这一些操作。这些继承关系可以用下图表示。

graphviz image

这里这些概念有些复杂,我们把这些操作制作成表格,方便大家理解,而后我们给出示例代码。首先是序列的操作列表,listtuplestrbytesbytearrayrange都支持下表中的操作:

操作 结果 注释 来源
x in s 如果s的一个元素为x,则为True否则为False strbytesbytearray的一些序列使用in进行子串匹配 Sequence提供的方法
x not in s 如果s的一个元素为x,则为False否则为True
s + t 连接序列st,返回新的序列 这种连接如果需要执行多次会有较高的时间开销 某些序列拥有的额外方法,range之类的序列没有
s * nn * s 重复序列s n 如果n为负,则当做0处理返回空串,注意s中的对象发生的是浅拷贝
s[i] formula个元素,从0开始计数 如果formulaformula是负数,则分别等价于len(s) + ilen(s) + j,但-0仍是0 Sequence要求的方法
s[i:j] 找出所有下标formula满足formula
s[i:j:k] 找出所有下标formula满足formula
len(s) s的长度
iter(s) 返回s的迭代器 Sequence提供的方法
reversed(s) 返回s的反向迭代器 Sequence提供的方法
s.index(x[, i[, j]]) x出现在s中的第一次位置的下标,额外的参数基本等价于s[i:j].index(x),找不到会抛出ValueError异常
s.count(x) s中出现了x的总数

下面是一些示例代码:

>>> s, t = list(range(5)), list(range(5, 10))
>>> s
[0, 1, 2, 3, 4]
>>> t
[5, 6, 7, 8, 9]
>>> 5 in s
False
>>> 5 not in s
True
>>> s + t
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> s * 2
[0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
>>> s[-1]
4
>>> s[::-1]  # 一种颠倒序列的方式
[4, 3, 2, 1, 0]
>>> len(s)
5
>>> list(reversed(s))  # 另一种颠倒序列的方式
[4, 3, 2, 1, 0]
>>> s.index(3)
3
>>> s.count(1)
1

对于索引和切片下标值的含义,我们还是祭出下方的这个图,这是字符串Python的索引对应的位置。索引从0开始计数,索引6是个非法的索引,它越界了。此外还要注意切片是左闭右开的。

graphviz image

接着是可变序列的操作列表,listbytearray支持下表中的操作。

操作 结果 注释 来源
s[i] = x s中的第i个元素被替换为x MutableSequence要求的方法
s[i:j] = t sij(不包括j)的切片被替换为序列t
del s[i:j] 等价于s[i:j] = []
s[i:j:k] = t s[i:j:k]中的元素替换为序列t 切片和t的长度必须相等
del s[i:j:k] 移除s[i:j:k]中的元素
s.append(x) x添加到s的末尾,等价于s[len(s):len(s)] = [x] MutableSequence提供的方法
s.clear() 移除s中的所有元素,等价于del s[:] Python 3.3新加入的方法 某些可变序列拥有的额外方法
s.copy() 创建s的浅拷贝,等价于s[:]
s.extend(t)s += t 用序列t扩展s,等价于s[len(s):len(s)] = t MutableSequence提供的方法
s *= n 更新s的内容重复n 内容会被浅拷贝 某些可变序列拥有的额外方法
s.insert(i, x) x插入到s下表为i的地方,等价于s[i:i] = [x] MutableSequence要求的方法
s.pop([i]) 返回下表为i的元素并且移除它,默认i-1 MutableSequence提供的方法
s.remove(x) 移除第一个值等于x的元素,没找到时抛出ValueError异常
s.reverse() s中的元素倒过来

下面是示例代码:

>>> s = list(range(5))
>>> s
[0, 1, 2, 3, 4]
>>> s[0] = 5
>>> s
[5, 1, 2, 3, 4]
>>> s[1:3] = [2, 1]
>>> s
[5, 2, 1, 3, 4]
>>> s[::-1] = s  # 一种原处颠倒序列的方法,和 s[:] = s[::-1] 以及 s.reverse() 等价
>>> s
[4, 3, 1, 2, 5]
>>> del s[::2]  # 删除下标是偶数的元素
>>> s
[3, 2]
>>> s.append(6)
>>> s
[3, 2, 6]
>>> s += [7, 8]
>>> s
[3, 2, 6, 7, 8]
>>> s *= 2
>>> s
[3, 2, 6, 7, 8, 3, 2, 6, 7, 8]
>>> s.pop()
8
>>> s
[3, 2, 6, 7, 8, 3, 2, 6, 7]
>>> s.remove(8)
>>> s
[3, 2, 6, 7, 3, 2, 6, 7]
>>> s.reverse()
>>> s
[7, 6, 2, 3, 7, 6, 2, 3]

注意s * n以及s *= n都是进行浅拷贝,这在二维数组的时候会出现问题,看下面的例子:

>>> s = [[0]] * 3
>>> s
[[0], [0], [0]]
>>> s[0][0] = 1
>>> s
[[1], [1], [1]]

如果你想要避免这种情况,建议使用1.5.5 列表推导式。这里先给出代码:

>>> s = [[0] for _ in range(3)]
>>> s
[[0], [0], [0]]
>>> s[0][0] = 1
>>> s
[[1], [0], [0]]

此外list还提供了以下方法:

  • list.sort(key=None, reverse=False):将列表排序。key是一个函数,接受元素,返回排序的键,如果reverseTrue,改为由大到小排序,排序一定是stable的。

示例代码如下:

>>> fruits = ['orange', 'apple', 'banana']
>>> fruits_sorted = fruits[:]
>>> fruits_sorted.sort()  # 普通的排序方法
>>> fruits_sorted
['apple', 'banana', 'orange']
>>> fruits_with_index = list(enumerate(fruits))  # argsort的排序方法
>>> fruits_with_index
[(0, 'orange'), (1, 'apple'), (2, 'banana')]
>>> fruits_with_index.sort(key=lambda x: x[1])
>>> fruits_with_index
[(1, 'apple'), (2, 'banana'), (0, 'orange')]
>>> fruits_argsorted = list(map(lambda x: x[0], fruits_with_index))
>>> fruits_argsorted  # argsort排序完成,得到的结果就是数字。数组是从小到大各个元素的下标
[1, 2, 0]

原理

获取s[i](包括切片)都会调用s.__getitem__(i)s[i] = t会调用s.__setitem__(i, t),而del s[i]会调用s.__getitem__(i)。实际上如何处理i是负数、切片之类的完全有对象s掌管。如果i不是一个合适的类型,可以抛出TypeError异常;对于序列类型,如果i越界,可以抛出IndexError异常;对于映射类型,如果i不存在,可以抛出KeyError异常。

len(s)会调用s.__len__(),这个方法应当返回formula的整数。

reversed(s)会先尝试调用s.__reversed__()得到逆序迭代器,如果这个方法不存在,它会试图创建一个迭代器,这个迭代器会,从s.__len__() - 1一直递减到0,调用iterable.__getitem__()获取元素。

# 1.5.5 列表推导式

列表推导式是一种创建列表更简介的方式。让我们先以创建一系列的平方数为例:

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

可以看到这里变量x在循环结束之后依旧存在。我们可以用下面不带任何副作用的函数式写法:

squares = list(map(lambda x: x**2, range(10)))

但是这种写法可读性不是特别高,我们可以用下面的列表推导式的写法:

squares = [x**2 for x in range(10)]

列表推导式语法上是由一个中括号括住的里面包含一个表达式紧跟for子句而后可跟任意数目的forif子句。其结果就是一个新的列表,列表里面是在forif语句上下文中对表达式求值得到值。

举个例子,下面的列表推导式会结合两个列表中的元素,如果它们是不相同的:

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

它和下面的代码是等价的:

>>> combs = []
>>> for x in [1,2,3]:
...     for y in [3,1,4]:
...         if x != y:
...             combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

此外列表推导式也可以嵌套:

>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]
>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

这等价于:

>>> transposed = []
>>> for i in range(4):
...     transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

又进一步等价于:

>>> transposed = []
>>> for i in range(4):
...     # the following 3 lines implement the nested list comprehension
...     transposed_row = []
...     for row in matrix:
...         transposed_row.append(row[i])
...     transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

# 1.6 字典

字典属于可变映射类型,它可以存储键值对。

# 1.6.1 字典的字面量

字典的字面量是由一对花括号{},逗号分隔的键值对key: value组成,最后一个键值对的末尾可以有逗号,,这里keyvalue可以是任意表达式,但需要注意key表达式的结果一定是可哈希对象(hashable),想知道那些对象是可哈希对象可以查看本章开头的那张图,继承自Hashable的对象都是可哈希对象,所有的数学类型、元组、字符串、bytes是可哈希的。在Python标准库中不可变对象都是可哈希的,可变对象都不是可哈希的,但实际上可变和可哈希没有必然的联系。

可能大家还是不清楚什么叫“哈希”,哈希的意思是将对象的内容映射到一个整数,这种映射就像一个指纹,这对于哈希表这种数据结构是必须的,而哈希表在存储键值对的数据中有很优秀的性能。

>>> {'a': 1, 1: 'a', ('1', 1): 'a'}
{'a': 1, 1: 'a', ('1', 1): 'a'}
>>> {
...   'a': 1,  # 末尾可以跟逗号
... }
{'a': 1}
>>> {['1', 1]: 'a'}  # 列表不是可哈希对象
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> {}  # 空的字典
{}

提示

所谓可哈希对象必须满足两个条件:

  • hashable.__hash__()方法返回一个整数,这个整数在对象存在期间不会发生变化。该函数会被内置函数hash(object)调用;
  • hashable.__eq__(other)方法可以判断对象是否相等,对于相等的对象,它们的哈希值必须相等。该函数会被二元运算符==调用。

一旦定义了__hash__()方法,从逻辑上而言必须定义__eq__(other)方法。

对于一个用户自定义的类,默认就是有上述两个方法的,这个时候x == y,等价于xy就是同一个对象。且这时,xy拥有相同的哈希值即hash(x) == hash(y)

如果一个自定义的类覆盖了__eq__()方法,却没定义__hash__()方法,则其__hash__会隐式地成为None,这会使得它被hash()调用时返回TypeError。如果这个类是一个子类,它可以通过__hash__ = ParentClass.__hash__沿用父类的哈希方法。

如果一个自定义类想彻底禁用哈希,可以__hash__ = None

此外,字典的字面量中还允许字典的解包语法,这和函数的解包是一样的。

当字典的字面量包含重复的键的时候,后面的那个会起效。

>>> a = {'a': 0, 'b': 1}
>>> b = {**a, 'a': 1}  # 解包
>>> b
{'a': 1, 'b': 1}
>>> {1: 'a', 1.0: 'b'}  # 由于 1 == 1.0 ,后面的键值对会替代前面的
{1: 'b'}

从Python 3.7开始,映射的迭代顺序一定遵循插入顺序。

# 1.6.2 其他构造出字典的方式

dict是字典的构造函数。dict可以接受其他映射对象构造出一个新的字典,也可以从可迭代对象构造出新的字典,这个可迭代对象的元素是包含键和值的二元素可迭代对象。dict还接受关键字参数,可以给先前说的两种构造方式添加新的元素。

当出现重复的键时,后面的有效,由于关键字参数位于最后,所以关键字参数总归有效。我们来看例子:

>>> a = {'a': 0, 'b': 1}
>>> dict(a, a=1)
{'a': 1, 'b': 1}
>>> dict([('a', 0), ('b', 1), ('c', 2)])
{'a': 0, 'b': 1, 'c': 2}
>>> dict(zip(['a', 'b', 'c'], range(3)))
{'a': 0, 'b': 1, 'c': 2}

# 1.6.3 映射的操作

graphviz image

操作 结果 来源
d[k] 返回键k对应的值,如果该值不存在,抛出KeyError异常 Mapping要求的方法
len(d) 返回d中元素的个数
iter(d) 返回遍历d所有键的迭代器,等价于iter(d.keys())
d[k] = v d[k]设置为v,如果'k'存在会更新,不存在会插入 MutableMapping要求的方法
del d[k] 删除d[k],如果k存在会抛出KeyError异常
k in dk not in d 判断键k是否存在 Mapping提供的方法
d1 == d2d1 != d2 判断两个字典是否相等,只要有相同的键值对的字典就认为相同,这与插入顺序无关
d.keys() 返回字典键的视图,这是一个包含了字典所有键的集合,详见字典的试图
d.items() 返回字典键值对的试图,这是一个包含了(key, value)二元组的集合,详见字典的试图
d.values() 返回字典值的试图,这是一个包含了所有值的迭代器,注意d.values() == d.values()False
d.get(key, default=None) 类似d[key],只是如果该值不存在会返回default
d.pop(key[, default]) 如果key在字典中,移除它并返回,否则返回default,如果default也没指定,抛出KeyError异常 MutableMapping提供的方法
d.popitem() 返回(key, value)二元组键值对,并移除它,如果字典空会抛出KeyError,对于字典,最后插入的会最先pop出来
d.clear() 移除字典中所有的元素
d.update([other, ]**kwargs) 和[其他构造出字典的方式]#_1-6-2-其他构造出字典的方式)类似,只是会保留或者更新字典中的键值对,other可以是映射或者包含键值对的可迭代对象,也可以携带关键字参数
d.setdefault(key, default=None) 等价于d.get(key, default),如果key不存在,会执行d[key] = default
d.copy() 返回字典的一个浅拷贝 字典对象的额外方法

同样的,对于上述操作,我们会给出示例,置于字典的视图,更详细的示例代码会在字典的试图那一节给出。

>>> d = {'a': 1, 'b': 2}
>>> len(d)
2
>>> list(d)  # 等价于list(d.keys())
['a', 'b']
>>> d['c'] = 3
>>> d
{'a': 1, 'b': 2, 'c': 3}
>>> del d['a']
>>> d
{'b': 2, 'c': 3}
>>> 'a' in d
False
>>> d == {'c': 3, 'b': 2}  # 可以看出顺序并不影响是否相等
True
>>> list(d.items())
[('b', 2), ('c', 3)]
>>> list(d.values())
[2, 3]
>>> d.get('d', 4)
4
>>> d.pop('c', 5)
3
>>> d
{'b': 2}
>>> d.update([('a', 1), ('c', 3)])
>>> d.popitem()  # 最后插入的是('c', 3),所以它被pop了出来
('c', 3)
>>> d
{'b': 2, 'a': 1}
>>> d.clear()
>>> d
{}
>>> d.setdefault('a', 0)
0
>>> d
{'a': 0}
>>> d.copy()
{'a': 0}

原理

对于字典d[k]的操作,如果字典的子类型实现了d.__missing__(key)的方法,当k找不到的时候就会调用并返回d.__missing__(k)

# 1.6.4 字典的视图

dict.keys()dict.values()dict.items()返回的是视图对象。它们是动态的,也就是说如果字典发生了变化,这些视图也会发生变化。下面这张图是上面图的子图,显示了各个视图对象的关系。

graphviz image

所有的视图都支持下列操作:

操作 结果 来源
len(view) 得到字典元素的个数 MappingView提供的方法
iter(view) 按照插入顺序遍历,边遍历边插入删除元素会抛出RuntimeError ItemsViewKeysViewValuesView提供的方法
x in view 判断x是否是键、值或键值对

dict.keys()返回的对象类似集合,而如果字典的值也是可哈希对象,dict.items()返回的对象也类似集合。这里类似集合是指可以用判断是否相等的运算(==!=)、判断子集的运算(<<=>>=)、交(&)、并(|)、差(-)、对称差(^)和isdisjoint()。具体集合的操作可以查看1.7 集合和frozenset

下面是例子:

>>> dishes = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500}
>>> keys = dishes.keys()
>>> values = dishes.values()

>>> # 迭代,会调用iter(values)
>>> n = 0
>>> for val in values:
...     n += val
>>> print(n)
504

>>> # keys和values按照同样的顺序(插入顺序)
>>> list(keys)
['eggs', 'sausage', 'bacon', 'spam']
>>> list(values)
[2, 1, 1, 500]

>>> # 视图对象是动态的,会反映字典的变化
>>> del dishes['eggs']
>>> del dishes['sausage']
>>> list(keys)
['bacon', 'spam']

>>> # 集合操作
>>> keys & {'eggs', 'bacon', 'salad'}
{'bacon'}
>>> keys ^ {'sausage', 'juice'}
{'juice', 'sausage', 'bacon', 'spam'}

最后我指出一下,其实视图对象用得最多的地方是for循环。如果你只需要遍历键,就可以for k in d:d是个字典)或者for k in d.keys():;如果只需要遍历值,就可以for v in d.values():;如果需要遍历键和值,就可以for k, v in d.items():

# 1.6.5 字典推导式

与列表类似,字典也有推导式。字典的推导式语法与列表类似,只是使用花括号{}括起,单个表达式也变成了:分隔的两个表达式:

>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}

# 1.7 集合和frozenset

集合是一序列互不相同的可哈希对象组成的无序容器,它们可以被用于测试in关系(比一般的序列快),去重,以及进行交并补计算。setfrozenset的差别在于前者是可变的、不可哈希的,而frozenset是不可变的、可哈希的。

# 1.7.1 集合字面量

集合的字面量和字典的字面量比较类似,都是用一对花括号{}括起的,并用逗号分隔的。不同之处是字典是键值对key: value组成的一对表达式,而集合则只包含一个表达式:

>>> {1, 3, 2}          # 集合元素迭代的顺序不一定遵循插入顺序
{1, 2, 3}
>>> {1, 1, 3.0, 3, 2}  # 集合会去除掉重复的元素
{1, 2, 3.0}

集合字面量同样支持可迭代对象的解包:

>>> a = [1, 2, 3]
>>> b = [2, 3, 4]
>>> {*a, *b}
{1, 2, 3, 4}

# 1.7.2 可迭代对象构与集合和frozenset之间的转换

集合和frozenset本身就是可迭代对象,这意味着你获取字典和frozenset的迭代器、将他们转换成列表或元组、用for循环遍历,具体支持的操作可以看1.5.3 可迭代对象的操作,当然仍需要指出集合和frozenset迭代的顺序不一定遵循插入顺序

>>> a = {1, 3, 2}
>>> [2 * i for i in a]
[2, 4, 6]
>>> tuple(a)
(1, 2, 3)

另一方面集合setfrozenset的构造函数可以接受一个可迭代对象,构建出新的集合和frozenset对象,其中重复的元素会被消除掉。

>>> a = [1, 2, 3, 2, 1]
>>> set(a)
{1, 2, 3}
>>> frozenset(a)
frozenset({1, 2, 3})

所以这两个操作和在一起,先把可迭代对象转成集合,再从集合转换为可迭代对象可以成功地消除重复的元素:

>>> a = [1, 2, 3, 2, 1]
>>> list(set(a))         # 一个典型的去重操作
[1, 2, 3]

# 1.7.3 集合和frozenset的操作

graphviz image

从上图我们可以看到集合和frozenset的继承关系,首先我们可以看到所有集合都共有的一些操作:

操作 结果 来源
len(s) s的元素个数 Set要求的方法
x in s / x not in s 测试s是否包含/不包含x
set.isdisjoint(other) 返回两个集合是否没有相同的元素 Set提供的方法
set <= other_set / set < other_set / set >= other_set / set > other_set 测试set是否是other_set的子集/真子集/超集/真超集
set | other_set / set & other_set / set - other_set / set ^ other_set 返回setother_set的并集/交集/差集/对称差
issubset(other_iterable) / issuperset(other_iterable) 几乎等价于set <= other/set >= other setfrozenset特有的方法
set.union(*other_iterables) / set.intersection(*other_iterables) / set.difference(*other_iterables) 几乎等价于set | other | .../set & other & .../set - other - ...
set.symmetric_difference(other_iterable) 几乎等价于set ^ other
set.copy() 返回set的浅拷贝

这里需要指出交、并、差、对称差这4个操作和子集、超集都有对应的两个版本:运算符和方法。这两个我们说是“几乎”等价的,差别在于运算符版要求操作数必须同样是集合类型的,而方法则可以接受可迭代对象,除此之外,某些方法还接受任意多参数。下面我们即将讲到的可变集合操作也类似。

集合和frozenset之间是可以混合运算的,所以我们有set('abc') == frozenset('abc')。对于交、并、差、对称差,如果存在混合操作,返回的类型是第一个操作数的类型。

这里我们需要指出,集合之间的<>==并不组成全序关系,也就是说可能存在两个集合aba < ba > ba == b都为假,这和我们学习到的实数之类的序的关系是不一样的。

集合的交、并和对称差&|^可能对于新手不那么容易记,这3个符号来自于C系语言位运算,和集合运算也恰恰是对应上的。之后我们会在表达式中再次遇到这3个运算符作用在整数上的情况。

接下来我们来看看上面运算的例子:

>>> a = {1, 2, 3}
>>> b = {2, 3, 4}
>>> c = {1, 2, 3, 4}
>>> len(c)
4
>>> a.isdisjoint(b)
False
>>> a < c                 # 真子集
True
>>> a & b                 # 交
{2, 3}
>>> a ^ b                 # 对称差
{1, 4}
>>> c.intersection(a, b)  # a、b和c的交集
{2, 3}

上面所讲的是普通集合,也就是集合和frozenset都支持的操作。下面我们来讲可变集合,也就是集合set支持的操作。

操作 结果 来源
set |= other_set / set &= other_set / set -= other_set / set ^= other_set 等价于set = set | other_set(并)/set = set & other_set(交)/set = set - other_set(差)/set = set ^ other_set(对称差) MutableSet提供的方法
set.update(*other_iterables) / set.intersection_update(*other_iterables) / set.difference_update(*other_iterables) 等价于set = set.union(*other_iterables)/set = set.intersection(*other_iterables)/set = set.difference(*other_iterables) set特有的方法
set.symmetric_difference_update(other_iterable) 等价于set =set.symmetric_difference(other_iterable)
set.add(elem) set中添加元素elem MutableSet要求的方法
set.discard(elem) set中移除元素elem,如果elem不存在不报错
set.remove(eleme) set中移除元素elem,如果elem不存在则抛出KeyError错误 MutableSet提供的方法
set.pop() 移除一个元素并返回它
set.clear() 移除集合中的所有元素

同样地,我们来看一下例子:

>>> a = {1, 2, 3}
>>> b = {4, 5}
>>> a |= b
>>> a
{1, 2, 3, 4, 5}
>>> a.add(7)
>>> a.discard(6)
>>> a.remove(6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 6
>>> a.pop()
1
>>> a
{2, 3, 4, 5, 7}
>>> a.clear()
>>> a
set()

# 1.7.4 集合推导式

集合的推导式和列表的很像,只是使用了花括号括起:

>>> {x**2 for x in (2, 4, 6)}
{16, 4, 36}

# 1.8 复数

Python有内置对复数的支持。

Python提供了构造纯虚数的方法,通过在一个整数或浮点数后面添加jJ就能产生实部为0,虚部为该数的纯虚数。这里要注意单位虚数formula不是写作j,而是1j。同时复数也有构造函数,分别接受实部和虚部,构造新的复数。

>>> 1 + 1j  # 产生复杂复数的方法
(1+1j)
>>> complex(1, 2)
(1+2j)

复数支持加减乘除和次方运算,此外还有一些额外的操作,见下面的例子:

>>> a = 1 + 2j
>>> abs(a)  # 求模
2.23606797749979
>>> a.real  # 取实部
1.0
>>> a.imag  # 取虚部
2.0
>>> a.conjugate() # 求共轭复数

更多的复数操作在cmath中,比如获取复数的幅角,计算附属的指对、三角等函数。

# 2 习题

# 2.1 A+B问题

# 题目要求

输入两个浮点数,输出它们的和,你可以有适当的输出提示来让用户输入浮点数和显示结果。

# 样例输入输出

Please input the first real number: 1.2
Please input the second real number: 2.3
1.2 + 2.3 = 3.5

# 提示

# 答案

点击查看答案
if __name__ == '__main__':
    a = float(input('Please input the first real number: '))
    b = float(input('Please input the second real number: '))
    print(str(a) + ' + ' + str(b) + ' = ' + str(a + b))

# 2.2 判断正负

# 题目要求

输入一个浮点数,输出它是正数、负数还是0。

# 样例输入输出

Please input a real number: -1
-1.0 is a negative number

# 提示

# 答案

点击查看答案
if __name__ == '__main__':
    number = float(input('Please input a real number: '))
    if number > 0.0:
        print(str(number) + ' is a positive number')
    elif number < 0.0:
        print(str(number) + ' is a negative number')
    else:
        print(str(number) + ' is zero')

# 2.3 最大值与最小值

# 题目要求

输入一系列的浮点数,直到输入为0停止,输出他们的最大值最小值平均数和和。

# 样例输入输出

Please input a real number (input 0 to stop): 2
Please input a real number (input 0 to stop): 1
Please input a real number (input 0 to stop): 3
Please input a real number (input 0 to stop): 0
The maximum number is 3.0
The minimum number is 1.0
The average number is 2.0
The sum number is 6.0

# 提示

# 答案

点击查看答案

对于这道题,我们有两种解法。第一种是存下所有的数字,这种方法的代码较为简短,这里需要额外指出的是如果是空的序列min()max()会报错,平均数也有可能除零异常

if __name__ == '__main__':
    numbers = []
    while True:
        number = float(input('Please input a real number (input 0 to stop): '))
        if number == 0:
            break
        numbers.append(number)
    if numbers:
        print('The maximum number is ' + str(max(numbers)))
        print('The minimum number is ' + str(min(numbers)))
        sum_of_numbers = sum(numbers)
        print('The average number is ' + str(sum_of_numbers / len(numbers)))
        print('The sum number is ' + str(sum_of_numbers))
    else:
        print('You entered a empty sequence')

另一方式就是,只存放最大值、最小值和和。这样空间性能会比较好,如果有大量的甚至超出内存承受范围的浮点数需要输入,也不会出现问题。

import math

if __name__ == '__main__':
    max1, min1, sum1, n = -math.inf, math.inf, 0.0, 0
    while True:
        number = float(input('Please input a real number (input 0 to stop): '))
        if number == 0:
            break
        max1 = max(max1, number)
        min1 = min(min1, number)
        sum1 += number
        n += 1
    if n:
        print('The maximum number is ' + str(max1))
        print('The minimum number is ' + str(min1))
        print('The average number is ' + str(sum1 / n))
        print('The sum number is ' + str(sum1))
    else:
        print('You entered a empty sequence')

# 2.4 判断回文串

# 题目要求

输入一行字符串,判断是不是回文字符串(正过来和倒过来都一样的字符串),如abcdcba就是回文数。

# 样例输入输出

Please input a line: aba
It is a palindrome

# 提示

# 答案

点击查看答案

这道题我们也有几种解法,第一种解法非常简单,使用反向的切片就可以得到原来字符串反过来的字符串。

if __name__ == '__main__':
    line = input('Please input a line: ')
    if line == line[::-1]:
        print('It is a palindrome')
    else:
        print('It is not a palindrome')

但是我们会发现这个实现中创建了一个临时字符串,所以第一种改进方法是不再创建这个临时字符串:

if __name__ == '__main__':
    line = input('Please input a line: ')
    for a, b in zip(line, reversed(line)):
        if a != b:
            print('It is not a palindrome')
            break
    else:
        print('It is a palindrome')

最后我们还会发现回文字符串多比较了几次,所以稍加改动性能就可以翻倍,这里只改动了第3行:

if __name__ == '__main__':
    line = input('Please input a line: ')
    for a, b, _ in zip(line, reversed(line), range(len(line) // 2)):
        if a != b:
            print('It is not a palindrome')
            break
    else:
        print('It is a palindrome')

# 2.5 统计字符数目

# 题目要求

输入一行字符串,统计英文字符(包括大小写)和数字字符的个数。

# 样例输入输出

Please input a line: a1h45
Number of alphas is 2
Number of digits is 3

# 提示

# 答案

点击查看答案
if __name__ == '__main__':
    line = input('Please input a line: ')
    n_alpha, n_digit = 0, 0
    for char in line:
        if char.isalpha():
            n_alpha += 1
        elif char.isdigit():
            n_digit += 1
    print('Number of alphas is ' + str(n_alpha))
    print('Number of digits is ' + str(n_digit))

# 2.6 统计单词数目

# 题目要求

输入一行字符串,字符串包含若干单词(单词中不包含空白符),单词之间有空白符,统计每个单词出现的个数,并将所有的单词按照字母表顺序排列出来:

# 样例输入输出

Please input a line: what the fuck does what the fuck mean
does: 1
fuck: 2
mean: 1
the: 2
what: 2

# 提示

  • 你应当使用字典之类的类型存储个数;
  • 你可以使用sorted()函数或list.sort()方法。

# 答案

点击查看答案
if __name__ == '__main__':
    line = input('Please input a line: ')
    words = line.split()
    counter = {}
    for word in words:
        counter[word] = counter.get(word, 0) + 1
    for key, value in sorted(counter.items()):
        print(str(key) + ': ' + str(value))

# 2.7 考拉兹猜想

# 题目要求

考拉兹猜想,又称为奇偶归一猜想、3n+1猜想、冰雹猜想、角谷猜想、哈塞猜想、乌拉姆猜想或叙拉古猜想,是指对于每一个正整数,如果它是奇数,则对它乘3再加1,如果它是偶数,则对它除以2,如此循环,最终都能够得到1。(来自维基百科)。

formula

我们的目标是输入一个整数,输出考拉兹猜想对应的序列,如输入为6,序列就是6,3,10,5,16,8,4,2,1。

# 提示

  • 你需要熟悉一下数学运算。

# 样例输入输出

Please input a number: 6
6
3
10
5
16
8
4
2
1

# 答案

点击查看答案
if __name__ == '__main__':
    num = int(input('Please input a number: '))
    while True:
        print(num)
        if num == 1:
            break
        elif num % 2 == 0:
            num //= 2
        else:
            num = 3 * num + 1

# 2.8 快速幂

# 题目要求

Python内置的**运算符其实就是快速幂的一种实现。快速幂是求一个数的整数次方时采用的算法,它的计算方法来源于这个式子:

formula

你的目标是输入两个整数formulaformula,求formula而不使用**运算符。我们强烈建议初学者使用递归来解决,这里给出递归的模板代码:

def power(x, n):
    # Your need to call power inside this function
    pass


if __name__ == '__main__':
    x = int(input('Please input the base: '))
    n = int(input('Please input the exponent: '))
    print(str(x) + '^' + str(n) + ' = ' + str(power(x, n)))

# 样例输入输出

Please input the base: 2
Please input the exponent: 5
2^5 = 32

# 答案

点击查看答案
def power(x, n):
    if n == 0:
        return 1
    elif n % 2:
        return x * power(x * x, (n - 1) // 2)
    else:
        return power(x * x, n // 2)


if __name__ == '__main__':
    x = int(input('Please input the base: '))
    n = int(input('Please input the exponent: '))
    print(str(x) + '^' + str(n) + ' = ' + str(power(x, n)))

# 2.9 统计新出现的单词

# 题目要求

输入两行字符串,这两行字符串包含若干单词(单词中不包含空白符),单词之间有空白符,输出那些出现在第二行但不出现在第一行单词。

# 样例输入输出

Please input the first line: how are you today
Please input the second line: how old are you
old

# 2.10 绘制曼德博集合

# 题目要求

在做这个项目之前你需要执行下面的命令,来安装两个依赖:

pip install numpy matplotlib

曼德博集合formula是一个复平面上美丽的分形图形。我们的目标就是用Python绘制这样一个图形。它的定义是这样的:对于一个复数formula,构建一个数列:

formula

如果数列formula的绝对值formula是发散的,那我们就说formula

在我们的这次题目中,问题被简化。我们不太可能迭代无穷次来判断数列是否发散。此外我们发现只要存在formula使formula,那么formula。所以,总而言之,你需要做的就是完善下面代码中的mandelbrot函数。你需要最多迭代MAX_ITER次,计算出formula,一旦遇到某个formula,就返回formula,如果始终没有遇到,就返回MAX_ITER

import numpy as np
import matplotlib.pyplot as plt

MAX_ITER = 256


def mandelbrot(c):
    # TODO
    pass


def main():
    resolution = 512
    c = np.linspace(-2, 1, resolution)[np.newaxis, :] \
        + 1j * np.linspace(-1.5, 1.5, resolution)[:, np.newaxis]
    result = np.vectorize(mandelbrot)(c)
    plt.imshow(np.log(result + 1))
    plt.show()


if __name__ == '__main__':
    main()

# 样例输出

Mandelbrot Set

2016-2020 Ziping Sun
京ICP备 17062397号