pyhon-整数以及内存表示方式

    从这一章我们就开始一起开始进入学习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整数是无限精度的,只要内存足够大,我们就可以表示任意大小的整数,同时我们学习了整数在内存中的二进制表示方式,以及二进制和十进制的相互转换,同时从十进制转二进制的过程中引出了计算机补码和反码的概念。

由于篇幅问题,我们没有谈其他进制的转换问题,我们留到下一章单独来说这个问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白快快跑哦

您的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值