第6章 动态类型

第6章 动态类型
动态类型以及它提供的多态性,是Python语言简洁性和灵活性的基础。

在Python中,我们并不会声明对象的确切类型,大多数程序甚至可以不在意特定的类型,相反的他们能够自然的适用于更广泛的场景。

动态类型是Python语言灵活性的根源。

缺少声明语句的情况

如果你有学习编译或静态类型语言C、C++或Java的背景,你会困惑,我们在使用变量时,没有声明变量的存在和类型,但变量还可以工作。
例如:a=3,Python怎么知道那代表一个整数,怎么知道a是什么?

一旦你开始问这样的问题,就已经进入了Python动态类型模型的领域。

在Python中,类型是在运行时自动决定的,而不是通过代码生命的,这意味着没必要事先声明变量

变量、对象和引用

当在Python中运行赋值语句a=3时,即使没有告诉Python将a作为一个变量来使用,或者没有告诉他a应该作为一个整数类型对象,一样都能工作。
在Python语言中,这些都会以一种非常自然的方式完成:

  • 变量创建
    一个变量(变量名),就像a,当代码第一次给他赋值时就创建了他,之后赋值将会改变以创建的变量名的值。
    从技术上讲,Python在代码运行之前先检测变量名,但你可以理解为最初的赋值操作在创建变量。
  • 变量类型
    变量永远不会拥有任何和他关联的类型信息或舒服。
    类型的概念存在于对象而不是变量名中。
    变量原本是通用的,他只是在一个特定的时间点,简单的引用了一个特定的对象而已.SDasd.sdddsaddsad
  • 变量使用
    当变量出现在表达式中时,他马上被当前引用的对象所代替,无论这个对象是什么类型。
    所有的变量必须在使用前被明确的赋值,使用未赋值的变量会产生错误。

变量在赋值的时候才被创建,他可以引用任何类型的对象,而且必须在引用之前赋值
这意味着不需要通过脚本声明所要使用的名字,但是必须初始化名字然后才能更新他们,例如必须把计数器初始化为0,才能增加他(计数器概念立刻就会讲到)

a=3

1、创建一个对象来表示值3
2、创建一个变量a,如果他还没有创建的话
3、将变量与新的对象3相连接

在这里插入图片描述

在运行a=3后的变量名和对象。变量a变成对象3的一个引用。在内部,变量事实上是到对象内存空间(通过运行常量表达式3而创建)的一个指针

变量和对象保存在内存中不同的部分,并通过连接相关联。
变量总是连接到对象,并且决不会连接到其他变量上,但是更大的对象可能连接到其他的对象,例如列表。

之前章节展现了很多类型结构的图片,并搜索到内存中存放变量名的位置,而且确定是现有数据(对象),后有变量的

In [20]: a=3

In [21]: id('a').to_bytes(length=8,byteorder='little').hex()+id(3).to_bytes(length=8,byteorder='little').hex()
Out[21]: '30894f683e0200007069ed673e020000'

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1、Python在初始化时,会自动生成0~255的数字对象
2、当执行a=3时,对象3可以直接使用预生成的数字对象
3、生成变量名,即’a’,字符串格式
4、将两者的内存地址写入到变量存储区

在这里插入图片描述
a=3: 1、3 2、a 3、=
在这里插入图片描述
之前的图片是来自于IPython,这张来自于Python,干扰信息少,更方便探索一些东西

s=set(('a'))
>>> mem.data(s)
01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
07 00 00 00 00 00 00 00 e0 fb 85 f1 94 02 00 00
ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30 89 57 f1 94 02 00 00 19 eb 76 84 64 fd 32 b2

d={'a':3}
01 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 
50 A6 34 F6 FA 7F 00 00 04 00 00 00 00 00 00 00 
01 00 00 00 00 00 00 00 FF 00 FF FF FF FF FF FF 
19 EB 76 84 64 FD 32 B2 30 89 57 F1 94 02 00 00 
70 69 F8 F0 94 02 00 00 00 00 00 00 00 00 00 00

>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'd', 'mem', 's']

这里发现,30 89前的8字节数据,是’a’的哈希值,在原生Python中,变量区是哈希、变量名、对象三为一组的结构
哈希值可以在不遍历变量名字符串的情况下,判断该组是否是所调用变量

这里我用set确定了哈希值,之后看到三为一组时,想到这不就是字典的结构,于是又尝试了下字典,这部分结构完全一样

之前我尝试寻找是否有立刻得到该区域地址的方法,虽然还未果,但至少dir()是可以显示key之类的内容,依稀记得之前见过更直接的方法
help(dir)
If called without an argument, return the names in the current scope.
如果不带参数调用,则返回当前作用域中的名称。

sys.__dict__

>>> l=dir().copy()
>>> d={}
>>> for i in l:
...     d[i]=eval(i)
...
d
{'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__doc__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__name__': '__main__', '__package__': None, '__spec__': None, 'a': 3}
>>> l
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a']

作用域?真实顺序:
__name__、__doc__、__package__、__loader__、__spec__、__annotations__、__builtins__、a


搜索所得,作用域?:
01 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 
50 A6 34 F6 FA 7F 00 00 02 00 00 00 00 00 00 00 
08 00 00 00 00 00 00 00 FF 03 02 FF FF 05 01 04
FF 00 FF FF 07 FF 06 FF C9 9B 8D 69 A0 07 75 EE 
70 3B 52 59 81 01 00 00 30 D1 A4 59 81 01 00 00 
56 06 4F 43 E2 CD 7B 98 B0 36 52 59 81 01 00 00 
D8 2C 6C F6 FA 7F 00 00 76 EE 39 60 58 8D 39 A8 
70 90 56 59 81 01 00 00 D8 2C 6C F6 FA 7F 00 00 
D2 82 3C D5 75 36 10 29 B0 90 56 59 81 01 00 00 
30 23 35 59 81 01 00 00 47 68 1D 11 29 A0 32 E2 
F0 90 56 59 81 01 00 00 D8 2C 6C F6 FA 7F 00 00 
65 2F A1 52 18 D5 88 EF B0 A9 54 59 81 01 00 00 
80 D1 A4 59 81 01 00 00 2E 58 9B 8F 2C E9 02 73 
30 8D 58 59 81 01 00 00 30 EB 56 59 81 01 00 00 
1E 34 1E 61 96 F4 1B 1D 30 89 A2 59 81 01 00 00 
70 69 52 59 81 01 00 00

