从C/C++程序员角度看Python的动态类型

 

从C/C++程序员角度看Python的动态类型

IN: C/C++|PYTHON

102010

Python是一种功能强大且完善的动态高级语言,目前被广泛地应用在计算机开发的各个领域。作为动态语言,Python的一个特点就是它支持动态类型(Dynamic Typing)和强类型(Strong Typing)。

动态类型是指在编写代码的过程中,程序员不需要将某个变量声明为某种类型,而是解释器根据变量的赋值动态地分配内存、赋值并将该变量绑定到对象上。它不同于静态类型(Static Typing),后者对于从事C、C++、Java、C#等面向对象语言开发的人来说,再熟悉不过了。静态类型语言在声明变量的时候,需要指定该变量的类型,从而编译器在遇到该类型的对象时,会分配对应的空间并且赋值。让我们来看两个例子:

Python语言:
var  =  "string"  #不需要指定变量var的类型,赋值即可 
var  =  123       #可直接指向其他类型的对象,不需要转换对象类型
 
C++语言:
string  str  =  "string"//需要指定变量的类型,编译器将为其分配内存空间 
str  =  1;                //Error: string类没有重载整数的赋值

从上述例子中我们可以看出,在Python里类型并不与变量绑定,变量只是对象的一个别名,一个变量可以赋予任何类型的对象值——事实上Python的处理也是如此:类型与对象进行绑定,变量名只是对象的一个表示,在表达式中变量名将被展开成为对象进行求值。而C++等静态类型语言则不同,类型与变量绑定,当出现该变量时,编译器会根据变量的类型进行求值,程序员不能轻易地将不同类型的值赋予该变量。

强类型指的是运算符或者函数调用的合法与否依赖于操作数或者参数的类型是否为预期类型。比如"23" + 1这样的表达式就不被强类型语言接受。

Python的值传递方式是全局by-ref,这点与C#及Java类似。接下来本文就将以C/C++程序员的角度理解Python的动态类型机制。

前文中曾经提到,在Python中,类型并不与变量绑定,一个类型绑定一个对象,那么解释器在碰到一个变量的时候要如何解析该变量的类型和值呢?答案是通过给一个对象赋以额外的信息。在Python中,我们可以将一个对象看作是一个结构体:

C++语言:
typedef  struct { 
     Type *  type
     int    refCount
     char *  obj
}  PyObj;

该结构体分成三个部分:type,指向一个类型对象,该对象表明当时对象的类型(在Python中,所有的值都是对象,包括类型、函数等);refCount,表示该对象的引用次数,当该对象的引用次数为0的时候,GC将会自动回收内存空间;obj,真正对象存放的地方。我猜测,其他动态类型语言的实现细节从整体上看应该也是如此布局。也正是由于在动态类型语言中,解释器要知道一个变量所指向对象的类型,必须先从变量所指的地址得到该对象的类型,再对该对象进行一系列的操作,因此效率相对于静态类型语言而言要低。因为静态类型语言的编译器维护一张符号表,在表中记录着变量对应的类型,可以很方便地将相应操作直接转变为机器码或者字节码,而不用在运行时检测对象类型,效率会提高许多。

那么Python的引用语义是如何实现的呢?答案同C++类似,使用指针机制,然而Python中变量并没有其绑定的类型,这点与void指针很像。我们都知道,在C/C++中如何解析一个指针所指向内存的内容是通过该指针的类型来完成的,而void指针可以指向任意一块内存空间,只是编译器不知道该如何解析而已。在Python中类似,在前文的例子中我们可以得到一个void指针var,对该变量的操作则是如前如述通过与对象绑定的额外的类型信息获得的。知道了这点之后就可以很容易地理解Python的共享引用:

1) 共享引用和In-place修改

观察这么一段代码:

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

此时变量a和b指向的是同一块内存地址:即整数3这个对象,转换成对应的C++代码可以是:

void *a = &3 // 此处3是常量,而Python中3是一个对象,注意区别 
void *b = a

从C++代码中很容易就得出a、b所指向地址相同的结论。因此在Python中处理共享引用一个可更改对象(如List、Dictionary和Set)时要特别小心,例如:

>>> L1 = [2, 3, 4]   # A mutable object 
>>> L2 = L1          # Make a reference to the same object 
>>> L1[0] = 24       # An in-place change 
>>> L1               # L1 is different 
[24, 3, 4] 
>>> L2               # But so is L2! 
[24, 3, 4]

由于L1、L2指向的同样一块内存空间,因此对L1的修改同样会引起L2指向对象的变化,可以使用将引用语义转换至拷贝语义来规避这种潜在的语义错误:

>>> L1 = [2, 3, 4] 
>>> L2 = L1[:]       # Make a copy of L1 
>>> L1[0] = 24 
>>> L1 
[24, 3, 4] 
>>> L2               # L2 is not changed 
[2, 3, 4]

2) 对象比较

在C++中,比较运算符“==”在其操作数为指针时,返回的结果是两个指针对指向的地址是否相同,当其操作数为对象时,则是比较数值是否相等或者根据重载的运算符的行为来定义。而在Python中,由于其引用语义实际是通过指针的机制来实现,对于比较两个变量的行为就要特别注意。一般地,==运算符比较的是两个变量的值是否相等,例如:

>>> L = [1, 2, 3] 
>>> M = [1, 2, 3]    # M and L reference different objects 
>>> N = L            # N and L reference the same object
 
>>> L == M           # Same value 
True 
>>> L == N           # Same value 
True

在讨论共享引用的时候我们说过,N和L指向的是同一内存地址,因此无论Python中==运算符的语义为何,得到的结果都应该是True,而从比较L和M这两个虽然指向内容一样,但是对象的地址却不同的例子中我们可以得到==运算符的确切语义。那么如果想知道两个变量是否指向同一个对象应该如何编写代码呢?答案是使用is表达式:

>>> L is M           # Different objects! 
False 
>>> L is N           # Same object 
True

 

总结:

本文尝试从C/C++程序员熟悉的指针角度,简略地探讨了Python中动态类型的实现机制,分析了对象的内存布局,变量的共享引用机制。

你可能感兴趣的:(python)