Fluent Python2 【Chapter2_QA】

1. 如何打印出一个对象的ob_refcnt, ob_type, ob_fval

ob_refcnt: 对象的引用计数

ob_type: 指向对象类型的指针

ob_fval: 一个C语言double类型值, 存放float值

要访问对象的 ob_refcntob_type 和 ob_fval 属性,你需要使用 Python 的 C API,因为这些属性是 CPython 解释器内部的结构。在纯 Python 中无法直接访问这些属性。如果你想要通过 Python 代码来获取这些属性,可以使用 ctypes 库来访问 CPython 解释器内部的结构。

以下是一个示例代码,演示如何使用 ctypes 库来获取对象的 ob_refcntob_type 和 ob_fval 属性:

import ctypes

# 定义一个 ctypes 结构体,以匹配 PyObject 结构体的布局
class PyObject(ctypes.Structure):
    _fields_ = [
        ("ob_refcnt", ctypes.c_ssize_t),
        ("ob_type", ctypes.c_void_p),
        # 此处仅为示例,根据实际情况修改字段类型
        ("ob_fval", ctypes.c_double)  
    ]

# 创建一个 Python 对象
x = 42.0

# 使用 ctypes 将 Python 对象的地址转换为指针
x_address = id(x)
x_pointer = ctypes.cast(x_address, ctypes.POINTER(PyObject))

# 获取对象的 ob_refcnt、ob_type 和 ob_fval 属性
ob_refcnt = x_pointer.contents.ob_refcnt
ob_type = x_pointer.contents.ob_type
ob_fval = x_pointer.contents.ob_fval

# 打印属性值
print("ob_refcnt:", ob_refcnt)
print("ob_type:", ob_type)
print("ob_fval:", ob_fval)

这段代码使用了 Python 的 ctypes 模块来访问 CPython 解释器的内部结构,以获取对象的 ob_refcntob_type 和 ob_fval 属性。下面是对每一行代码的逐行解释:

step1: 导入 ctypes 模块:

import ctypes

这一行导入了 Python 的 ctypes 模块,用于访问 CPython 解释器的内存结构。

step2: 定义 PyObject 结构体:

class PyObject(ctypes.Structure):
    _fields_ = [
        ("ob_refcnt", ctypes.c_ssize_t),
        ("ob_type", ctypes.c_void_p),
        ("ob_fval", ctypes.c_double)
    ]

这里定义了一个 PyObject 类型的结构体,用于匹配 CPython 解释器中的 PyObject 结构体的布局。结构体中包含了三个字段:ob_refcntob_type 和 ob_fval,分别对应于对象的引用计数、类型和值。

step3: 创建 Python 对象并获取其地址:

x = 42.0
x_address = id(x)

这里创建了一个 Python 浮点数对象 x,然后使用 id() 函数获取了该对象的内存地址,并将其保存在 x_address 变量中。

 

step4: 将地址转换为指针:

x_pointer = ctypes.cast(x_address, ctypes.POINTER(PyObject))

使用 ctypes.cast() 函数将 Python 对象的地址 x_address 转换为指向 PyObject 结构体的指针,并将结果保存在 x_pointer 变量中。

step5: 获取对象的属性值:

ob_refcnt = x_pointer.contents.ob_refcnt
ob_type = x_pointer.contents.ob_type
ob_fval = x_pointer.contents.ob_fval

这里通过 x_pointer.contents 来访问指针所指向的 PyObject 结构体的属性,从而获取了对象的 ob_refcntob_type 和 ob_fval 属性的值,并分别将它们保存在 ob_refcntob_type 和 ob_fval 变量中。

最后,使用 print() 函数打印了对象的 ob_refcntob_type 和 ob_fval 属性的值。

 

2. 理解下如下返回结果的差异

a = (10, 'alpha', [1, 2])

b = (10, 'alpha', [1, 2])

a == b # return True

a is b # return False

在这个示例中,我们首先定义了两个元组 a 和 b。它们的值完全相同,都是 (10, 'alpha', [1, 2])

  1. a == b 返回 True。这是因为 == 操作符用于比较两个对象的值是否相等。在这种情况下,a 和 b 包含完全相同的值,因此它们被认为是相等的。
  2. a is b 返回 Falseis 操作符用于检查两个变量是否引用同一个对象。在这里,a 和 b 是两个独立创建的元组对象,即使它们的值相同。它们在内存中有不同的地址,因此 a is b 返回 False

需要注意的是,对于不可变对象(例如数字、字符串和元组),Python 会在内部缓存这些对象,以提高性能和减少内存使用。但是,对于可变对象(例如列表和字典),Python 不会进行缓存,每次创建都会产生一个新的对象。

在这个例子中,[1, 2] 是一个可变对象(列表),因此 a 和 b 中的列表是两个不同的对象,即使它们的值相同。这就是为什么 a == b 返回 True(值相等),而 a is b 返回 False(不是同一个对象)。