d:
01 00 00 00 00 00 00 00 B0 88 6B F6 FA 7F 00 00 
08 00 00 00 00 00 00 00 A0 55 00 00 00 00 00 00 
10 0C A4 59 81 01 00 00

01 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 
50 A6 34 F6 FA 7F 00 00 02 00 00 00 00 00 00 00 
08 00 00 00 00 00 00 00 FF 07 03 FF FF 00 02 05
06 04 FF FF FF FF 01 FF 65 2F A1 52 18 D5 88 EF 
B0 A9 54 59 81 01 00 00 80 D1 A4 59 81 01 00 00 
2E 58 9B 8F 2C E9 02 73 30 8D 58 59 81 01 00 00 
30 EB 56 59 81 01 00 00 56 06 4F 43 E2 CD 7B 98 
B0 36 52 59 81 01 00 00 D8 2C 6C F6 FA 7F 00 00 
D2 82 3C D5 75 36 10 29 B0 90 56 59 81 01 00 00 
30 23 35 59 81 01 00 00 C9 9B 8D 69 A0 07 75 EE 
70 3B 52 59 81 01 00 00 30 D1 A4 59 81 01 00 00 
76 EE 39 60 58 8D 39 A8 70 90 56 59 81 01 00 00 
D8 2C 6C F6 FA 7F 00 00 47 68 1D 11 29 A0 32 E2 
F0 90 56 59 81 01 00 00 D8 2C 6C F6 FA 7F 00 00 
1E 34 1E 61 96 F4 1B 1D 30 89 A2 59 81 01 00 00 
70 69 52 59 81 01 00 00

使用以上方法,我对比作用域?结构与内存结构的区别,结果是,搜索到的疑似作用域与dir()的顺序不一样,dir()对其进行了排序。就纯粹的结构来说,搜索到的内容是和字典的数据区内容是近乎一样的。根据字典的结构,我搜索到内存中有且只有一处有该块的地址。

在这里插入图片描述

最终找到了疑似_io._IOBase(class io.IOBase),无果,告一段落!

在Python中,从变量到对象的连接称作引用。也就是说,引用是一种关系,通过内存中的指针的形式来实现。

有C语言编程背景的读者可能会发现Python的引用很像C的指针(内存地址)。事实上,引用的实现非常像指针,而且他们通常扮演者和指针相同的角色,尤其是对于那些能够原位置改变的对象。
然而,因为引用在使用时总是被自动解引用,你永远不可能对引用本身做任何有用的操作;这是一个能够避免一系列C语言中错误的语言特性。

一旦使用(引用)变量,Python自动跟踪这个变量到对象的连接。

  • 变量是一个系统表的入口,包含了指向对象的连接
  • 对象是被分配到的一块内存,有足够的空间去表示他们所代表的值
  • 引用时自动形成的从变量到对象的指针

从概念上讲,每次通过运行一个表达式生成一个新的值,Python都创建一个新的对象(一块内存)表示这个值
从内部来看,作为一种优化手段,Python缓存了这一类不可变的对象对其进行复用。
从逻辑角度,这工作起来像每一个表达式结果的值都是一个不同的对象,而每一个对象都是不同的内存
从技术上讲,每一个对象都有两个标准的头部信息:类型标志符(type designator)表示这个对象的类型;引用计数器(reference counter)决定何时回收这个对象。

reference:参考,引用

for i in range(-10,260):
   a=eval('%s'%i)
   b=eval('%s'%i)
   if not a is b:print(i)
   
-10
-9
-8
-7
-6
257
258
259

for i in range(0x110000):
   c=chr(i)
   if c.isprintable():
       a=eval('%s'%repr(c))
       b=eval('%s'%repr(c))
       if not a is b:
           print(i,c)
           break
           
256 ?

a='香'

b='香'

a is b
Out[8]: False

a=1000

b=1000

a is b
Out[13]: False

a,b=1000,1000

a is b
Out[15]: True

a='a'

b='a'

a is b
Out[18]: True

a='iqdb'

b='iqdb'

a is b
Out[24]: True

关于’a’,我并不确定每次开机是否会生成,毕竟去看’a’的内存,他并不像是1等数字,连贯的排列的。但看结果,0~FF,即ascii、latin1,他们的内存指向是一样的,而且单词中,是一样的

for i in range(0x100):
   c=chr(i)
   a=eval('{0}{0}'.format(repr(c)))
   b=eval('{0}{0}'.format(repr(c)))
   if a is b:
       print(c,end='')
       
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

字母、数字、下划线,这岂不是变量名规则的三元素,于是这个规则可能是为了变量名而出现的,为了遍历变量名,而出现了应用的规则,顺便应用到字符串的判断中去,至少写了一个2200个字符的字符串,仍然支持判断。

类型属于对象,而不是变量

a=3
a='spam'
a=1.23
  • a是一个整数
  • a变成了一个字符串
  • a最后变成了一个浮点数
    这对于C程序员来说,是很奇怪的,因为当a='spam’时,a的类型似乎从整数变成了字符串。

事实并非如此,在Python中,变量名没有类型。类型属于对象,而不是变量名。
我们只是把a修改为对不同的对象的引用。因为变量没有类型,我们实际上并没有改变变量a的类型,只是让变量引用了不同类型的对象而已。
实际上,Python的变量就是在特定的时间引用了一个特定的对象。

对象知道自己的类型,每个对象都包含一个头部信息,其中标记了这个对象的类型。

我经常使用mem.data展示内存中,变量的内容,结构等,这是我自己写的一个.py,使用了ctype模块,开始时,mem.data是包含头部信息的,但之后我觉得没必要,所以我取消了头部信息

a=233621

id(a)
Out[5]: 2612324266128

import os

os.getpid()
Out[7]: 7216

import mem

mem.data(a)
01 00 00 00 00 00 00 00 95 90 03 00

变量a引用的对象的内容:[intel架构下,内存存储是小端]
01 00 00 00 00 00 00 00 D0 1F 0A FA FA 7F 00 00 [引用计数器] [数据类型]
01 00 00 00 00 00 00 00 95 90 03 00             [区块/正负]  [233261]

