# 变量
# 赋值
C 语言中,给一个变量赋值之前,需要事先定义该变量,声明其数据类型,例如
int a;
。而 Python 中,不需要这么做。因为:- 给一个名为 a 的变量赋值时,如果该名称的变量不存在,则 Python 会自动创建该变量,然后赋值。
- Python 的变量不区分数据类型,变量所指的对象才区分数据类型。
- 例:
>>> a = 1 # 给变量 a 赋值为 1 。实际上是让变量 a ,指向 1 这个 int 数据类型的对象 >>> a = 'hello' # 给变量 a 赋值为 'hello' 。实际上是让变量 a ,指向 'hello' 这个 str 数据类型的对象 >>> type(a) # 用函数 type() 可以查看变量所指对象的类名 <class 'str'> >>> del a # 用关键字 del 可以删除一个变量,但不会删除它所指的对象 >>> a # 再次读取变量 a ,会报错说它不存在 NameError: name 'a' is not defined
C 语言中,可以用关键字 const 将一个变量声明为常量。而 Python 中,不支持将变量声明为常量,而是采用以下做法:
- 通常将变量名大写所有字母,表示该变量被当作常量使用,不应该被修改。
- Python 中 int、float、complex、tuple、str、bytes 等数据类型的对象,是只读的,相当于常量。
# 标识符
变量名、函数名、类名等名称,统称为标识符。
- 每个标识符的命名格式:
- 可以由阿拉伯数字、英文字母、下划线几种字符组成。
- 不能以数字开头。
- 字母区分大小写。例如 man 与 Man 是两个不同的标识符。
- 每个标识符的命名格式:
int
、class
等名称,是 Python 的关键字,用户不能创建与它们同名的标识符。如果一个标识符由多个英文单词组成,如何分隔每个单词,从而方便供人阅读?
- 在 C、Java 等编程语言中,有以下几种常见的命名风格:
- 下划线命名法:在单词之间插入下划线。例如
my_name
- 大驼峰命名法:将每个单词的首字母大写,直接拼接在一起。例如
MyName
- 小驼峰命名法:类似于大驼峰命名法,但是将第一个单词的首字母小写。例如
myName
- 匈牙利命名法:类似于大驼峰命名法,但是在标识符前加上一个小写字母,表示数据类型。例如
sMyName
- 现代的 IDE 通常能自动提示标识符的数据类型,因此没必要使用匈牙利命名法。
- 下划线命名法:在单词之间插入下划线。例如
- 在 Python 中,建议采用以下命名风格:
- 变量名、函数名采用下划线命名法,所有字母都小写。例如:
my_name
- 类名采用大驼峰命名法,每个单词的首字母大写。例如
MyName
- 常量名采用下划线命名法,所有字母都大写。例如:
MY_NAME
- 变量名、函数名采用下划线命名法,所有字母都小写。例如:
- 在 C、Java 等编程语言中,有以下几种常见的命名风格:
单独的下划线
_
是一个特殊的变量。- 编程时,可以将不会使用的临时变量命名为
_
,表示读者不需要关心该变量。例如:for _ range(10): print('hello')
- 在 Python 终端中交互式编程时,如果执行某条语句的返回值不为 None ,则会自动赋值给名为
_
的变量。例如:>>> _ # 刚打开 Python 终端时,变量 _ 尚未创建 NameError: name '_' is not defined >>> 1 # 该语句的返回值不会 None ,因此会被赋值给变量 _ 1 >>> _ 1 >>> print(2) # 执行该语句的返回值为 None ,因此不会赋值给变量 _ 2 >>> _ 1
- 进行 I18N 国际化、L10N 本地化时,通常将翻译字符串的函数命名为
_
,便于调用。例如:def _(message): return message print(_('hello'))
- 编程时,可以将不会使用的临时变量命名为
# 作用域
运行 Python 程序时,会创建许多个作用域。每个作用域像一个命名空间,可以存储一些标识符。
- 有的标识符,只在一个作用域中存在,不能被其它作用域访问。
- 有的标识符,可以跨越多个作用域存在。比如全局变量,可以在所有函数内访问。
Python 中,作用域分为四种:
- 内建作用域(Built-in)
- 启动一个 Python 解释器时,会创建一个内建作用域,用于存储 Python 解释器内置的一些标识符。
- 启动多个 Python 解释器时,如果修改其中一个 Python 解释器的内建作用域,则不会影响其它 Python 解释器。
- 全局作用域(Global)
- 一个 Python 解释器,可能执行多个 Python 脚本。每个脚本会创建一个全局作用域,用于存储该脚本中创建的标识符。
- 在一个 Python 脚本中,不能访问另一个 Python 脚本中的标识符,除非使用关键字
import
导入。 - 当一个 Python 脚本执行结束时,其全局作用域中的所有标识符,会被自动销毁。
- 外部非全局作用域(Enclosing)
- 如果在一个函数或类中,嵌套定义了另一个函数或类,
- 相对于全局作用域而言,当前函数或类,属于局部作用域。
- 相对于内层函数或类而言,当前函数或类,属于外部非全局作用域。
- 如果在一个函数或类中,嵌套定义了另一个函数或类,
- 局部作用域(Local)
- 在一个 Python 脚本中,定义一个函数或类时,会创建一个局部作用域,用于存储当前函数或类中创建的标识符。
- 在函数或类中创建的标识符,只会存储在局部作用域,不会存储在全局作用域。
- 当一个函数执行完毕时,其局部作用域中的所有标识符,会被自动销毁。
- C 语言中,if、for、while 语句也会创建一个局部作用域。但 Python 不会这样。
- 内建作用域(Built-in)
例:
print(__name__) # 读取内建作用域中的变量 g = 1 # 全局作用域 def outer(): e = 2 # 外部非全局作用域 def inner(): l = 3 # 局部作用域 print(a) inner()
- 这四种作用域,范围依次递减,优先级依次递增。
- 假设读取一个名为 a 的变量,Python 解释器会先在当前作用域中(也就是当前命名空间中),查找名为 a 的变量。
- 如果找到了,则读取该变量。
- 如果没找到,则到外部作用域中查找。
- 如果外部作用域中没找到,则到更外部作用域中查找。
- 如果所有作用域中都没找到,则抛出异常:
NameError: name 'a' is not defined
Python 中,在局部作用域中,可以读取外部作用域中的变量,但不能直接修改该变量。
- 如果是外部非全局变量,则用关键字 nonlocal 声明之后,才能修改。
- 如果是全局变量,则用关键字 global 声明之后,才能修改。
- 错误示例:
>>> a = 1 >>> def fun1(): ... a = 2 # 在局部作用域中,不能直接修改外部变量。因此执行这条赋值语句时,不会考虑是否存在外部变量 a ,而是直接创建一个局部变量 a ... >>> def fun2(): ... b = a # 这里读取变量 a ,赋值给变量 b 。Python 解释器会发现,当前作用域不存在变量 a ,而全局作用域存在变量 a ,因此引用全局变量 a ... a = 2 # 这里变量 a 依然是指全局变量 a ,不能直接对它赋值,否则会抛出异常 ... >>> fun1() >>> fun2() UnboundLocalError: local variable 'a' referenced before assignment
- 正确示例:
>>> a = 1 >>> def outer(): ... global a # 这里将变量 a 声明为全局变量。因此 Python 解释器就知道了,当前作用域中提到的变量 a ,都是指全局变量 a ... a = 2 ... b = 2 ... def inner(): ... nonlocal b ... b = 3 ... inner() ... print(a, b) ... >>> outer() 2 3
# 对象
- Python 是一种纯面向对象的编程语言。Python 中的所有值,都是以对象的形式保存。
- C++ 虽然也是一种面向对象的编程语言,但 int 等基本数据类型的值,不会保存为对象。例如:
int a = 1; char* b = "hello"; // 变量 b 是一个指针,不是对象 std::string c = "hello"; // 变量 c 是一个 std::string 对象
- Python 中,即使用户输入 int 等基本数据类型的值,也会被自动保存为某个内置 class 的实例对象。例如:
>>> type(1) <class 'int'> >>> type('hello') <class 'str'>
- C++ 虽然也是一种面向对象的编程语言,但 int 等基本数据类型的值,不会保存为对象。例如:
# id
Python 解释器每创建一个对象,会给该对象分配一个 id ,取值随机、全局唯一。
- 如果两个对象的 id 相同,则说明它们是同一个对象。
- CPython 解释器中,是使用对象的内存地址作为 id 。
使用内置函数
id()
,可以查询一个对象的 id ,返回值为 int 类型。- 定义:
id(obj) -> int
- 例:
>>> id(1) # 1 这个值,会作为一个 int 类型的对象保存 2400851487024 >>> a = 1 >>> id(a) # 如果给 id() 输入一个变量,则会查询该变量所指对象的 id 2400851487024
- 定义:
理论上来说,Python 解释器会在内存中保存很多个对象,每个对象的 id 都不同。
- 例:
>>> a = dict() # 创建一个空的 dict 对象,赋值给变量 a >>> id(a) 2354410663808 >>> b = dict() # 创建一个空的 dict 对象,赋值给变量 b >>> id(b) 2354410660224 # 可见两个 dict 对象虽然都是空的,取值相同,但 id 不同 >>> a == b True >>> a is b False
- 实践中,可能需要反复创建一些取值相同的对象。为了节省开销,Python 解释器只会将这种对象创建一份,并自动缓存。
- 如下,与其每次循环都创建一个 'hello' 对象,不如只创建一个 'hello' 对象,然后每次循环都复用同一个对象。
for i in range(10000): print('hello')
- 不是所有的对象都适合缓存。Python 解释器主要会缓存
[-5, 256]
取值范围内的 int 类型对象、长度较短的 str 类型对象。例:>>> a = int(256) >>> b = int(256) >>> a is b True # 两个变量,指向同一个对象 >>> a = int(257) >>> b = int(257) >>> a is b False # 两个变量,指向不同的对象
- 总之,这属于 Python 解释器的自动优化,优化策略可能变化,用户只需了解。
- 如下,与其每次循环都创建一个 'hello' 对象,不如只创建一个 'hello' 对象,然后每次循环都复用同一个对象。
- 例:
# 引用
# 引用传递
Python 中的变量,用法像 C 语言中的通用指针,可以指向任何类型的对象。
- 变量本身不会存储数据,只是一个标识符、一个名称代号。
- 给一个变量赋值时,不会让该变量直接存储该值,而是让该变量,指向(或者说引用),这个值(或者说对象)。例如:
>>> a = 'hello' >>> id(a) 2858938357104 >>> a = 'world' # 给变量 a 赋予另一个值 >>> id(b) # 可见,变量 a 指向了另一个对象 2858938356480
Python 中的变量,虽然用法像 C 语言的通用指针,但实际上并不是指针,而是引用。
- 例如 C 语言中的以下语句,是创建一个
char *
类型的指针变量 p ,存储一块内存空间的首地址。char *p = malloc(10*sizeof(char));
- 用户使用指针时,知道了对象的内存首地址,就可以访问从首地址开始的第 n 个字节。但指针比较危险:
- 一个指针变量,存储的内存地址可能是无效的(比如用户修改了指针存储的内存地址),使用该指针可能导致程序崩溃。
- 用户可以故意访问特殊的内存地址,比如缓冲溢出攻击。
- 为了提高安全性,C++、Java、C#、Python 等编程语言,将指针类型的变量,改进为引用类型的变量。
- 引用,只能定位对象,不允许用户直接操作内存地址。
- C++ 代码示例:
int a = 1; // 1 属于基本数据类型,将它赋值给变量时,会采用值传递,将 1 这个值拷贝到变量 a 的内存空间中存储 int *b = &a; // 这是创建一个指针类型的变量 int &c = a; // 这是创建一个引用类型的变量。将变量 a 的引用传递给变量 c 时,会采用引用传递,让变量 c 指向同一个对象
- 例如 C 语言中的以下语句,是创建一个
# 值传递
- Python 中,使用
=
进行赋值时,总是采用引用传递,而不是值传递。因此原对象只存储了一份,修改该对象时,会影响所有引用者。- 例:
>>> a = [0, 1] >>> b = a # 这是引用传递 >>> b [0, 1] >>> a[0] = 1 # 这是值传递,修改 a[0] 的值 >>> a # 此时变量 a 与 b 所看见的对象,都被修改了 [1, 1] >>> b [1, 1] >>> a = [] # 这是引用传递,让变量 a 指向另一个对象 >>> a # 此时变量 a 与 b 指向不同对象 [] >>> b [1, 1]
- 如果用户希望进行值传递,对于 list 类型的对象,可使用切片、list.copy() 等方法,实现浅拷贝。例:
>>> a = [0, 1] # 该 list 包含两个元素,相当于两个 Python 变量,第一个变量指向 0 这个对象,第二个变量指向 1 这个对象 >>> b = a.copy() # 浅拷贝时,是创建一个新 list ,将每个变量所指的对象拷贝一份 >>> b [0, 1] >>> a == b # 两个 list 的取值相等 True >>> b is a # 两个 list 不是同一对象 False
- 之所以叫浅拷贝,是因为只会拷贝每个元素指向的对象,不会拷贝这些对象引用的其它对象。例:
>>> a = [0, 1, [2]] >>> b = a.copy() >>> b [0, 1, [2]] >>> a is b False >>> a[2] is b[2] # a[2] 指向 [2] 这个对象,该对象在 copy() 时是引用传递,因此 a[2] 与 b[2] 指向同一对象 True >>> a[2].append(3) # 修改 a[2] ,会导致 b[2] 也变化,因为它们指向同一对象 >>> a [0, 1, [2, 3]] >>> b [0, 1, [2, 3]]
- 例:
# 引用计数
Python 程序创建的所有对象,都存储在内存中。Python 解释器会自动管理每个对象占用的内存空间,不需要用户干预。
- 例:
>>> a = list() # 创建一个 list 对象,此时会自动分配内存空间 >>> a.append(1) # 往 list 对象中添加一个元素,此时会自动占用更多内存空间
- 在 CPython 解释器的底层,每个 Python 对象用一个 C 语言的结构体表示:
typedef struct _object { Py_ssize_t ob_refcnt; // 该对象的引用计数 struct _typeobject *ob_type; // 该对象的类型 } PyObject;
- 例:
Python 解释器如何自动回收内存?
- Python 解释器会记录每个对象的被引用数,从而得知它被多少个变量、其它对象引用了。
- 如果某个对象的引用计数减少至 0 ,则说明该对象停止使用,Python 解释器会自动回收该对象的内存空间。
- 用户可以主动查询一个对象的引用计数:
>>> import sys >>> a = 'hello' >>> sys.getrefcount(a) 2 >>> b = a >>> sys.getrefcount(a) # 该对象被变量 b 引用,因此引用计数加 1 3
对于循环引用,Python 解释器不一定能进行垃圾回收。
- 比如对象 A 引用对象 B ,对象 B 又引用对象 A ,导致这两个对象的引用计数一直不会减至 0 ,因此一直不会被回收内存。
- 如果用户编写一段代码时,不得不出现循环引用,则可以使用标准库 weakref ,创建弱引用(weak reference)。
- 弱引用只能用于定位对象,不会增加引用计数。
- int、str 等内置类型,不支持创建弱引用。
- 例:删除对象之后,可见:
>>> import sys >>> import weakref >>> class Test: ... name = 'test' ... >>> a = Test() >>> sys.getrefcount(a) 2 >>> b = weakref.ref(a) >>> b # 变量 b 指向 weakref 对象,而这个 weakref 对象可以定位到 Test() 对象 <weakref at 0x000001B0921D4C20; to 'Test' at 0x000001B0921AC400> >>> a is b() # 将变量 b 当作函数调用,会返回弱引用的对象。如果目标对象被删除,则返回 None True >>> sys.getrefcount(a) # 引用计数没有增加 2 >>> weakref.getweakrefcount(a) # 查询该对象有多少个弱引用 1
>>> del a >>> b <weakref at 0x000001B0921D4C20; dead> >>> b() is None True