欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

从排序数组中删除重复项 引发的对python内存分配的小思考

程序员文章站 2022-06-30 18:31:54
引子: Leetcode上:从排序数组中删除重复项 ​ 然而这个方法一直没通过,我看了测试过程后才发现原来nums并没有被修改。这是为什么呢?原来 nums = list(set(nums)) 这个被重新赋值的nums并不是指向的原来传进来的nums 的地址,set()后重新申请了一块空间,然后li ......
引子:

Leetcode上:从排序数组中删除重复项

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}
我当时的解决方法:
class Solution:
    def removeDuplicates(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        nums = list(set(nums))
        return len(nums)

​ 然而这个方法一直没通过,我看了测试过程后才发现原来nums并没有被修改。这是为什么呢?原来nums = list(set(nums))这个被重新赋值的nums并不是指向的原来传进来的nums 的地址,set()后重新申请了一块空间,然后list()又重新申请了一块空间,最后将nums指向这个空间当然就不是原来的实参(形参)原来的空间了,自然实参的值没有得到改变。

python中不需要像C++中那样传引用或是传地址,而是只用在函数内对传进来的变量重新赋值即可,python函数传参的过程可看做是深拷贝,地址是相同的,所以可以通过改变形参的值来改变实参的值。但是如果是整型是不可以的。

a = 1
print(id(a)) =>1624465632
def do1(nums):
  nums = 2
do1(a)
print(id(a)) =>1624465632
print(a)    => a=1
----
b = [1,2]
print(id(b)) =>2578463263304
def do2(list):
  list.append(3)
do2(b)
print(id(b)) =>2578463263304
print(b)   => [1,2,3]

a=1只是a引用了1,指向了1这个地址。10的引用计数位0的时候,解释器会自动回收。

当然这跟python中整型分配内存空间的方式也有关。

num = 10
print(id(num)) 
print(id(10) )
#=>结果是相同的.
#num_2=10 id(num_2)=id(num)=id(10)
更进一步说明:
print(num is 10) # => True

​ 其中操作符is的作用就是判断两个是否指向同一个内存。在python中,相当于给"10"这个数值分配了内存,然后让变量"num"去指向“10”(可以看做是指针),num可以看做是10的引用。而python中的id()则可以用来查询地址。

在python中,一开始初始化存储在内存的东西是不可以更改的,python中对内存的操作是由解释器来管理的。对于整型来说,我们并不能改变地址,我们所能更改的只是它的指向。


然而对于python的内置数据类型如列表list(),字典{},集合{}来说。又是另外一种情况。

list1 = [1,2,3]   
list2 = [1,2,3]
print(id(list1) )   # =>1904062761544
print(id(list2) )   # =>1904062762952
print(id([1,2,3]))  # =>1904092664008
#=>结果是不同的.
更进一步说明:
print(list1 is list2) # => False

Python中list内存的分配方式是动态的:

创建时:
if (numfree) {
    numfree--;
    op = free_list[numfree];
    _Py_NewReference((PyObject *)op);
} else {
    op = PyObject_GC_New(PyListObject, &PyList_Type);
    if (op == NULL)
        return NULL;
}
resize时:
    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
    /* check for integer overflow */
    if (new_allocated > PY_SIZE_MAX - newsize) {
        PyErr_NoMemory();
        return -1;
    } else {
        new_allocated += newsize;
    }

​ 即使是相同内容的列表,但他们的地址是不同的,相当于仅仅是一次浅拷贝。

​ 至于为什么整型、元组不能改变他们内存上的值,而字典、列表可以,我个人认为主要还是python对它们内存管理方式上的不同。学识尚欠,不敢误言,以后整明白了再补上吧。


Python sys.getsizeof(int())
print(id(2)) #=>1624465664
print(id(3)) #=>1624465696
print(id(4)) #=>1624465728

发现整型地址之间只差了32,这似乎意味着在64位里python是占4字节的。于是为了证实这个想法,我使用了sys.getsizeof(int())。

△.注:sys.getsizeof(int()) 为<class 'int'>这个类的总大小

>>> sys.getsizeof(0)
24
>>> sys.getsizeof(1)
28
>>> sys.getsizeof(2 ** 30 - 1)
28
>>> sys.getsizeof(2 ** 30)
32
>>> sys.getsizeof(2 ** 60 - 1)
32
>>> sys.getsizeof(2 ** 60)
36

​ 可以看到的是,随着存储的数字大小不同,int占有的空间也是不同的,越大的数,需要的存储空间越大。这种能够变得更长的表现暗示着它像列表之类的数据类型。事实上,在C语言中python的int是这样定义的:

typedef struct {
    PyObject_HEAD
    long ob_ival;   #类型为长整型
} PyIntObject;
为了避免每次需要一个新的整数对象时都要申请新的对象,python提前为未使用的整数对象分配了一块空的区间。
struct _intblock {
    struct _intblock *next;
    PyIntObject objects[N_INTOBJECTS];
};
typedef struct _intblock PyIntBlock;

▲.0是没有价值的,所以不在存储值内,所以getsizeof(0)==24.

通常在32、64位中的int都是4字节的,然而得到的结果明显远大于这个值,原因是在python中int已经很完善了,实现多种功能的同时,就意味着它需要占用更多的空间,消耗更多的内存。

总的来说,这些都是跟硬件有关的,在32位和64位中不同数据类型所占的空间还是不同的。

继续讨论下那块已经提前分配好的整数空间

print(id(-6))  #3221839702960
print(id(-5))  #1624465440
print(id(255)) #1624473760
print(id(256)) #1624473792
print(id(257)) #2143657436272
print(id(258)) #2143657438960

可以看到-6到-5和256到257,地址突然跃变了,这其实是因为重新为[-5,256]之外的数分配了新的内存空间。

#define NSMALLPOSINTS           257
#define NSMALLNEGINTS           5
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

A specific structure is used to refer small integers and share them so access is fast. It is an array of 262 pointers to integer objects. Those integer objects are allocated during initialization in a block of integer objects we saw above. The small integers range is from -5 to 256. Many Python programs spend a lot of time using integers in that range so this is a smart decision.

if integer value in range -5,256:
    return the integer object pointed by the small integers array at the
    offset (value + 5).
else:
    if no free integer object available:
        allocate new block of integer objects
    set value of the next free integer object in the current block
    of integers.
    return integer object

​ 为了让更小的数字被访问的更快,事先python已经为[-5,256]之间的整数分配好了内存。如果要使用这个范围里面的数直接访问即可,加上相应的偏移量就能得到正确的数值,(-5的偏移量为0,-4的偏移量则为1)。如果不在这个范围内,python将另外创建个新的block来存放PyIntObject。


从排序数组中删除重复项 引发的对python内存分配的小思考


参考:
https://*.com/questions/10365624/sys-getsizeofint-returns-an-unreasonably-large-value
http://www.laurentluce.com/posts/python-integer-objects-implementation/