D0 1F 0A FA FA 7F 00 00:
EE 00 00 00 00 00 00 00 60 4C 0A FA FA 7F 00 00 [type]
00 00 00 00 00 00 00 00 80 CB EF F9 FA 7F 00 00 [int]

b=233621

a is b
Out[11]: False
# 此时a的内容中,引用计数器仍是1,引用计数器立刻就要见到了

b=a
# 引用计数器+1,变为2

对象的垃圾收集

a=3
a='spam'

当a再次被赋值时,如果原来的对象没有被其他的变量名或对象所引用的话,那么值钱的对象占用的空间就会被回收。这种自动回收对象空间的技术叫作垃圾回收。

>>> import mem
>>> a=3
>>> mem.data(a)
0c 01 00 00 00 00 00 00 d0 1f c2 42 fe 7f 00 00
01 00 00 00 00 00 00 00 03 00 00 00
>>> id(a)
2848214182256
>>> a='spam'
>>> mem.data(a)
03 00 00 00 00 00 00 00 40 51 c2 42 fe 7f 00 00
04 00 00 00 00 00 00 00 41 48 88 63 50 d0 19 70
e5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
73 70 61 6d 00
>>> id(a)
2848250987440
>>> mem.data(mem)
04 00 00 00 00 00 00 00 90 29 c2 42 fe 7f 00 00
00 94 4b 27 97 02 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70 7f da 26 97 02 00 00 65 27 00 00 00 00 00 00
00 00 00 00 00 00 00 00
>>> mem.data(mem.data)
04 00 00 00 00 00 00 00 50 0c c2 42 fe 7f 00 00
a0 7e 49 27 97 02 00 00 00 94 4b 27 97 02 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 13 1e 27 97 02 00 00
b0 66 d2 26 97 02 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 70 7f da 26 97 02 00 00
00 00 00 00 00 00 00 00 b0 66 d2 26 97 02 00 00
60 ae 8a 42 fe 7f 00 00 00 00 00 00 00 00 00 00
40 05 00 29 97 02 00 00

a两次的id——对象的内存地址,不一样,3是个预设的数值变量,被多次引用是理所当然,上面是python交互界面下的,被引用了258次,而jupyter QtConsole下,被引用了912次。不过令人惊讶的是’spam’,被引用了3次,目前我并未知晓追溯引用的方法,就例如我仍为知晓dir()对象的地址一样,但是我想尝试着搜索下!

但内存结果却是1,于是我推测是运行的时候发生的问题。例如方法传递时,生成了一个临时的在方法的局部区域的a的对象。

于是将代码在交互中运行试试!

>>> import ctypes
>>> id(a)
2848250987440
>>> import sys
>>> sys.getsizeof(a)
53
>>> ctypes.string_at(2848250987440,53).hex()
'01000000000000004051c242fe7f000004000000000000004148886350d01970e50000000000000000000000000000007370616d00'
>>> ctypes.string_at(id(a),sys.getsizeof(a)).hex()
'01000000000000004051c242fe7f000004000000000000004148886350d01970e50000000000000000000000000000007370616d00'

a='spam'

import sys,ctypes

def data(d):
   b=ctypes.string_at(id(d),sys.getsizeof(d))
   for i in range(0,len(b),16):
       print(b[i:i+16].hex(' '))

ctypes.string_at(id(a),sys.getsizeof(a)).hex()
Out[4]: '01000000000000004051c242fe7f00000400000000000000c87c1fc23e376330e50000000000000000000000000000007370616d00'

data(a)
03 00 00 00 00 00 00 00 40 51 c2 42 fe 7f 00 00
04 00 00 00 00 00 00 00 c8 7c 1f c2 3e 37 63 30
e5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
73 70 61 6d 00

a='spam'

import sys,ctypes

def data(d):
   '查看数据在内存中的形态\nctypes.string_at(id(d),sys.getsizeof(d)).hex(' ')'
   b=ctypes.string_at(id(d),sys.getsizeof(d))
   for i in range(0,len(b),16):
       print(b[i:i+16].hex(' '))
       

ctypes.string_at(id(a),sys.getsizeof(a)).hex()
Out[4]: '01000000000000004051c242fe7f00000400000000000000c87c1fc23e376330e50000000000000000000000000000007370616d00'

data(a)
03 00 00 00 00 00 00 00 40 51 c2 42 fe 7f 00 00
04 00 00 00 00 00 00 00 c8 7c 1f c2 3e 37 63 30
e5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
73 70 61 6d 00

def data(d):
   print(ctypes.string_at(id(d),sys.getsizeof(d)))
   

data(a)
b'\x03\x00\x00\x00\x00\x00\x00\x00@Q\xc2B\xfe\x7f\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\xc8|\x1f\xc2>7c0\xe5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00spam\x00'

i=233621

ctypes.string_at(id(i),sys.getsizeof(i)).hex()
Out[10]: '0100000000000000d01fc242fe7f0000010000000000000095900300'

data(i)
b'\x03\x00\x00\x00\x00\x00\x00\x00\xd0\x1f\xc2B\xfe\x7f\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x95\x90\x03\x00'

def data(d):
   id_=id(d)
   size_=sys.getsizeof(d)
   del d
   print(ctypes.string_at(id_,size_))
   

data(i)
b'\x02\x00\x00\x00\x00\x00\x00\x00\xd0\x1f\xc2B\xfe\x7f\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x95\x90\x03\x00'

def data(d):
   dir()
   

data(i)

def data(d):
   print(dir())
   

data(i)
['d']

这里的细节是,通过方法运行,瞬时内多了两次引用,即便在函数中删除一个变量,仍多一个引用,这里我尝试获取全局中是否有变量变化!

>>> a='spam'
>>> import ctypes
>>> id_=id(a)
>>> int.from_bytes(ctypes.string_at(id_,1),byteorder='little')
1
>>> def data(d):
...     print(int.from_bytes(ctypes.string_at(id_,1),byteorder='little'))
...
>>> data(a)
3
>>> def data(d):
...     def data1(d):
...             print(int.from_bytes(ctypes.string_at(id_,1),byteorder='little'))
...     data1(d)
...
>>> data(a)
5

…全局变量是全局,局部变量是局部,额外多出的那个可能是他们间的传递的额外消耗!未知未果~

