先分开作用域、链接与生存期
跳转到“先分开作用域、链接与生存期”Python 与 C 都不能用“哪个函数调用了我”来决定普通名字的词法归属,但二者绑定名字、组织文件和管理对象的规则有明显差异。本文保留原笔记逐项对照的学习方式,以 Python 3.11、C11 为主要范围;原文夹在 C 段落里的类示例单独标为 C++17。
| 概念 | 需要回答的问题 | 不能直接推出的结论 |
|---|---|---|
| 作用域 | 在哪一段代码中可以使用这个名字 | 对象一定在离开该段代码时消失 |
| 链接属性,主要用于这里的 C 讨论 | 不同声明是否指向同一个对象或函数 | 另一个源文件无需声明就能访问 |
| 对象生存期/存储期 | 对象在执行过程中的存在区间 | 对象的名字在整个区间都可见 |
| Python 属性查找 | obj.name 从哪里取得属性 | 与方法中的裸名字 name 使用同一套查找路径 |
1. 普通语句块:C 的新声明与 Python 的赋值
跳转到“1. 普通语句块:C 的新声明与 Python 的赋值”C 的复合语句块可以引入局部声明,内层同名对象会遮蔽外层对象。下面保存为 block_scope.c,独立编译运行。
#include <assert.h>#include <stdio.h>
int main(void){ int x = 10; if (x > 5) { int y = 20; assert(x == 10 && y == 20); printf("inside if: x=%d y=%d\n", x, y); } /* y is not declared here. */ assert(x == 10); { int x = 110; assert(x == 110); printf("inner x=%d\n", x); } assert(x == 10); printf("outer x=%d\n", x); return 0;}内层 int x = 110 声明了另一个对象,不会把外层 x 改为 110。块外直接写 printf("%d", y) 则缺少可见声明,应由编译器诊断。声明的可见范围还与声明出现的位置有关,不能把它粗略说成从函数第一行就开始。
Python 的普通 if、for、while 不因缩进建立同类的局部作用域。同一个函数中的普通赋值会更新相应绑定;分支从未执行时,名字也可能从未绑定值。
def block_demo(flag): x = 10 if flag: y = 20 x = 110 return x, y
assert block_demo(True) == (110, 20)try: block_demo(False)except UnboundLocalError: print("false branch: y has no value")else: raise AssertionError("expected UnboundLocalError")assert "y" not in globals()这里 y 属于函数,不是模块全局变量。推导式、函数、类定义有另外的名字规则,不能把“普通 if 不产生作用域”推广为所有缩进结构都一样。Python 名字绑定与解析
2. 函数中的局部名字:术语不要混用
跳转到“2. 函数中的局部名字:术语不要混用”C 日常表达中的“函数局部变量”,在标准术语里通常属于块作用域。C 的正式“函数作用域”专用于标签名;此外还有文件作用域和函数原型作用域。函数形参在函数定义中的名字也属于相应的块作用域。
Python 函数则确实有自己的局部名字环境。下面同时说明局部变量不能由调用者直接访问,以及被调用函数不会去调用者的局部变量中寻找同名值。
value = 40
def read_value(): local_value = 30 assert local_value == 30 return value
def caller(): value = 999 result = read_value() assert value == 999 return result
assert caller() == 40assert "local_value" not in globals()print("callee uses its defining module:", caller())caller 的 value=999 不会动态改变 read_value 的名字解析环境。更完整的 global、nonlocal 和 a += 1 例子见 Python 作用域与名字绑定。
3. “全局”不是整个程序无条件可见
跳转到“3. “全局”不是整个程序无条件可见”C 在函数外声明的对象通常具有文件作用域;其他翻译单元若要访问具有外部链接的对象,需要可见且兼容的声明。文件作用域加 static 则常用于限制为内部链接。这里要分别考虑可见声明与链接关系,而非一句“所有函数和类之外的变量处处可访问”;C 本身没有类。
Python 的全局名字属于具体模块。一个模块定义函数后,即使其他模块导入并调用该函数,它的普通全局查找仍指向定义它的模块。global 也不会将名字发布到所有模块。Python 模块教程
4. 多文件:C 头文件/源文件与 Python 模块
跳转到“4. 多文件:C 头文件/源文件与 Python 模块”下面的 C 示例由三个文件组成。头文件提供声明,源文件提供唯一的外部对象定义;另有内部链接的调用计数供访问器使用。
mymodule.h:
#ifndef MYMODULE_H#define MYMODULE_H
extern int global_value;int get_global_value(void);int get_read_count(void);
#endifmymodule.c:
#include "mymodule.h"
int global_value = 60;static int read_count = 0;
int get_global_value(void){ ++read_count; return global_value;}
int get_read_count(void){ return read_count;}module_main.c:
#include <assert.h>#include <stdio.h>#include "mymodule.h"
int main(void){ assert(get_global_value() == 60); assert(get_read_count() == 1); global_value = 61; assert(get_global_value() == 61); assert(get_read_count() == 2); printf("value=%d reads=%d\n", global_value, get_read_count()); return 0;}编译时需要把 mymodule.c 和 module_main.c 一起链接,例如 gcc -std=c11 -Wall -Wextra -Werror -pedantic-errors mymodule.c module_main.c -o module_demo。只包含头文件不会自动把定义链接进来;遗漏源文件将产生未解析符号。read_count 的定义具有内部链接,另一个源文件不能仅写一个外部声明就取得这个内部对象。
Python 版本同样用两个文件,但导入建立的是模块对象及名字绑定关系。
mymodule.py:
global_value = 60
def get_global_value(): return global_value
def increment(): global global_value global_value += 1同目录下的 module_main.py:
import mymodulefrom mymodule import global_value as imported_value
assert mymodule.get_global_value() == 60assert imported_value == 60mymodule.increment()assert mymodule.global_value == 61assert imported_value == 60print("module:", mymodule.global_value, "imported binding:", imported_value)from ... import ... 在本模块创建名字绑定,不是将两个变量名永久连接。increment() 重新绑定了 mymodule.global_value,本例中先前导入的整数绑定仍为 60。若共享的是可变对象,修改对象与重新绑定又有不同结果;参见 copy 与对象身份。Python 模块也不都来自普通 .py 文件,内置模块与扩展模块同样可以参与导入。
5. 类的对照:这是 C++,不是 C
跳转到“5. 类的对照:这是 C++,不是 C”原笔记在“C 语言”标题下使用了 class、public 和 std::cout。这些属于 C++。此外,“类成员只能在类内部访问”也不准确:下面的公开成员可以经对象在类外访问。
#include <cassert>#include <iostream>
class Example {public: int classValue = 50; int readValue() const { return classValue; }};
int main(){ Example object; assert(object.classValue == 50 && object.readValue() == 50); object.classValue = 51; assert(object.readValue() == 51); std::cout << object.classValue << '\n';}这是 C++17 的独立程序,可用 g++ -std=c++17 编译。类内名字查找与 public / private / protected 访问控制是相关但不同的问题;这里的非静态成员值属于每个实例。C++ 类作用域、成员访问控制
Python 的同名对照也不能直接照搬 C++:类体赋值通常创建类属性,而方法里的裸名字不自动去类属性表查找。
class_value = 5
class Example: class_value = 50
def read_values(self): return class_value, self.class_value, type(self).class_value
first = Example()second = Example()assert first.read_values() == (5, 50, 50)first.class_value = 51assert first.read_values() == (5, 51, 50)assert second.class_value == 50 and Example.class_value == 50print(first.read_values())为 first.class_value 赋值在此例创建实例属性,并未修改 Example.class_value。描述符、继承和自定义属性访问还会进一步影响行为;这个例子只对照普通类与实例属性。Python 类与实例变量
6. C 有标识符名字空间,但没有 C++ 的 namespace 语法
跳转到“6. C 有标识符名字空间,但没有 C++ 的 namespace 语法”原说法“C 没有命名空间”容易产生歧义。C 没有 C++ 那样的 namespace group { ... } 组织语法,却在语言规则中区分标签、结构/联合/枚举标签、成员及普通标识符等名字空间。下面刻意使用同一个拼写,展示语法上下文如何区分它们。
#include <assert.h>#include <stdio.h>
struct reading { int reading; };
int main(void){ int reading = 70; struct reading sample = {80}; assert(reading == 70 && sample.reading == 80); goto reading;reading: printf("ordinary=%d member=%d\n", reading, sample.reading); return 0;}这不是推荐同名命名风格,只用于验证规则。struct reading、变量 reading、成员 sample.reading 和标签 reading: 各自靠不同上下文识别。C 项目使用模块前缀减少外部符号冲突仍然很有用,但这是命名约定,不等同于语言新增了一层用户命名空间。
7. 名字遮蔽与普通赋值
跳转到“7. 名字遮蔽与普通赋值”前面的 C 程序在嵌套块内重新声明 int x,退出内层后又能用原来的外层 x。Python 在普通 if 内写 x=... 则没有声明另一个块级局部名字,后续同一函数中的 x 看到的是更新后的绑定。原文的两组“作用域链”和“变量隐藏”例子主要展示的就是这个差别。
不要因此推断 Python 不会遮蔽名字:内层函数的局部名字可以遮蔽外层函数或模块中的同名名字,函数内赋值还可能导致 UnboundLocalError。也不要把 C 作用域链机械写成“块→函数局部→全局”,忽略正式术语中的标签作用域和声明位置。C 的有关规则可对照委员会草案 N1570,6.2.1–6.2.3。
8. 词法作用域、闭包和 nonlocal
跳转到“8. 词法作用域、闭包和 nonlocal”Python 的 global 与 nonlocal 不会切换为动态作用域。它们指定名字应绑定到哪一个词法环境。闭包可以在创建它的外层调用结束后继续访问所保留的绑定。
global_value = 150
def outer_function(): nonlocal_value = 160
def inner_function(): nonlocal nonlocal_value nonlocal_value += 10 return nonlocal_value
return inner_function
increment = outer_function()assert increment() == 170assert increment() == 180assert global_value == 150print("closure survives the outer return")C11 不支持在函数体内定义普通嵌套函数;某些编译器扩展不能作为通用 C 规则。需要把状态与操作一起传递时,可以显式使用结构体和函数指针,而不是假设 C 会捕获外层局部变量。本例没有通过返回悬空指针模拟闭包。Python nonlocal
9. 块结束不意味着所有 C 对象都结束生存期
跳转到“9. 块结束不意味着所有 C 对象都结束生存期”原总结“C 的变量在块结束时被销毁”只适合特定自动存储期对象,不能覆盖块内 static。下面 calls 的名字只能在 next_call 内使用,但对象在函数调用之间保留值。
#include <assert.h>#include <stdio.h>
static int next_call(void){ static int calls = 0; ++calls; return calls;}
int main(void){ int first = next_call(); int second = next_call(); assert(first == 1 && second == 2); printf("calls: %d %d\n", first, second); return 0;}C 的自动、静态、线程、分配存储期分别有各自规则,作用域与生存期需要分开讨论;指向已经结束生存期的自动对象的指针不能再用于访问该对象。Python 的对象也可能因为闭包、容器或其他引用而继续存在,离开某个函数不等于所有曾由局部名字引用的对象都立即消失。本文的程序验证名字可见性与这些受控状态变化,没有执行越界生存期访问。
原文来源:CSDN:Python 和 C 中作用域的差异,首次发布于 2025-03-29。