从这一章我们就开始一起开始进入学习python的阶段了,在前面几章,我讲了一些正式学习python之前最好需要了解的知识,从这一章开始,我讲开始讲授python语言本身。这个章节我打算来谈一谈python种的第一个大的类型,数字。当然,还是秉承我一贯理念,我们不仅仅是学习python语言本身,还要学习计算机相关的基础知识。
1 数字的类型
谈到数字,如果读过我之前的文章,明白数字是常量,所以数字是不可改变的对象。python的数字有以下几种类型:整数,浮点数,复数。注意,我们现在所谈论的python的语法,都是基于python3.0之后的。这一章我们先谈整数。
1.1 整数
如果学过C,都明白整数是有精度的,一般32位的机器,整数(int类型)是32位的,所以能表示的大小是确定的。但是python中的整数是没有大小限制的,只要你内存够,就可以表示无限大小。看一下下面这段代码:
print(2**100-1)
打印出的结果是: 1267650600228229401496703205375,这是一个相当大的数字。这个代码是2的100次方减1,如果是C代码,假如是32位机,这个运算会得出一个不可思议的结果,因为产生了溢出。什么是溢出呢?当前的这个例子,我们要放这个超级大的数,需要101位才能放置,最高位是符号位。假如现在我们有101位来存放这个数,这个数在内存中是如何表示的?
我们都知道,内存中都是比特(bit)表示的,每个比特代表1位,这个值不是1就是0,也就是我们所说的二进制表示。对这个1267650600228229401496703205375数值,我们在内存中最高位是符号位,因为是整数,所以最高位为0,剩下的100位全部为1,就像下面这样:
当然,python自己有一套存储超大整数的机制,我相信不会像我这样用101位来表示,最起码会用8的倍数位来表示,在这里我只是为了说明问题。
刚才我们谈到,在C中,这样就会产生溢出,因为C中是32位的长度来表示整数(int类型),所以这个超大整数会被截断,变成32位。截断的是高位截断,所以在C中,32存储了101位中低的32位,所以最后在C中这个32位存储的全是1,我们都知道,最高位是符号位,所以这个数字的值变成了-1。这个数字是不是有点奇怪,本来是正数,最后却变成了负数。我们把这种情况叫做溢出,因为超出了32位表示的范围,就像水桶满了,剩下的水就溢出去了。至此,我相信大家都明白了什么是溢出,我们本来需要101位才能表示这个数,但是目前只有32位,所以溢出去了69位,这样说虽然有点奇怪,不过很形象。
再回过头看这个数的内存表示,为什么我知道第一位是0,剩下的100位都是1呢。
这个涉及到一个换算的问题,我们如何将一个二进制转换为十进制呢?这引出了另外一个话题。
1.1.1 整数的表示方式
我们知道内存中整数是以二进制存储的,我们写代码的时候基本上是以十进制形式书写的,当然还有八进制,十六进制,如果你愿意,也可以用二进制书写,因为二进制的位数太长,书写很不方遍,一般我们用十六进制表示的多。看一下下面的代码:
#十进制表示
a = 8
#二进制表示
b = 0b1000
#八进制表述
c = 0o10
#十六进制表示
d = 0x8
print("a={}".format(a)) #a=8
print("b={}".format(b)) #b=8
print("c={}".format(c)) #c=8
print("d={}".format(d)) #d=8
上面这段代码说明了十进制,二进制,八进制和十六进制如何表示数字8。
十进制:这个符合我们的书写习惯,直接写8即可,
二进制:数字0开头,第二个是字母b,后面二进制数字,只有1和0
八进制:数字0开头,第二个字母是o,后面是八进制数字,从0到7
十六进制:数字0开头,第二个字母是x,后面是16进制数字,从0到15
因为我们计算机内存中都是二进制,就好比刚才的代码print(2**100-1),这个如何能快速知道内存中是如何表示的呢。接下来我主要谈一下二进制和十进制的相互转换,下面的内容虽然有点枯燥,但是却是计算机的基础知识,一旦掌握,这对于你以后更深入的了解很重要。
1.1.2 从二进制到十进制的转换
假如我们整数用16位存储(这里只是假设,现实中可不是),最高一位表示符号,如果是1,则表示负数,如果是0,则表示整数,看如下的表示:
这个代表-1,如何计算的呢,其实在计算机的二进制表示中,符号位是参与计算的,和十进制一样,最低一位的权值是2**0,二的零次方,从低到高依次为2**0,2**1,2**2 ... 2**15 ,我们计算二进制表示的十进制值的时候,只需要权值乘以位值相加即可,所以:(2**0)*1+(2**1)*1+(2**2)*1 + ... + (2**14)*1+ (2**15)*-1 = 2**15-1+(-2**15) = -1。因为符号位是参与计算的,所以最高位的值是-2**15,根据等比数列求和,前15位的值的和为2**15-1,所以最后的结果为-1 。其中**是python的冥运算符。
通过上面的知识,我们很容易知道16位所能表示的最大值和最小值。当表示最大值是,最高位必须为0,其余位皆为1,根据上面的计算方式,最大数为2**15-1,最小值为最高位为1,其余位数皆为0,结果为-2**15,这就是为啥最大数和最小数在取绝对值差1的原因。
相信读到这里,你已经明白了二进制到十进制是如何转换的。
1.1.3 从十进制到二进制的转换
俗话说,来而不往非礼也,我们上小结学习了二进制到十进制的转换,那如何从十进制到二进制的转换呢?我们还是以16位来说明。十进制到二进制转换的过程,其实就是从二进制到十进制的逆过程。假如有一个数26,如果我们计算这个数的各个位的值呢?当然我们很容易看出来个位数是6,十位数是2。但是如果是让你写一个函数,输入一个十进制,让你把这个十进制每个位的值都打印出来,你怎么做呢?当然python可以用相应的函数,我们首先把这个整数转化为字符串,然后每次打印一个字符,这样也能得出来,假如你不用任何转换函数呢?
接下来我们说下这个思路,首先我们将26模除以10,得到6,然后再用26整除以10,得到2,那么此时6就是个位数,然后2模除以10,得到2,然后2整除以10,得到0。这时候我们计算出了十分位为2,因为剩下是零了,我们计算终止,因为在计算下去,没有意义,因为得出来的都是0。
按照这个思路我们用python代码实现:
def get_place(a):
#对参数取绝对值
a = abs(a)
#定义一个列表用来存放每一位
res = []
#如果a大于0,循环继续,如果a小于等于0,跳出循环
#实际上,a不会小于0,最后只能为0
while a > 0:
#先进行模除,得到最低位
tmp = a % 10
#去掉最低位
a = int(a /10)
#得到的最近位插入列表第一位,因为我们最先或许低位的值
#所以最后得到的是最高位,需要放在最前面
res.insert(0,tmp)
#打印这个列表
print(res)
get_place(26)
这里面涉及到python的语法,因为我假定大家还没有学到,所以我加了详尽的注释,提一下这个abs函数,是取绝对值,如果是负数-26,我们首先将其转换为正数26,因为我们只是想得到位数的值,所以不考虑符号,这对十进制来讲,没什么关系,但是对二进制来讲,就会有区别,我先不讲区别。十进制,我们是以10为权,那么二进制,我们是不是可以理解为以2权呢,那么上面这个算法,我们直接讲10改为2是不是就可以了?我们把代码改一下:
def get_place(a):
#对参数取绝对值
a = abs(a)
#定义一个列表用来存放每一位
res = []
#如果a大于0,循环继续,如果a小于等于0,跳出循环
#实际上,a不会小于0,最后只能为0
while a > 0:
#先进行模除,得到最低位
tmp = a % 2
#去掉最低位
a = int(a /2)
#得到的最近位插入列表第一位,因为我们最先或许低位的值
#所以最后得到的是最高位,需要放在最前面
res.insert(0,tmp)
#打印这个列表
print(res)
get_place(26)
这个代码我们运行一下,得到的结果是:[1, 1, 0, 1, 0],这只有五位,因为这个是16位,所以剩下的高位全是0,我们用之前的二进制转十进制的方法计算一下,是不是得出来就是26啊,大家可以验证下。
因为定义的位数是16位,那我们稍稍改进一下程序,让其打印出16位的形式
def get_place(a,bits):
#对参数取绝对值
a = abs(a)
#定义一个列表用来存放每一位
res = []
#如果a大于0,循环继续,如果a小于等于0,跳出循环
#实际上,a不会小于0,最后只能为0
while a > 0:
#先进行模除,得到最低位
tmp = a % 2
#去掉最低位
a = int(a /2)
#得到的最近位插入列表第一位,因为我们最先或许低位的值
#所以最后得到的是最高位,需要放在最前面
res.insert(0,tmp)
#如果没有bits位,则高位补0
while len(res) <bits:
res.insert(0, 0)
#打印这个列表
print(res)
get_place(26,16)
代码改完之后,我们再次运行代码就得到了如下的结果:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0]。 我在原来的函数增加了一个参数bits,来说明这个是基于多少位的,这样代码就会变的灵活,最后增加了while循环,如果这个数组没有达到的bits的长度,我们在最前面添加值为零的成员。
现在我们是不是已经完美的实现了二进制到十进制的转换呢?答案是否定的,当初我们对要转换的整数取了绝对值,试想一下,如果这时候我们输入的是-26,上面的程序得到了和二十六一样的结果,这显然是不正确的,整数和负数二进制的表示肯定不同的。
那么对于负数26我们该如何计算呢?
数学的角度来看,26+(-26) = 0
那么,这个数字零,用二进制如何表示呢?,根据之前的我们所学,二进制应该是所有数位全是0才对[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0 0, 0, 0],那么对于,26+(-26) = 0,二进制表示[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0] +[ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]=[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0 0, 0, 0]
所以得出:
[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0 0, 0, 0] -[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0]
这看上去就是一个二进制的减法,我们从低位开始减:
第一位: 0-0=0
第二位:0-1,需要向高位借一位,所以是2-1=1
第三位:0-0,因为之前高位借了一位,所以还得减1,但是呢这个位是0,还得向高位借,得出2-1=1
第四位:0-1,因为之前高位接了一位,所以还得再减1,这个位还是0,还得从高位借位,得出2-2=0
第五位:0-1,因为之前在高位接了一位,所以还得再减一,这个位还是0,还得从高位借位,得出2-2=0
第六位:0-0,因为之前在高位接了一位,所以还得再减一,这个位还是0,还得从高位借位,得出2-1=1
第7位:0-0,因为之前在高位接了一位,所以还得再减一,这个位还是0,还得从高位借位,得出2-1=1
第8位:0-0,因为之前在高位接了一位,所以还得再减一,这个位还是0,还得从高位借位,得出2-1=1
直到第16位,因为都是0-0并且高位借位了,所以都是1,
这样不停的借位,最后得到这个结果:
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0] 我们观察下这个结果,和26的二进制对比下:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0]
观察这两个数,大多数位数都是相反的,除了后面的两位,但是如果我们把原来的26都按位取反再加1,是不是就得到了和-26相同的位数表示?事实上确实是如此的。
这就引出了计算机二进制数表示的两个概念:补码和反码。
先说反码,对于正数来讲,反码和原码是一样的,所以对于26来讲,他的反码和原码都是[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0] ,那么对于负数,他的反码就是负数的绝对值的的原码按位取反,也就是26的原码按位取反。
接下来说补码,对于正数来讲,补码和原码是一样的,对于负数来讲,它的原码是负数的绝对值的原码按位取反+1,看到这里,我们刚才的推导其实就是计算出负数的补码。
虽然计算机中用反码和补码都可以表示整数,但是反码有个问题,会存在-0和+0的问题,+0的补码全是零,但是-0的反码全是1,我们都是知道无论是+0还是-0,其结果都是零,这就对同一值有两种不同的表示了,这就出现了一致性的问题。但是如果是补码,-0的反码还是全是0,这样就一致了。
所以计算机中,我们用补码来表示整数,在上面所谈的二进制转化十进制的时候,也都是以补码为基础来讲的。所以我们明白了整数和负数补码之间的关系,那么上面的程序,我们再做修改,考虑到负数的情况:
def get_place(a,bits):
#是否复数的标志
minus = False
#对参数取绝对值
if a < 0:
a = abs(a)
minus = True
#定义一个列表用来存放每一位
res = []
#如果a大于0,循环继续,如果a小于等于0,跳出循环
#实际上,a不会小于0,最后只能为0
while a > 0:
#先进行模除,得到最低位
tmp = a % 2
#去掉最低位
a = int(a /2)
#得到的最近位插入列表第一位,因为我们最先或许低位的值
#所以最后得到的是最高位,需要放在最前面
res.insert(0,tmp)
#如果没有bits位,则高位补0
while len(res) <bits:
res.insert(0, 0)
#如果是复数
if minus:
i = len(res)-1
#按位取反
while i >= 0:
if res[i] == 0:
res[i] = 1
else:
res[i] = 0
i = i - 1
i = len(res)-1
#将取反的结果加一,因为是列表,我们们从列表的最后一位加1
#如果该列表元素加一之后不等2,就结束循环,如果等于2,将该位置0
#继续对数组的上一位加1
while i >= 0:
tep = res[i] + 1
if tep == 2:
res[i] = 0
else:
res[i] = res[i] + 1
break
i = i - 1
#打印这个列表
print(res)
get_place(26,16)
get_place(-26,16)
上面的代码增加了一个变量用来标记是否为负数,代码最后增加了如果是负数的逻辑,如果为负数,首先将这个列表的各项取反,这里的取反并不是真正意义的取反,我们可以认为是将0变成1,将1变成0,第二步是从列表的最后一项加1,如果加一之后该值为2,则将该值置0,同时继续给倒数第二项加1,如果还是2,则继续相同的操作,直到该项加1不等于2。
代码最后输出了26和-26的补码:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0]
好了,这一章就结束了,通过这一章,我们了解了python整数是无限精度的,只要内存足够大,我们就可以表示任意大小的整数,同时我们学习了整数在内存中的二进制表示方式,以及二进制和十进制的相互转换,同时从十进制转二进制的过程中引出了计算机补码和反码的概念。
由于篇幅问题,我们没有谈其他进制的转换问题,我们留到下一章单独来说这个问题。