x=42
x='shrubbery'
x=3.1415
x=[1,2,3]

当每一次x被赋值给一个新的对象,Python都会回收之前对象的空间:
例如当他赋值为’shrubbery’时,对象42马上被回收(假设没有其他对象引用);
对象的空间自动放入自由内存空间池,等待后来的对象使用

Python是这样来实现这一功能的:
他在每个对象中保留一个计数器,计数器记录当前指向该对象的引用数目,一旦这个计数器被设置为零,这个对象的内存空间就会自动回收。
假设每次x都被赋值给一个新的对象,而前一个对象的引用计数器变为零,就会导致它的空间被回收。

垃圾回收最之家、可感受的好处就是可以再脚本中任一使用对象而不需要考虑申请或释放内存空间。
在程序运行时,Python将会清理这些不再使用的空间,与C和C++这样的底层语言相比,这样做省去了大量的基础代码。

关于Python垃圾回收的更多讨论

Python的垃圾收集主要基于引用计数器。然而他也有一部分组件可以及时的检测并回收带有循环引用的对象。
如果你确保自己的代码没有产生循环引用,可以关闭这部分功能,但该功能是默认可用的。

循环引用是给予引用计数器的垃圾回收需要面对的经典问题。
由于引用实现为指针,一个对象有可能会引用自身,或者引用另一个引用了自身的对象。例如L.append(L)
对来自用户定义的类的对象的属性赋值的时候,会产生同样的对象。尽管相对很少,由于这样的对象的引用计数器不会清零,必须特别对待他们。

要了解Python的循环检测器的更多细节,参见库手册gc模块(30.12 gc — 垃圾回收器接口)。
好消息是,基于垃圾回收的内存管理已经在Python中为你实现好了,并且是由专人精心设计过。
但这里介绍的垃圾收集器只适用于CPython;诸如Jython、IronPython和PyPy的其他实现方案可能采用不同的机制,但最后的效果都是类似的,为使用的内存空间都会被自动回收,尽管不一定如标准Python中那般迅速。

import mem

l=['spam']
d=id(l[0])
mem.ds(d,48)
l1=l
mem.ds(d,1)
l2=l.copy()
mem.ds(d,1)
l[:]
mem.ds(d,1)
l3=l[:]
mem.ds(d,1)
l.copy()
mem.ds(d,1)
del l,l1,l2,l3
mem.ds(d,48)
input()

这段代码,理想的结果是

01
01
02
03
04
04
01

但同时运行时,ipython的结果是223341、python实际运行环境是334452
这里要注意的是l[:]
1、l[:]相当于l.copy()
2、但是这里并没有赋值,该情况却是多了不知何来的一个引用
3、最后的那个1不一定意味着引用是1,说不定是已经是随时可写入的内存空间,只是还没其他数据写入

import mem

l=['spam']
d=id(l[0])
mem.ds(d,1)
l[:]
mem.ds(d,1)
l[:]
mem.ds(d,1)
del l
mem.ds(d,1)
input()

总之,这个在不同环境下,不同次序下,仍旧结果不同,好在实际并不是特别影响结果,但我也遇到了影响的情况,例如之前用大数值观测内存状态,在之后就是有一个不明的内存占用,不是for i的i

之前我提到过我不知道如何获取作用域变量,刚才我看到了一个方法,虽然用着有问题,但毋庸置疑他是全局作用域

globals()
Out[2]: 
{'__name__': '__main__',
 '__doc__': 'Automatically created module for IPython interactive environment',
 '__package__': None,
 '__loader__': None,
 '__spec__': None,
 '__builtin__': <module 'builtins' (built-in)>,
 '__builtins__': <module 'builtins' (built-in)>,
 '_ih': ['',
  "import mem\n\nl=['spam']\nd=id(l[0])\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\ndel l\nmem.ds(d,1)\ninput()",
  'globals()'],
 '_oh': {1: ''},
 '_dh': ['C:\\Users\\Jone\\AppData\\Local\\Programs\\Python\\Python39\\Scripts'],
 'In': ['',
  "import mem\n\nl=['spam']\nd=id(l[0])\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\ndel l\nmem.ds(d,1)\ninput()",
  'globals()'],
 'Out': {1: ''},
 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x000002405DDECF70>>,
 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x2405ddec5b0>,
 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x2405ddec5b0>,
 '_': '',
 '__': '',
 '___': '',
 '_i': "import mem\n\nl=['spam']\nd=id(l[0])\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\ndel l\nmem.ds(d,1)\ninput()",
 '_ii': '',
 '_iii': '',
 '_i1': "import mem\n\nl=['spam']\nd=id(l[0])\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\nl[:]\nmem.ds(d,1)\ndel l\nmem.ds(d,1)\ninput()",
 'mem': <module 'mem' from 'C:\\code\\mem.py'>,
 'd': 2475476499376,
 '_1': '',
 '_i2': 'globals()'}

dir()
Out[3]: 
['In',
 'Out',
 '_',
 '_1',
 '_2',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_i2',
 '_i3',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 'd',
 'exit',
 'get_ipython',
 'mem',
 'quit']

def __clear_env():
    for key in globals().keys():
        if not key.startswith("__"): # 排除系统内建函数
            globals().pop(key)

__clear_env()
RuntimeError: dictionary changed size during iteration 字典在迭代期间改变了大小

关于模块内置函数全局变量的帮助:
全局变量()
返回包含当前作用域全局变量的字典。
注意:本字典的更新**影响当前的名称查找
全局范围,反之亦然。

这里使用list(globals().keys()),以及根据实际情况排除系统内建,例如jupyter下,不只是__开头,还有_开头
很奇妙的是,globals().pop(key)却是得到了想要的执行结果,于是globals(),他返回的是一个确定的对象,而不是计算出的对象,而且是个字典,所以可以使用.pop(key)。

例如dir()就是类似于sorted(list(globals()))的运算结果,而globals()类似于return a。

共享引用

import ctypes,sys

a='spam'

b=a

ctypes.string_at(id(a),sys.getsizeof(a)).hex()
Out[6]: '020000000000000040514185fd7f000004000000000000002286248b3dcbf51fe50000000000000000000000000000007370616d00'

b is a,a is b
Out[7]: (True, True)