对于不可变对象,如果两个变量具有相同的值,它们有可能指向同一个对象(在这种情况下,is也会返回True)。但是,对于可变对象,每次创建新对象时,它们总是会得到一个新的内存地址。

 

5. 理解下如下函数

def fixed(o):
    try:
        hash(o)
    except TypeError:
        return False
    return True

这个函数 fixed 用于检查一个对象是否是不可变对象(immutable object)。

在 Python 中,不可变对象是指其值在创建后不能被改变的对象,例如数字、字符串、元组等。而可变对象则可以改变其值,例如列表、字典等。

该函数的工作原理如下:

  1. 首先使用 try 语句尝试对传入的对象 o 调用 hash(o) 函数。
  2. 在 Python 中,只有不可变对象才能被哈希(hash)。如果对象是可变的,则会引发 TypeError 异常。
  3. 如果引发了 TypeError 异常,说明传入的对象是可变对象,函数返回 False
  4. 如果没有引发异常,说明传入的对象是不可变对象,函数返回 True

因此,这个函数可以用于检测一个对象是否是不可变的。你可以使用它来确定某个对象是否可以作为字典的键值,或者是否可以被缓存等。

示例:

print(fixed(1))      # True (数字是不可变的)
print(fixed('hello'))# True (字符串是不可变的)
print(fixed((1, 2)))# True (元组是不可变的)
print(fixed([1, 2]))# False (列表是可变的)
print(fixed({'a': 1}))# False (字典是可变的)

这个函数利用了 Python 中对象哈希的特性,提供了一种简单的方法来检测对象的可变性。如果你需要判断一个对象是否可以被哈希或者是否可变,这个函数会很有用。

 

3. 理解下dis.dis()函数,from Example 2-18

逐行解释如下代码:

import dis

print(dis.dis('s[a]+=b'))

1 0 LOAD_NAME 0 (s)

2 LOAD_NAME 1 (a)

4 DUP_TOP_TWO

6 BINARY_SUBSCR

8 LOAD_NAME 2 (b)

10 INPLACE_ADD

12 ROT_THREE

14 STORE_SUBSCR

16 LOAD_CONST 0 (None)

18 RETURN_VALUE

None

这行导入了 Python 的 dis 模块,它提供了对 Python 字节码的访问。

import dis

这行调用了 dis.dis() 函数,该函数以字符串形式接收一段 Python 代码,并打印出它对应的字节码指令序列。在这里,传入的代码是 's[a]+=b'

print(dis.dis('s[a]+=b'))

这是第一条字节码指令。它从当前命名空间加载名为 s 的对象并将其推到栈顶。

1 0 LOAD_NAME 0 (s)

这条指令从当前命名空间加载名为 a 的对象并将其推到栈顶。现在栈顶有两个对象,依次是 a 和 s

2 LOAD_NAME 1 (a)

这条指令将栈顶的两个对象复制两份,现在栈顶有四个对象,依次是 asas

4 DUP_TOP_TWO

这条指令对栈顶的两个对象执行下标操作,即 s[a]。它弹出栈顶的两个对象 a 和 s、执行下标操作,然后将结果推到栈顶。

6 BINARY_SUBSCR

这条指令从当前命名空间加载名为 b 的对象并将其推到栈顶。现在栈顶有两个对象,依次是 b 和 s[a]

8 LOAD_NAME 2 (b)

这条指令对栈顶的两个对象执行就地加法操作,即 s[a] += b。它弹出栈顶的两个对象 b 和 s[a]、执行加法操作,然后将结果推到栈顶。

10 INPLACE_ADD

这条指令将栈顶的三个对象旋转。现在栈顶有三个对象,依次是原来的第二个对象 s、原来的第三个对象 a、以及加法操作的结果。

12 ROT_THREE

这条指令将栈顶的加法结果赋值给 s[a]。它弹出栈顶的三个对象,并将加法结果存储到 s[a] 中。

14 STORE_SUBSCR

这两条指令将 None 推到栈顶,然后从函数返回。在这个例子中,它们只是为了让字节码序列以正确的方式结束。

最后,代码输出了 None

16 LOAD_CONST 0 (None)
18 RETURN_VALUE

总的来说,这段代码展示了对增量赋值语句 s[a] += b 的字节码解释。它首先加载需要的对象,执行下标操作、加法操作和赋值操作,最后返回 None。通过查看字节码,你可以更好地理解 Python 是如何执行这种语句的。

 

4. 解释下最后的返回结果,from Example 2-21

import array

numbers = array.array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
print(len(memv))
print(memv[0])

memv_oct = memv.cast('B')
print(memv_oct.tolist())
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]

 首先,对于如下代码

import array

numbers = array.array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
print(len(memv))  # 输出: 5
print(memv[0])  # 输出: -2

