# 变量

# 赋值

  • 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 等数据类型的对象,是只读的,相当于常量。

# 标识符

  • 变量名、函数名、类名等符号,统称为标识符。

    • 每个标识符的命名格式:
      • 可以由阿拉伯数字、英文字母、下划线几种字符组成。
      • 不能以数字开头。
      • 字母区分大小写。
    • 用户创建的标识符,不能与 Python 的保留字同名,比如 intclass 等。
      • 用户可以给标识符加上 _ 后缀来进行区分。例如将 int 写作 int_
  • 单独的下划线 _ 是一个特殊的变量。

    • 编程时,可以将不会使用的临时变量命名为 _ ,表示读者不需要关心该变量。例如:
      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'))
      

# 作用域

  • 大部分编程语言中,创建一个标识符之后,该标识符只能在一定范围内使用。这个范围,称为作用域(scope)。

    • 例如执行多个函数,每个函数内是一个独立的作用域。
    • Python 中,会为每个作用域,创建一个独立的命名空间,用于存储当前作用域存在的一些标识符。
    • 一个标识符,可能跨越多个作用域,存在于多个命名空间中。比如全局变量,可以在所有函数内访问。
  • Python 中,标识符的作用域分为四种:

    • 内建作用域(Built-in)
      • 启动一个 Python 解释器时,会创建一个内建作用域,用于存储 Python 解释器内置的一些标识符。
      • 启动多个 Python 解释器时,如果修改其中一个 Python 解释器的内建作用域,则不会影响其它 Python 解释器。
    • 全局作用域(Global)
      • 一个 Python 解释器,可能执行多个 Python 脚本。每个脚本会创建一个全局作用域,用于存储该脚本中创建的标识符。
      • 在一个 Python 脚本中,不能访问另一个 Python 脚本中的标识符,除非使用关键字 import 导入。
      • 当一个 Python 脚本执行结束时,其全局作用域中的所有标识符,会被自动销毁。
    • 外部非全局作用域(Enclosing)
      • 如果在一个函数或类中,嵌套定义了另一个函数或类,
        • 相对于全局作用域而言,当前函数或类,属于局部作用域。
        • 相对于内层函数或类而言,当前函数或类,属于外部非全局作用域。
    • 局部作用域(Local)
      • 在一个 Python 脚本中,定义一个函数或类时,会创建一个局部作用域,用于存储当前函数或类中创建的标识符。
      • 在函数或类中创建的标识符,只会存储在局部作用域,不会存储在全局作用域。
      • 当一个函数执行完毕时,其局部作用域中的所有标识符,会被自动销毁。
      • C 语言中,if、for、while 语句也会创建一个局部作用域。但 Python 不会这样。
  • 例:

    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'>
      

# 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 解释器的自动优化,优化策略可能变化,用户只需了解。

# 引用

# 引用传递

  • 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 指向同一个对象
        

# 值传递

  • 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