变量a和b都引用了相同的对象,即指向了相同的内存空间

a='spam'
b=a
globals()

{'__name__': '__main__',
 '__doc__': None,
 '__package__': None,
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000231269FD550>,
 '__spec__': None,
 '__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>,
 '__file__': 'A:\\0.py',
 '__cached__': None,
 'a': 'spam',
 'b': 'spam'}

在这里插入图片描述

运行赋值语句b=a之后的变量名和对象,变量b成为对象3的一个引用。
在内部,变量实际上是一个指针指向对象的内存空间,该内存空间是通过运行字面量表达式3创建的。

这种情况在Python中称为共享引用或共享对象,即多个变量名引用了同一个对象。
名字a和b此时并没有彼此关联;实际上,Python中不可能发生两个变量相互关联。
真实情况是两个变量通过他们的引用指向同一个对象。

import mem

mem.mem()
Out[2]: '内存占用:52.97 MB'

s=' '*2**20*100

mem.mem()
Out[4]: '内存占用:152.99 MB'

l=[s]

mem.mem()
Out[6]: '内存占用:153.00 MB'

del s

mem.mem()
Out[8]: '内存占用:153.01 MB'

del l

mem.mem()
Out[10]: '内存占用:52.97 MB'

for i in range(10):
	a='spam'*i
	b='spam'*i
	print(a is b)
True
True
False
False
False
False
False
False
False
False

# 在for中,公式结果失效

i=5
s1,s2='a'*2**i,'a'*2**i
s1 is s2
Out[13]: False

s1,s2='a'*2**5,'a'*2**5
s1 is s2
Out[15]: True

for i in range(5):
    s1,s2=eval("'a'*2**%i"%i),eval("'a'*2**%i"%i)
    print(id(s1),id(s2),i)
    del s1,s2
    
2005577665200 2005577665200 0
2005626051696 2005626051696 1
2005646527152 2005646527152 2
2005646527344 2005646527344 3
2005646454256 2005646454256 4

for i in range(2**11,2**13):
    s1,s2=eval("'a'*%i"%i),eval("'a'*%i"%i)
    if not s1 is s2:
        print(id(s1),id(s2),i)
        break
    del s1,s2
    
2005636883872 2005639974736 4097

2**12
Out[28]: 4096

64*64
Out[30]: 4096
# 当字符串的长度大于等于4096时,不会再与之前的字符长度相同

在这里插入图片描述

与其他一些语言不同,在Python中,变量总是一个指向对象的指针,而不是改变内存区域的标签:给一个变量复制一个新值,并不是替换原始的对象,而是让这个变量去引用完全不同的一个对象。实际的效果就是对一个变量赋值,仅仅会影响那个被赋值的变量。
然而当可变的对象以及原位置的改变进入这个场景时,是另一种情况…↓

共享引用和在原位置修改

有一些对象和操作(包括列表、字典和集合在内的Python可变类型),确实会在原位置改变对象。
例如,在一个列表中对一个偏移进行赋值确实会改变这个列表对象,而不是生产全新的列表对象。

但确实可能生成了新的对象,例如这个被赋值或增加的偏移的元素,入过在程序中是全新的,他会线生成一个全新的对象,通过指针挂载到列表上,此时对象的引用是1,是被列表的索引(偏移)所引用。
列表的内存结构是一个个指向元素对象的指针的队列。

这种特殊性有时候会对你的代码造成很大的影响,对于支持这种在原位置修改的对象,共享引用时的确需要加倍小心,因为对一个变量名的修改会影响其他的变量。否则,你的对象可能会莫名的发生改变。考虑到所有的赋值都是基于引用(包括函数传参)的,这种风险普遍存在。

很多时候,涉及到列表和函数,我都是不传参,直接调用全局的列表变量名,函数的全局变量调用的规则是,不改变变量的对象即可,这里指的是对象而非对象内容,于是需要操作的是全局的可变对象的内容,我就会直接调用全局变量名,当然前提是我了解并的确是在使用这个机制,我知道我在干什么!

i,l=10,[1]

def f():
   print(i)
   

f()
10

def f():
   print(i)
   i=1
   

f()
UnboundLocalError: local variable 'i' referenced before assignment

def f():
   l.append('a')
   l.append(i)
   print(l)
   

f()
[1, 'a', 10]
L1=[2,3,4]

L2=L1

L1 is L2
Out[35]: True

L2
Out[37]: [2, 3, 4]

L1=24

L1
Out[39]: 24

L2
Out[40]: [2, 3, 4]

L1=L2

L1[0]=24

L2
Out[44]: [24, 3, 4]

这里我们并没有改变L1,而是改变了L1所引用的对象的一个元素。
这类修改会在原位置覆盖列表对象中的某部分值。因为这个列表对象是与其他对象共享的(被其他对象引用),那么一个像这样在原位置的改变不仅仅会对L1有影响。也就是说,你必须意识到当作了这样的修改时,他会影响程序的其他部分。

i=‘spam’

import ctypes

d=id(i)

ctypes.string_at(d)
Out[48]: b’\x01’

def f():
print(ctypes.string_at(d))

f()
b’\x01’

def f():
i
print(ctypes.string_at(d))

f()
b’\x01’
def f():
global i
print(ctypes.string_at(d))

f()
b’\x01’
def f(i):
print(ctypes.string_at(d))

f(i)
b’\x03’

ctypes.string_at(d)
Out[58]: b’\x01’

def f(a):
#print(id(locals()))#与globals()不同,locals()会多引用一次对象,而且只是个结果反馈,对他的删除操作并不会影响作用域变量
input()

def f(a):
print(id(locals()))
for i in locals():
pass
del locals()[i]
print(a)
input()

这里,函数内使用了全局变量,但其实并不能证明全局变量在函数中使用时,是否增加了引用,因为调用后可能被释放了,但global后仍是1,而之后的参数传递,又回到了上面提到的问题,在参数传递后,函数中,引用多了两个。函数执行后,引用又变成了1
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210408165001555.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2poc3h5MjAwNQ==,size_16,color_FFFFFF,t_70#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210408165021341.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2poc3h5MjAwNQ==,size_16,color_FFFFFF,t_70#pic_center)
这三个不一样的内存,大概就是多出来的引用,除了最后一个明显的是locals(),另外两个并没有遵循字典格式

