Skip to content

Latest commit

 

History

History
253 lines (167 loc) · 8.45 KB

104_理解动态语言.md

File metadata and controls

253 lines (167 loc) · 8.45 KB

变量、对象和引用

当我们输入a = 3时,既没有告诉Python将a作为一个变量来使用,也没有告诉它将a作为一个整数类型对象。在Python中,它是如下工作的:

  • 变量创建

    一个变量(变量名)当第一次给它赋值时就创建了它。之后的赋值将会改变已创建的变量名的值。从技术上讲,Python在代码运行之前先检测变量名,但你可以理解为最初的赋值操作在创建变量

  • 变量类型

    变量永远不会拥有任何和它关联的类型或约束。类型的概念存在于对象而不是变量名中。变量原本是通用的,它只是在一个特定的时间点,简单的引用了一个特定的对象而已

  • 变量使用

    当变量出现在表达式中,它会马上被当前引用的对象所替代,无论这个对象是什么类型。此外,所有变量必须在使用前被明确地赋值,使用未赋值的变量会产生错误

示例如下:

>>> a = 3

- 创建一个对象来达标值3
- 创建一个变量a,如果它还没有创建的话
- 将变量与新的对象相连接

在Python中从变量到对象的连接称作引用,也就是说引用是一种关系,通过内存中的指针来实现,一旦引用这个变量,Python自动追踪这个变量到对象的连接,具体术语如下:

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

至少从概念上讲,在脚本中,每一次通过运行一个表达式生成一个新的值,Python都创建了一个新的对象(一块内存)表示这个值。从内部来看,作为一种优化手段,Python缓存了这一类不可变的对象并对其进行复用,例如,小的整数和字符串。

从技术上讲,对象不仅仅有足够的空间表示它的值,还包含了更复杂的结构。每一个对象都有两个标准的头部信息:类型标志符(type designator)标识这个对象的类型;引用的计数器(reference counter)决定何时回收这个对象。

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

看下面的Demo实例:

>>> a = 3
>>> a = 'spam'
>>> a = 1.23

这不是常规的Python代码,但是它是可运行的。

在Python中,变量名没有类型。就像题目说的,类型属于对象,而不是变量名。就上面的例子来看,我们只是把a修改未不同的对象的引用。因为变量没有类型,我们实际上并没有改变变量a的类型,只是让变量引用了不同类型的对象而已。

对象的垃圾收集

看下面的Demo示例

>>> x = 42
>>> x = 'spam'
>>> x = 3.1415
>>> x = [1, 2, 3]

对象的引用值在此过程中会被逐个丢弃,没每一次x被赋值给一个新的对象,Python都会回收之前对象的空间。例如,当它赋值未字符串’spam‘,对象42马上被回收;对象的空间自动放入自由内存空间池,等待后来的对象使用。

在内部,Python是这样实现的,它在每个对象中保留了一个计数器,激素其记录当前指向该对象引用的数目,一旦这个计数器被设置为0,这个对象的内容空间就会自动回收。

垃圾回收的最直接的、可感受到的好处就是,这意味着可以在脚本中任意使用对象而不需要考虑沈或释放内存空间,在程序运行时,Python将会清理那些不再使用的空间。

sys.getrefcount

这个方法能够让我们直观的看到对象的计时器值

注意,不要用常量或ipython来试,例如常量1不只被你引用,内存中的1指向的都是一个常量。

计数模式

计数增加
  • 赋值给其他变量就会增加引用计数,例如x=3;y=x;z=[x,1]
  • 实参传参,如foo(y)
计数减少
  • 函数运行结束时,局部变量就会被自动销毁,对象的引用计数减少
  • 变量被赋值给其他对象,例如x=3;y=x;x=4

Demo

引用计数增加
import sys      # 导入python自带的sys库
x = []          # 将 x 赋值为一个空列表
print(sys.getrefcount(x))       #打印变量x 的引用计数

---
2

为什么结果会是2,因为前面赋值是引用一次,后面的打印里面是实参传入,引用计数会加一次,所以引用计数是2

x = []  # 1
y = x   # 2
z = x   # 3
print(sys.getrefcount(x))  # 4

---
4
引用计数减少
import sys
x = []  # 1
y = x   # 2
z = y   # 3
print(sys.getrefcount(x))  # 4


x = 1    # 引用计数减少
print(sys.getrefcount(z))    # 此时z的引用计数应该为4-1

y = 2
print(sys.getrefcount(z))    # 此时z的引用计数应该为4-1-1

----
4
3
2

当引用计数为0时,GC不会立刻启动,它会在合适的时间去启动GC,也不用手动去清除垃圾,这种虚拟机上的语言,如Java或Python都是这种机制

共享引用

>>> a = 3
>>> b = a

输入以上两行命令后,第二行命名会使Python创建变量b。变量a正在使用,并且它在这里没有被赋值,所以变量b实际引用的对象是3。实际的效果就是变量ab都引用了相同的对象,也就是说,指向了相同的内存空间

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

下一步,增加一条输入

>>> a = 3
>>> b = a
>>> a = 'spam'

对于所有的Python赋值语句,这条语句简单的创建了一个新的对象,并设置a对这个新的对象进行引用。尽管这样,这并不会改变b的值,b仍然使用原始的对象---整数3。

与其他语言不同,在Python中变量总是一个指向对象的指针,而不是可改变的内存区域的标签:给一个变量赋值一个新值,并不是替换原始的对象,而是让这个变量去

共享引用在原位置修改

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

>>> L1 = [2, 3, 4]
>>> L2 = L1

运行上面的赋值语句后,L1和L2引用了同一个共享的对象,就像之前的a和b一样。接下来继续扩展

>>> L1 = 24

L1直接设置为一个不同的对象,L2仍然是最初的列表。

>>> L1 = [2, 3, 4]
>>> L2 = L1
>>> L1[0] = 24
>>> L1
[24, 3, 4]
>>> L2
[24, 3, 4]
  • 在这里我们并没有改变L1,而是改变了L1的所引用对象的一个元素。这类修改会在原位置覆盖列表对象中的某部分值

  • 因为这个列表是共享的,L2的引用对象也会随之修改。因为它们都引用了相同的对象

切片操作避免上述现象

如果想不改变其他变量,可以使用copy或者切片操作,下面介绍切片操作

>>> L1 = [2, 3, 4]
>>> L2 = L1[:]
>>> L1[0] = 24
>>> L1
[24, 3, 4]
>>> L2
[2, 3, 4]

这里的L2是L1所引用对象的一个副本,而不是原来的对象了,这两个变量指向的是不同的内存区域

语言强弱

强语言类型

不同类型之间的操作,必须强制类型转换为同一类型

print('a'+1)

TypeError: can only concatenate str (not "int") to str
会抛异常!

弱类型语言

不同类型之间的操作,不必要类型转换,系统会自动帮我们转换,即隐式类型转换

下面打开浏览器的Web控制台用JS来试验下
console.log(1+'a')

1a
说明JS是弱类型语言