【Python学习笔记】 第5章 数值类型

数值类型基础知识

在Python中,数值实际上是一种类型的分类,包括:

  • 整数、浮点数

  • 复数

  • 小数:固定精度对象

  • 分数:有理数对象

  • 集合:带有数值运算的集合体

  • 布尔值:True, False

  • 内置函数与块:round,math,random

  • 表达式、无限制精度整数(很大的整数)、位运算、八进制、十二进制、二进制

  • 第三方扩展:向量、库、可视化、作图等
    其中整数、浮点数最为常见,因此先从它们讲起。

数值字面量

字面量 解释
1234, -24, 0, 99999999999999 整数(无大小限制)
1.23, 1., 3.14e-10, 4E210, 4.0e+210 浮点数
0o177, 0x9ff, 0b101010 Python 3.X中八进制、十六进制、二进制字面量
0177, 0o177, 0x9ff, 0b101010 Python 2.X中两种八进制、十六进制、二进制字面量
3+4j, 3.0+4.0j, 3J 复数字面量
set('spam'), {1, 2, 3, 4} 集合
Decimal('1.0'), Fraction(1, 3) 小数、分数扩展
bool(X), True, False 布尔类型、字面量

注意:

  • 浮点数带小数点,或加上科学技术标志e/E。如果字面量带有上述描述,那么将其识别为浮点数。

  • 整数:在Python2中有32位整数和长整数(无限制长),Python3中只有整数,即无限长的整数。

  • 十六进制、八进制和二进制字面量:八进制以0o0O开头,十六进制以0x开头,二进制以0b0B开头。

  • 复数:字面量位实部+虚部,虚部以jJ结尾。

内置数值工具

处理数字的工具包括:

  • 表达式运算符

  • 内置数学函数

  • 工具模块
    它们也有一些专用于特定类型的方法。

Python表达式运算符

表达式的定义:数字/变量与运算符相结合,在执行时计算为一个值。

Python所有的运算符表达式如下:

运算符 描述
yield x 生成器函数send协议
lambda args: expression 创造匿名函数(lambda表达式)
x if y else z 三元选择表达式,如果y为真则取x,否则取z
x or y 逻辑或
x and y 逻辑与
not x 逻辑非
x in y, x not in y x是/不是y的成员
x is y, x is not y `对象是否同一(不止是值相等)
x < y, x > y, x <= y, x >= y x是否小于/大于/不大于/不小于y
x == y, x != y x等于/不等于y(和上面的is不一样)
x | y 如果是数字,则按位与;如果是集合,则取并集
x ^ y 如果是数字,则按位异或;如果是集合,则取对称差集
x & y 如果是数字,则按位与;如果是集合,则取交集
x << y, x >> y x左移/右移y
x + y 如果是数字,则加;如果是序列,则拼接
x - y 如果是数字,则减;如果是集合,则取差集
x * y 如果是数字,则乘;如果是序列,则重复
x % y 如果是数字,则取x除以y的余数;否则是格式化
x / y, x // y 除法、除法向下取整
-x, +x 取负、取正
~x 按位非(取反码)
x ** y xy次幂
x[i] 索引
x[i:j:k] 分片
x(...) 调用函数/方法/类等等
x.attr 属性引用
(...) 元组、表达式、生成器表达式
[...] 列表、列表推导
{...} 字典、集合、字典与集合推导

混合运算遵循运算符优先级

  • 以上表为标准,运算符从上到下优先级逐渐升高;

  • 如果优先级相同,则从左到右运算。

括号分组子表达式

如果我们使用了括号,就不用考虑优先级,因为Python优先计算括号内的子表达式,再计算整个表达式。

混合类型向上转换

如果表达式中有复数,那么结果就为复数;如果表达式中只有整数和浮点数,则结果为浮点数;如果表达式中只有整数,则结果为整数(除非有除法)。

我们也可以通过强制转换的方法选择想要的类型:

>>> int(3.1415)
3
>>> float(3)
3.0

预习:运算符重载和多态

我们在之前也看见,+也适用于字符串拼接。这是因为,字符串类型对+进行了重载。用户定义的新类型也可以重载+

Python的这个特性叫多态。

数字的实际应用

变量与基础表达式

变量是名称,用于记录程序中的信息。在Python中:

  • 变量在第一次赋值时被创建

  • 变量在表达式中被使用时,会被替换为它们的值

  • 变量在表达式中使用之前,必须已经赋值

  • 变量引用对象,不需要事先声明
    例:下面的例子中,自动创建变量ab

>>> a = 3
>>> b = 4

在表达式中使用刚刚创建的变量,此时变量替换为它们的值:

>>> a + 1, a - 1
(4, 2)
>>> b * 3, b / 2
(12, 2.0)
>>> a % 2, b ** 2
(1, 16)
>>> 2 + 4.0, 2.0 ** b
(6.0, 16.0)

注意到,我们输入了多个表达式并用逗号分隔它们,这样Python对每个表达式分别求值,并将它们用元组的方式组合起来。

如果使用还没创建的变量,则Python会报错:

>>> c * 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

Python解释数值表达式的时候,计算的先后顺序与数学上的优先顺序一致(如先乘除后加减,如果有括号先算括号)

数值的显示格式

在早一些版本,会出现:

>>> b / (2.0 + a)
0.80000000000000004
>>> print(b / (2.0 + a))
0.8

后面的版本已经解决了这个问题。我们也可以用其他方法显示计算机中数字的位数(比如直接在交互界面输入变量名,或者使用字符串格式化):

>>> num = 1 / 3.0
>>> '%e' % num
'3.333333e-01'
>>> '%4.2f' % num
'0.33'
>>> '{0:4.2f}'.format(num)
'0.33'

普通比较与链式比较

普通数值可以进行比较,并返回一个布尔值。

>>> 2.0 >= 1
True
>>> 2.0 != 2.0
False

但是,一次比较只能比较两个数,因此Python运行我们把多个比较连接起来:

>>> X = 2
>>> Y = 4
>>> Z = 6
>>> X < Y < Z
True
>>> X < Y and Y < Z
True

实际上,前者更有效率,因为Python只计算一次Y。

比较链可以是任意形式,也可以是任意长度。

>>> X < Y > Z
False
>>> 1 < 2 < 3.0 < 4
True

如果需要判断表达式是否相等,且用到浮点数,那么对其额外处理(如判断两者的差的绝对值是否小于一个很小的数):

>>> 0.1 + 0.2 == 0.3
False
>>> 0.1 + 0.2
0.30000000000000004

除法、经典除法、向下取整除法和真除法

  • x / y:在Python2.X中,整数取省去小数部分,浮点数保留余项;在Python3.X中,无论用何种类型,都会变成真除法(即保留小数部分)。

  • x // y:向下取整除法。

>>> 10 / 4
2.5
>>> 10 / 4.0
2.5
>>> 10 // 4
2
>>> 10 // 4.0
2.0

可见,//依赖于操作数的类型。

支持两个Python脚本

在Python 2.X中,要实现像Python 3.X的除法效果,需要从模块__future__导入division

向下取整除法 vs 截断除法

除法//是向下截断除法。对浮点数采用向下截断的函数是math.floor,向零截断的函数是math.trunc,效果如下:

>>> import math
>>> math.floor(2.5)
2
>>> math.floor(-2.5)
-3
>>> math.trunc(2.5)
2
>>> math.trunc(-2.5)
-2

在Python中,截断除法//总是向下取整。

>>> 5 / 2, 5 / -2
(2.5, -2.5)
>>> 5 // 2, 5 // -2
(2, -3)
>>> 5 / 2.0, 5 / -2.0
(2.5, -2.5)
>>> 5 // 2.0, 5 // -2.0
(2.0, -3.0)

为什么截断很重要

Python 3.X中的非截断行为会严重影响大量的Python 2.X程序。

整数精度

Python 3.X支持无限制的整数大小,但Python 2.X识别长整数时在末尾加一个L。这方便了高精度运算(在C/C++需要额外实现)。

复数

复数的后缀为jJ,我们可以把实部非0的复数写成实部与虚部相加的形式。比如,实部为2,虚部为-3的复数可以写为2 + -3j2 - 3j

复数运算的例子:

>>> 1j * 1J
(-1+0j)
>>> 2 + 1j * 3
(2+3j)
>>> (2 + 1j) * 3
(6+3j)

复数允许访问实部和虚部,并支持所有数学表达式和cmath模块中的数学函数。

十六进制、八进制和二进制:字面量与转换

在Python中,编写以0o0O为开头的字面量,则将其识别为八进制;以0x0X为开头的字面量,则将其识别为十六进制;以0b0B为开头的字面量,则将其识别为二进制。

>>> 0o1, 0o20, 0o377
(1, 16, 255)
>>> 0x01, 0X10, 0xFF
(1, 16, 255)
>>> 0b1, 0B10000, 0b11111111
(1, 16, 255)

Python默认用十进制显示数值。如果要将一个数转换为八进制、十六进制和二进制,可以分别用octhexbin

>>> oct(64), hex(64), bin(64)
('0o100', '0x40', '0b1000000')

内置函数int将一个(十进制、八进制、十六进制、二进制)数字字符串转换为一个整数,第一个参数是要转换的字符,第二个参数是要把这个数字字符串看作是几进制,不填则默认为十进制。

>>> int('64'), int('100', 8), int('40', 16), int('1000000', 2)
(64, 64, 64, 64)
>>> int('0x40', 16), int('0b1000000', 2)
(64, 64)

eval函数把字符串作为Python代码运行。

字符串格式化方法和表达式把整数转换为指定的字符串:

>>> '{0:o}, {1:x}, {2:b}'.format(64, 64, 64)
'100, 40, 1000000'
>>> '%o, %x, %x, %X' % (64, 64, 255, 255)
'100, 40, ff, FF'

注意,在Python3.X中用0开头的数字表示八进制会引发错误。其次,八进制、十六进制和二进制都可以是任意长度的整数。

>>> X = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
>>> X
25711008708143844408671393477458601640355247900524685364822015
>>> oct(X)
'0o77777777777777777777777777777777777777777777777777777777777777777777'
>>> bin(X)
'0b111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'

按位操作

Python支持C语言中的大多数数学表达式,比如把整数作为二进制串处理的运算(位运算)。如:左移、右移、逻辑与/非/或/异或。

>>> x = 1   # 0001
>>> x << 2  # 0100
4
>>> x | 2   # 0001 | 0010 = 0011
3
>>> x & 1   # 0001 & 0001 = 0001
1

在位运算下,之前介绍的二进制、十六进制就十分有用(因为十六进制的每一位对应二进制的四位)。

Python引入了新的整数方法bit_length,查询二进制表示一个数字的值时至少需要的位数。

>>> X = 99
>>> bin(X), X.bit_length(), len(bin(X)) - 2
('0b1100011', 7, 7)

事实上,位运算在Python中不太重要。

其他内置数学工具

内置模块math

>>> import math
>>> math.pi, math.e                     # 数学常数
(3.141592653589793, 2.718281828459045) 
>>> math.sin(2 * math.pi / 180)         # 三角函数
0.03489949670250097
>>> math.sqrt(144), math.sqrt(2)        # 算术平方根
(12.0, 1.4142135623730951)
>>> pow(2, 4), 2 ** 4, 2.0 ** 4.0       # 幂运算
(16, 16, 16.0)
>>> abs(-42.0), sum((1, 2, 3, 4))       # 绝对值、和
(42.0, 10)
>>> min(3, 1, 2, 4), max(3, 1, 2, 4)    # 序列的最小值、最大值
(1, 4)

模块math包含多种取整方式:

>>> math.floor(2.567), math.floor(-2.567)       # 向下取整
(2, -3)
>>> math.trunc(2.567), math.trunc(-2.567)       # 向零取整
(2, -2)
>>> int(2.567), int(-2.567)
(2, -2)
>>> round(2.567), round(2.467), round(2.567, 2) # 四舍五入
(3, 2, 2.57)

注意到,优先函数如sin需要导入math,有些则不用。因为这些函数是内置函数,位于隐秘的命名空间内,对应于Python的builtins模块。

随机数random也需要导入。它可以选择在区间\([0, 1]\)的任意实数:

>>> import random
>>> random.random()
0.83057242993689
>>> random.random()
0.36589352300294087
>>> random.random()
0.9102553345613595

也可以在规定的范围内选择一个随机整数:

>>> random.randint(1, 10)
3
>>> random.randint(1, 10)
9
>>> random.randint(1, 10)
8

还能够从一个序列中随机地选取一项,以及打乱元素:

>>> random.choice(['Life of Brian', 'Holy Grail', 'Meaning of Life'])
'Holy Grail'
>>> random.choice(['Life of Brian', 'Holy Grail', 'Meaning of Life'])
'Meaning of Life'
>>>
>>> suits = ['hearts', 'clubs', 'diamonds', 'spades']
>>> random.shuffle(suits)
>>> suits
['clubs', 'diamonds', 'spades', 'hearts']
>>> random.shuffle(suits)
>>> suits
['spades', 'clubs', 'hearts', 'diamonds']

其他数值类型

小数类型

小数对象的名称是Decimal,它们有固定的位数和小数点,它的精度是固定的。

基础知识

用浮点数来进行运算容易丢失精度:

>>> 0.1 + 0.2
0.30000000000000004

使用了小数对象,则结果更精确:

>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')

我们用decimal模块中的Decimal函数创建小数对象,传入一个表示小数的字符串。当不同精度的小数组合,则按照精确度最高的小数给出结果:

>>> Decimal('0.1') + Decimal('0.10')
Decimal('0.20')

我们可以从一个浮点数创建小数对象,但它会产生默认庞大的小数位数。

>>> Decimal(0.1) + Decimal(0.2)
Decimal('0.3000000000000000166533453694')

设置全局小数精度

默认的小数精度是:小数部分保留28位有效数字。

>>> import decimal
>>> decimal.Decimal(1) / decimal.Decimal(7)
Decimal('0.1428571428571428571428571429')

我们可以这么修改显示小数部分有效数字的位数:

>>> decimal.getcontext().prec = 4
>>> decimal.Decimal(1) / decimal.Decimal(7)
Decimal('0.1429')
>>> decimal.Decimal(0.1) + decimal.Decimal(0.2) - decimal.Decimal(0.3)
Decimal('1.110E-17')

小数上下文管理器

我们可以把特定的精度要求放在一个环境里,在环境外还是默认精度:

>>> decimal.Decimal('1.00') / decimal.Decimal('3.00')
Decimal('0.3333333333333333333333333333')
>>> with decimal.localcontext() as ctx:
...     ctx.prec = 2
...     decimal.Decimal('1.00') / decimal.Decimal('3.00')
...
Decimal('0.33')
>>> decimal.Decimal('1.00') / decimal.Decimal('3.00')
Decimal('0.3333333333333333333333333333')

分数类型

分数基础知识

向上面的Decimal,分数也需要导入模块fraction,并通过两个整数(第一个是分子,第二个是分母)作为参数构造:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> y = Fraction(4, 6)
>>> x
Fraction(1, 3)
>>> y
Fraction(2, 3)
>>> print(y)
2/3

我们可以把分数用于表达式运算符:

>>> x + y
Fraction(1, 1)
>>> x - y
Fraction(-1, 3)
>>> x * y
Fraction(2, 9)

分数和小数的数值精度

分数和小数的运算都比浮点数更直观、准确。而且,分数会自动简化结果。

>>> Fraction(6, 12)
Fraction(1, 2)

分数转换和混用类型

浮点数有一个方法,可以将本身转化为分子分母组成的元组。

>>> (2.5).as_integer_ratio()
(5, 2)

然后转换为分数,这里*表示将元组的每个元素分别作为函数的参数。

>>> f = 2.5
>>> z = Fraction(*f.as_integer_ratio())
>>> z
Fraction(5, 2)
>>> x = Fraction(1, 3)
>>> float(x)
0.3333333333333333
>>> float(z)
2.5

注意到,float()可以将分数类型转换为浮点数。Fraction也有一种方法将浮点数转换为分数。

>>> Fraction.from_float(1.75)
Fraction(7, 4)

分数加整数的结果是分数,分数加浮点数的结果是浮点数:

>>> x = Fraction(1, 3)
>>> x + 1
Fraction(4, 3)
>>> x + 1.0
1.3333333333333333

但是,如果浮点数不能精确地表达某些实数(如1/3),那么转换为分数结果不精确。

>>> 1.0 / 3
0.3333333333333333
>>> (1.0 / 3).as_integer_ratio()
(6004799503160661, 18014398509481984)

我们可以限制分母地最大值来简化结果:

>>> a = Fraction(*(1.0 / 3).as_integer_ratio())
>>> a
Fraction(6004799503160661, 18014398509481984)
>>> a.limit_denominator(10)
Fraction(1, 3)

集合

集合的性质与数学中集合的性质一致。如:无序、没有重复元素。

集合基础知识

我们可以用一个序列或可迭代对象来创建集合:

>>> x = set('abcde')
>>> y = set('bdxyz')
>>> x
{'d', 'a', 'c', 'b', 'e'}

集合的运算:

>>> x - y   # 差集
{'a', 'c', 'e'}
>>> x | y   # 并集
{'d', 'x', 'z', 'a', 'c', 'b', 'e', 'y'}
>>> x & y   # 交集
{'d', 'b'}
>>> x ^ y   # 对称差
{'e', 'z', 'a', 'x', 'y', 'c'}
>>> x > y, x < y # 是否为子集
(False, False)

集合成员测试in:元素是否在集合/列表/字符串内。

>>> 'e' in x
True
>>> 'e' in 'Camelot', 22 in [11, 22, 33]
(True, True)

集合操作还提供了与这些操作对应的方法:

>>> z = x.intersection(y)       # 交集
>>> z
{'d', 'b'}
>>> z.add('SPAM')               # 加入新的元素
>>> z
{'d', 'b', 'SPAM'}
>>> z.update(set(['X', 'Y']))   # 与集合合并(并集)
>>> z
{'Y', 'X', 'b', 'd', 'SPAM'}
>>> z.remove('b')               # 删除一个元素
>>> z
{'Y', 'X', 'd', 'SPAM'}

集合可以用于lenfor操作,但不适用于索引、分片。

集合操作对应的方法对任何可迭代类型有效:

>>> S = set([1, 2, 3])
>>> S | set([3, 4])
{1, 2, 3, 4}
>>> S | [3, 4]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'set' and 'list'
>>> S.union([3, 4])
{1, 2, 3, 4}
>>> S.union((3, 4))
{1, 2, 3, 4}

新旧版本的集合字面量

对于集合S = {1, 2, 3},新版本(Python2.7,Python3.X)这么显示:

>>> S
{1, 2, 3}

旧版本(Python 2.X)这么显示:

>>> S
set([1, 2, 3])

空集表示(因此字面量{}的类型是字典):

>>> S - {1, 2, 3}
set()

不可变性限制与冻结集合

集合只能包含不变的对象类型。

>>> S = {1.23}
>>> S.add([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> S.add({'a': 1})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> S.add((1, 2, 3))
>>> S
{1.23, (1, 2, 3)}

我们可以调用内置的frozenset,这种类型的集合不可改变。

Python 3.X和Python 2.7中的集合推导

集合推导会运行一个循环并在每次迭代时收集一个表达式的结果,通过一个循环变量访问当前的迭代值以用于集合表达式中。

>>> {x ** 2 for x in [1, 2, 3, 4]}
{16, 1, 4, 9}

在这个表达式中,循环部分在右侧,集合体表达式在左侧(x ** 2)。意思是:对于列表[1, 2, 3, 4]中的每一个x,给出包含x平方的一个新集合。推导式也适用于迭代其他类型的对象(如字符串)。

为什么使用集合

去除集合体中的重复

>>> L = [1, 1, 4, 5, 1, 4]
>>> set(L)
{1, 4, 5}
>>> list(set(L))
[1, 4, 5]

提取可迭代对象的差异,借助集合完成顺序无关的等价性测试。

>>> set(dir(bytes)) - set(dir(bytearray))
{'__bytes__', '__getnewargs__'}
>>> set(dir(bytearray)) - set(dir(bytes))
{'insert', 'reverse', '__delitem__', 'copy', 'extend', '__iadd__', 'pop', 'append', '__imul__', 'clear', 'remove', '__alloc__', '__setitem__'}

>>> L1 = [1, 5, 2, 4, 3]
>>> L2 = [5, 1 ,2, 3, 4]
>>> L1 == L2
False
>>> set(L1) == set(L2)
True

布尔型

Python通过判断表达式的真假得到布尔值。

布尔型实际上是特殊的整数类型,True对应1,False对应0。

>>> True + 4
5
>>> False + 4
4

数值扩展

NumPy可以处理更复杂的数值类型,如向量、矩阵。