这种行为仅针对原位置改变的可变对象,并且通常来说就是你想要的效果,但你应该了解他是如何运作的,以让他按照预期去工作。这也是默认形式:如果你不想要这样的现象发生,可以请求Python复制(copy)对象,而不是创建引用。

复制一个列表的方法有很多种,包括使用内置list函数或者库的copy模块。也许最常用的办法就是从头到尾的切片。

L1=[2,3,4]

L2=L1[:]

L1[0]=24

L1
Out[5]: [24, 3, 4]

L2
Out[6]: [2, 3, 4]
import copy
copy.deepcopy?
Signature: copy.deepcopy(x, memo=None, _nil=[])
Docstring:
Deep copy operation on arbitrary Python objects.

L1=[[1],2,3]

L2=L1[:]

L1[0].append(1)

L1
Out[4]: [[1, 1], 2, 3]

L2
Out[5]: [[1, 1], 2, 3]

L1[0]=666

L1
Out[7]: [666, 2, 3]

L2
Out[8]: [[1, 1], 2, 3]

L1=L2.copy()

L1[0].pop()
Out[10]: 1

L2
Out[11]: [[1], 2, 3]

import copy

L2=copy.copy(L1)

L1[0].append(2)

L2
Out[16]: [[1, 2], 2, 3]

L1=copy.deepcopy(L2)

L1[0].pop()
Out[18]: 2

L2
Out[19]: [[1, 2], 2, 3]

L2=L1.copy() # 119 ns
L2=L1[:] # 138 ns
L2=copy.copy(L1) # 498 ns
L1=[233621] # 233621,引用次数1

L1[:] # 2
Out[2]: [233621]

id(L1[0])
Out[3]: 2254827380592

import os

os.getpid()
Out[5]: 1092

L2=L1[:] # 3

L3=L1.copy() # 4

import copy

L4=copy.copy(L1) # 5

globals()
Out[10]: 
{'__name__': '__main__',
 '__doc__': 'Automatically created module for IPython interactive environment',
 '__package__': None,
 '__loader__': None,
 '__spec__': None,
 '__builtin__': <module 'builtins' (built-in)>,
 '__builtins__': <module 'builtins' (built-in)>,
 '_ih': ['',
  'L1=[233621]',
  'L1[:]',
  'id(L1[0])',
  'import os',
  'os.getpid()',
  'L2=L1[:]',
  'L3=L1.copy()',
  'import copy',
  'L4=copy.copy(L1)',
  'globals()'],
 '_oh': {2: [233621], 3: 2254827380592, 5: 1092},
 '_dh': ['C:\\Users\\Jone\\AppData\\Local\\Programs\\Python\\Python39\\Scripts'],
 'In': ['',
  'L1=[233621]',
  'L1[:]',
  'id(L1[0])',
  'import os',
  'os.getpid()',
  'L2=L1[:]',
  'L3=L1.copy()',
  'import copy',
  'L4=copy.copy(L1)',
  'globals()'],
 'Out': {2: [233621], 3: 2254827380592, 5: 1092},
 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x0000020CFE27DF70>>,
 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x20cfe27d5b0>,
 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x20cfe27d5b0>,
 '_': 1092,
 '__': 2254827380592,
 '___': [233621],
 '_i': 'L4=copy.copy(L1)',
 '_ii': 'import copy',
 '_iii': 'L3=L1.copy()',
 '_i1': 'L1=[233621]',
 'L1': [233621],
 '_i2': 'L1[:]',
 '_2': [233621],
 '_i3': 'id(L1[0])',
 '_3': 2254827380592,
 '_i4': 'import os',
 'os': <module 'os' from 'c:\\users\\jone\\appdata\\local\\programs\\python\\python39\\lib\\os.py'>,
 '_i5': 'os.getpid()',
 '_5': 1092,
 '_i6': 'L2=L1[:]',
 'L2': [233621],
 '_i7': 'L3=L1.copy()',
 'L3': [233621],
 '_i8': 'import copy',
 'copy': <module 'copy' from 'c:\\users\\jone\\appdata\\local\\programs\\python\\python39\\lib\\copy.py'>,
 '_i9': 'L4=copy.copy(L1)',
 'L4': [233621],
 '_i10': 'globals()'}
# 有两个可疑的变量:'___': [233621], '_2': [233621],但执行del后,并没有引发引用数减少的情况,使用指针逆推,我现在想写一个指针逆推的函数,但他要建立在搜索内存的基础上的,除非吃透了作用域,不需要内存搜索字符串。我查了下,没有查到相关库,还是继续用HxD吧!

在这里插入图片描述

这三个是运行时增加的三个指针,但并不像字典的格式,其中两个既是增加的引用,更多细节无从得知。

共享引用和相等

x=42
x='shrubbery' # 42被回收了吗

for i in range(-10,260):
    t1=eval('%s'%i)
    t2=eval('%s'%i)
    if not t1 is t2:print(i)
    
-10
-9
-8
-7
-6
257
258
259

a='a'*4096

a1='a'*4096

a is a1
Out[6]: True

b='b'

mem.data(b)
d8 00 00 00 00 00 00 00 40 51 ba 88 fd 7f 00 00
01 00 00 00 00 00 00 00 41 66 fc 06 b1 44 92 65
e5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
62 00

因为Python缓存并复用了小的整数(-5~256)和小的字符串(ascii字符,1、a、A组成的小于等于4096的字符串)。
这里,对象42也许并不一定会被回收;相反的,他将可能仍被保存在一个系统表中,等待下一次的你代码生成另一个42来重复利用。尽管这样,大部分种类的对象都会在不被引用时马上回收;对于那些不会被回收的对象,缓存机制并不影响你所编写的代码。

我搜索b的第二组字节串,即str的类型指针,得到87765个结果,同时搜索二三组,得到3548个结果
在ipython和python,运行机制是不一样的,ipython的一个语句块相当于一个python脚本,两个语句块不能同时使用一个临时变量/参数/环境

chr(0x7826)
Out[31]: '砦'

id(chr(0x7826))
Out[32]: 2625899812784

id(chr(0x7826))
Out[33]: 2625899813344

print(id(chr(0x7826)))
print(id(chr(0x7826)))
2625899843792
2625899843792