这部分代码创建了一个 array.array 对象 numbers,其中存储了 5 个短整型数值 [-2, -1, 0, 1, 2]。然后,我们使用 memoryview 创建了一个内存视图 memv,它直接访问 numbers 对象在内存中的缓冲区。

输出 len(memv) 为 5,因为它包含 5 个短整型数值。输出 memv[0] 为 -2,即数组中的第一个元素。

memv_oct = memv.cast('B')
print(memv_oct.tolist())  # 输出: [254, 255, 255, 255, 0, 0, 1, 0, 2, 0]

这部分代码使用 memv.cast('B') 创建了一个新的内存视图 memv_oct,将原来的短整型数值以无符号字节的形式进行解释。由于一个短整型数值占用 2 个字节,因此输出列表的长度为 10。

具体来说,输出列表中的每两个元素对应于原始数组中的一个短整型数值:

  • 254, 255 对应于 -2
  • 255, 255 对应于 -1
  • 0, 0 对应于 0
  • 0, 1 对应于 1
  • 2, 0 对应于 2

这是因为在内存中,一个短整型数值占用 2 个字节,而每个字节的取值范围是 0 到 255。负数在内存中是以补码的形式存储的。

例如,-2 的二进制补码表示为 1111111111111110,分成两个字节就是 1111111111111110,对应的十进制值分别为 255254,这就是为什么 -2 在输出列表中表示为 254, 255

通过将原始数据以不同的数据类型进行解释和输出,memoryview 提供了一种方便的方式来查看和操作内存中的原始字节数据。

 

5.对于4中的解释,如何理解“由于一个短整型数值占用 2 个字节,因此输出列表的长度为 10。”?

在这段代码中,我们有一个包含5个短整型数值的数组:

numbers = array.array('h', [-2, -1, 0, 1, 2])

其中, 'h' 表示数组中的元素是 short 类型,即 C 语言中的 short int。在 CPython 实现中,short int 占用 2 个字节,即 16 位。

当我们创建一个内存视图对象时:

memv = memoryview(numbers)

这个内存视图 memv 直接访问了 numbers 数组在内存中的字节序列。

接下来,我们使用 memv.cast('B') 创建了一个新的内存视图 memv_oct:

memv_oct = memv.cast('B')
print(memv_oct.tolist())  # 输出: [254, 255, 255, 255, 0, 0, 1, 0, 2, 0]

这个新的内存视图 memv_oct 将原始数组中的元素按无符号字节 ('B' 表示 unsigned char) 的方式进行解释和访问。

由于原始数组 numbers 中的每个元素都是一个 short int 类型,占用 2 个字节(16 位),而 memv_oct 将这些元素按字节(unsigned char)的形式进行访问和解释,因此输出列表的长度就变成了 10。

换句话说,原始数组中有 5 个元素,每个元素占用 2 个字节,那么按字节解释的时候,总共就有 5 * 2 = 10 个字节。

所以,输出列表 [254, 255, 255, 255, 0, 0, 1, 0, 2, 0] 的长度为 10,是因为它表示了原始数组中 5 个 short int 元素的所有字节。每两个元素对应于原始数组中的一个 short int 元素。

 

6. 对于4中的,-2. -1的补码的计算过程,详细解释下。

如下是关于有符号整数在内存中的补码表示,以及为什么-2-1的二进制补码分别是11111111111111101111111111111111 的具体解释

首先,我们需要了解有符号整数在计算机内存中是如何存储的。对于一个N位的有符号整数,规定:

  • 最高位(第N位)为0表示正数,为1表示负数
  • 剩余N-1位表示数值的绝对值,采用纯二进制表示

对于负数,我们需要使用"二进制补码"来表示。补码的计算规则是:

  1. 先求出该负数绝对值的纯二进制码
  2. 将上述纯二进制码按位取反(0变1,1变0)
  3. 对取反后的结果加1

让我们以-2为例:

  1. -2的绝对值是2,纯二进制码是0000000000000010(16位)
  2. 按位取反,得到1111111111111101
  3. 加1,得到1111111111111110

这就是-2的补码表示1111111111111110

现在,让我们把这个16位的补码拆分成两个字节(8位):

1111111111111110 分为
11111111  11111110
     255       254

所以-2在内存中的字节表示就是255254

同理,我们可以推导出-1的补码表示:

  1. -1的绝对值是1,纯二进制码是0000000000000001
  2. 按位取反得1111111111111110
  3. 加1得1111111111111111

所以-1的补码是1111111111111111

将其分成两个字节也是:

1111111111111111 分为
11111111 11111111
    255      255

-1在内存中的字节表示是两个255

总结一下,计算机内存中使用补码来存储有符号整数,最高位为符号位,剩余位表示绝对值。对于负数,需要按位取反加1得到补码,这种表示方式可以使用现有的无符号二进制运算来处理有符号整数的运算。

根据综上解答,便可以更好地理解有符号整数的内存表示。

 

posted @ 2024-03-31 20:25  AlphaGeek  阅读(21)  评论(0)    收藏  举报