c=0
for i in range(0x110000):
   d1=id(chr(i))
   d2=id(chr(i))
   if d1 == d2:
       print(hex(i))
       c=0
   else:        
       c+=1
       if c==1000:break

# 因为我想尝试python有哪些预设字符,失败

因为上面发现ipython的特性后,我打开python

>>> a='spam'
>>> import mem
>>> mem.data(a)
03 00 00 00 00 00 00 00 40 51 ba 88 fd 7f 00 00
04 00 00 00 00 00 00 00 63 2b 83 0b a6 f7 38 8d
e5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
73 70 61 6d 00

这里的str类型标识和ipython一样的,两次的存储地址一致
搜索40 51 ba 88 fd 7f 00 00 01 00 00 00 00 00 00 00,得到103个结果,只搜索标识的话,9274个结果

我使用HxD的保存功能,将python的内存片段保存,并打算对字符串进行处理,将1个长度的字符的引用数和内容输出出来。

with open(r'C:/Users/Jone/Documents/HxD/python - 选区.exe (12764)','rb') as f:
   b=f.read()

l=[]
t=bytes.fromhex('40 51 ba 88 fd 7f 00 00 01 00 00 00 00 00 00 00')
for i in range(8,len(b),16):
   if b[i:i+16]==t:l.append(b[i+40])

for i in l:
   print(chr(i),end='')# repr(chr(i))

7 '\x07' 响铃
8 '\x08' 退格
9 '\t' 制表
10 '\n' 换行
11 '\x0b' 行表
12 '\x0c' 换页
13 '\r' 回车

32 ' '
       ! "
35 '#'
36 '$'
37 '%'
38 '&'
39 "'"
40 '('
41 ')'
42 '*'
43 '+'
44 ','
45 '-'
46 '.'
47 '/'
48 '0'
49 '1'
50 '2'
51 '3'
52 '4'
53 '5'
54 '6'
55 '7'
56 '8'
57 '9'
58 ':'
59 ';'
60 '<'
61 '='
62 '>'
63 '?'
       @
65 'A'
66 'B'
67 'C'
68 'D'
69 'E'
70 'F'
71 'G'
72 'H'
73 'I'
74 'J'
75 'K'
76 'L'
77 'M'
78 'N'
79 'O'
80 'P'
81 'Q'
82 'R'
83 'S'
84 'T'
85 'U'
86 'V'
87 'W'
88 'X'
89 'Y'
90 'Z'
91 '['
92 '\\'
93 ']'
94 '^'
95 '_'
       ` 抑音符
97 'a'
98 'b'
99 'c'
100 'd'
101 'e'
102 'f'
103 'g'
104 'h'
105 'i'
106 'j'
107 'k'
108 'l'
109 'm'
110 'n'
111 'o'
112 'p'
113 'q'
114 'r'
115 's'
116 't'
117 'u'
118 'v'
119 'w'
120 'x'
121 'y'
122 'z'
123 '{'
124 '|'
125 '}'
126 '~'
       del

python中,预设的单字符,是ascii

s=set()
t=bytes.fromhex('40 51 ba 88 fd 7f 00 00 01 00 00 00 00 00 00 00')
for i in range(8,len(b),16):
   if b[i:i+16]==t:
       if b[i+24]==228 or b[i+24]==229:
           s.add(b[i+40])
len(s)
Out[25]: 128
# 这是jupyter QtConsole,暂时只是获取ascii的部分,但是已经是ascii全覆盖了
s=set()
t=bytes.fromhex('40 51 ba 88 fd 7f 00 00 01 00 00 00 00 00 00 00')
for i in range(8,len(b),16):
   if b[i:i+16]==t:s.add(b[i+40:i+48])

len(s)
Out[28]: 149
# 这里暴力的增加,也不过是多了21个字符,而且多是无效、重复的ascii,可能有效的,例如?

给予Python的引用模型,在Python程序中有两种不同的方法去检查是否相等:

L=[1,2,3]

M=L

L==M
Out[6]: True

L is M
Out[7]: True

==运算符,测试两个被引用的对象是否有相同的值,这种方法往往在Python中用作相等的检查。
is运算符,是在检查对象的同一性。如果两个变量名精确的指向同一对象,他会返回True。
这是一种更加严格形式的相等测试,他在大多数的程序中很少出现。

is只是比较实现引用的指针,所以如果有必要的话,他是代码中检测共享引用的一种方法。
入过变量名引用值相等,但是对象不同,他的返回值将是False:

L=[1,2,3]

M=[1,2,3]

L==M
Out[11]: True

L is M
Out[12]: False
L=[1,2,3]

M=L.copy()

L==M
Out[15]: True

L is M
Out[16]: False

L[0] is M[0]
Out[17]: True

L[:] is M[:]
Out[18]: False

import copy

M=copy.deepcopy(L)

L[0] is M[0]
Out[21]: True

L=[[1],2,3]

M=copy.deepcopy(L)

L[0] is M[0]
Out[24]: False

L[0][0] is M[0][0]
Out[25]: True

L=['abc','d e','香','香香']

M=copy.deepcopy(L)

L[0] is M[0]
Out[28]: True

L[1] is M[1]
Out[29]: True

L[2] is M[2]
Out[30]: True

L[3] is M[3]
Out[31]: True

l='d e'

m='d e'

l is m
Out[34]: False

l='d e'
m='d e'

l is m
Out[36]: False

l='香'

m='香'

l is m
Out[39]: False

L=[['a b'],['香']]

M=copy.deepcopy(L)

L[0][0] is M[0][0]
Out[42]: True

L[1][0] is M[1][0]
Out[43]: True

L=['a b','a b']

L[0] is L[1]
Out[45]: True

L.append('a b')

L[1] is L[2]
Out[47]: False

M=copy.deepcopy(L)

M[1] is M[2]
Out[49]: False

这里呈现的结果,copy尽可能的减少对象的开支,入过对象是不可变的,那就引用源对象,如果是可变的,重新创建,但是可变的里面不可变的,依然是引用源对象

X=42

Y=42

X==Y
Out[52]: True

X is Y
Out[53]: True

X和Y应该是==的(具有相同的值),但不是is的(同一个对象),因为我们运行了两个不同的字面量表达式(42).
不过,因为小的证书和字符串被缓存并复用了,所以is告诉我们X和Y引用了一个相同的对象。

这里其实是X和Y的引用的42是预设值(-5~256),入过超过这个范围的数,is结果是False。同理,ascii的字符也是预设值,超出ascii的不享受复用,另外ascii的(1、a、A)类型的字符串可以在有引用时复用


y='香'

x is y
Out[56]: False

x='`'

y='`'

x is y
Out[59]: True

d=id(x)

del x,y

x='`'

d==id(x)
Out[63]: True

x='233621'

y='233621'

x is y
Out[66]: True

d=id(x)

del x,y

x='233621'

d==id(x)
Out[70]: False

如果确实想刨根问底的话,你总能向Python查询对一个对象引用的次数,在标准的sys模块中,getrefcount函数会返回对象的引用次数。

import sys

sys.getrefcount('spam')
Out[2]: 3

sys.getrefcount(233621) #如果一个数没被引用过,这个方法调用时,会在运算前,引用了3次
Out[3]: 3

sys.getrefcount('a') #python的引用次数是13
Out[4]: 247

a='spam'

sys.getrefcount('spam') #但如果提前有一个变量引用了'spam',引用数也是3次,思考下该怎么办?
Out[6]: 3

b=a

sys.getrefcount('spam')
Out[8]: 4

这种对象缓存和复用的机制与代码是没有关系的(除非你运行is检查)。因为不能再原位置不可变的数字和字符串,所以无论在同一个对象中有多少个引用都没有关系,所有的引用都会看到同样的不可改变的值。
然而,这种现象也反映了Python为了执行速度而采用的优化其模式的众多方法中的一种。

>>> mem.data('spam')
05 00 00 00 00 00 00 00 40 51 5e 87 fd 7f 00 00
04 00 00 00 00 00 00 00 52 87 8b 6a 7f c6 57 b4
e5 00 a0 01 65 02 83 00 00 00 00 00 00 00 00 00
73 70 61 6d 00

作用域的变量存储是采用字典格式,即变量名hash、变量名、对象,但是如果有4096以内的字符串复用的话,应该有个地方存储字符串的hash:

>>> a='spam'
>>> id(globals())
2170966823040
>>> spam=3

在这里插入图片描述
在这里插入图片描述
我看到一个疑似的地方,但是并没有看出他的指针结构,现在也并不想深究,只想验证下他确实是hash结构的,结果确实如此,因为我之后创建的spam变量,这里生成的spam的hash和我搜索到的spam是一样的。

动态类型随处可见

在使用Python的过程中,你的确没有必要去用圆圈和箭头画变量名/对象的框图。
尽管在刚入门的时候,它会帮助你跟踪他们的引用结构,理解不常见的情况,就想我们再这里所做的那样。
此外,尽管目前来说动态类型看起来有些抽象,你最终还是需要关注他的。
因为在Python中,任何东西看起来都是通过赋值和引用工作的,对这个模型有基本了解,在不同的场合都是很有帮助的。
就像你会看到的那样,他也会在赋值语句、变量参数、for循环变量、模块导入、类属性等很多场合发挥作用。
值得高兴的是,这是Python中唯一的赋值模型。一旦你对动态类型上手了,将会发现他在这门语言中任何地方都有效。

从最实际的角度来说,动态类型意味着你将写更少的代码。
尽管这样,同等重要的是,动态类型也是Python中多态的根本。
因为我们在Python代码中没有对类型进行约束,他具备了精确性和高度的灵活性。
就像你将会看到的那样,如果使用正确的话,动态类型所蕴含的多台思想产生的代码,可以自动的使用系统的新需求。

“弱”引用

你可能偶尔会在Python世界中看到“弱引用”的字眼,这是一个相对高级的工具,但也与我们这里讨论的引用模型有关系,就像is运算符,离开了他就没有办法理解。

简单来说,弱引用就是通过weakref标准库来实现的一种用于防止对象被垃圾回收的引用(这一引用并非来源于自身)。
如果对象的最后一次引用是弱引用,那么这个对象将被重新声明,而相应的弱引用会被自动删除(或被告知)。

例如,这对于字典的大对象缓存来说十分有用。否则,单靠缓存的引用并不能确保对象存在于内存中。这种情况仍然可以视作引用模型的一种特例。

python中的垃圾回收机制是是引用计数器。当一个对象的引用数目为0的时候,才会被从内存中回收。当出现循环引用的时候,垃圾回收就变得不可靠了。

因此一些没必要的引用就无需写入计数器中,这类引用就定义为弱引用,以避免对象无用后而不会仍然驻在内存中,造成内存泄露。

python3学习笔记之 强引用和弱引用
python 弱引用
强引用、软引用、弱引用的区别和解析
[Python] 高级用法 - 弱引用详解
Python 弱引用 学习
python弱引用_python 图 自身遍历 及弱引用使用
python中的弱引用weakref

本章小结

本章对Python的动态类型模型(也就是Python自动为我们跟踪对象的类型,不需要我们在脚本中编写声明语句)进行了深入的学习。
在这个过程中,我们学会了Python中变量和对象是如何通过引用关联在一起的,还探索了垃圾收集的感念,学到了对象共享引用如何影响多个变量的,并看到了Python中引用是如何影响相当的概念的。

因为在Python中只有一个复制类型,并且赋值在这门语言中的任意之处都能碰到,所以在继续学习之前掌握这个模型是很有必要的。

本章习题

1、思考下面三条语句。他们会改变A打印出的值吗?
A=“spam”
B=A
B=“shrubbery”
2、同上
A=[“spam”]
B=A
B[0]=“shrubbery”
3、同上
A=[“spam”]
B=A[:]
B[0]=“shrubbery”

习题解答

1、不会

B=A时,他们引用的对象是一个不可变量,而当B重新赋值时,会之为B进行对象的重新引用。
2、是
这里并没有改变A和B的引用,只是改变了引用对象(容器)的一个引用的对象(元素)
3、不会
分片表达式语句会在A被赋值给B前创建一个副本,这样B在原位置赋值就不会有影响。相当于copy,而非deepcopy。只要只存在或只操作一阶不可变对象,B的元素的改变不会影响到A

遗留问题,a[:]多出来的引用,以及函数传递时多出来的两个引用,未确定

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值