Python基础篇:Python特殊语法(pass、import、doctest、package、generator )
初学Python会遇到很多自己从未见过的语法,这些语法在其它编程语言中都没见过,有些语法是Python独创的,有些语法只是改变了书写形式自己又造一种新形式,这里罗列一下我认为Python中比较特殊的语法。
0. pass 占位符
-
Python 的语法规定“代码块(suite)里必须至少有一条语句”,而 pass 就是“什么都不做的一条合法语句(null statement)”。
-
当函数或者代码块中不需要写任何代码时为了满足预发约束,所以就必须显式的写pass, pass表示占位符, 如果不写会报语法错误(expected an indented block)。
在其它编程语言中如果一个方法是空的给出一对大括号{}就表示方法体是空的,但是Python的方法体不是用大括号表示的而是用冒号+缩进方式表示的,Python为了表示一个空的方法体发明了这个pass占位符。
Python 的简洁观不是“字符最少”,而是“规则少、可读性高、歧义低”。
pass 看起来“多写了一个词”,但从语言设计角度,它其实是在用一个很小的显式成本,换来更一致的语法和更清晰的意图,并不违背 Python 的“简洁”。
-
规则一致性比省一个关键字更重要:Python 要求“冒号后必须有代码块,代码块里必须有语句”。这条规则到处都一样。 如果允许“真正空块”,就会引入一个特殊例外:有时块可以没有语句,有时不行。pass 让规则保持统一,不需要额外例外规则。
-
显式优于隐式:The Zen of Python 里有一句:Explicit is better than implicit.pass 明确告诉读代码的人:这里是“故意留空”,不是“漏写了”。这对维护代码很关键。
-
实际噪音很低:pass 只在“你确实要空实现”时才出现,不是日常大量重复语法。它的使用场景相对集中:脚手架、占位、抽象接口、临时跳过分支。所以总体上并不会让语言变“啰嗦”。
所以你看到的这个“多一个 pass”,更像是 Python 价值观里的一个典型取舍:宁可多写一个非常明确的词,也不引入隐式空块和特殊语法分支。
public void test() {
// java空方法体
}
def func():
pass
if True:
pass
因为工程上经常需要“先占位,后实现",pass 明确表达:这里是有意留空,不是漏写。
- 先把类/函数骨架搭起来
- 某个分支暂时不处理
- 抽象基类里先留接口
- 调试时临时留空
class Todo:
pass
def not_implemented_yet():
pass
# 有时你会看到用字符串字面量占位:
def f():
"""TODO"""
一句话总结: pass 不是为了解决解析歧义,而是为了满足“块内必须有语句”的语法约束,并且清晰表达“这里故意什么都不做”。
1. import 可执行语句
import 本质上是运行时执行的语句(
Import 是"可执行语句"),而不是编译时的纯粹声明,与编译型语言(如Java)完全不同。
导入语句可以写作代码块中(延迟导入)
在 Python 中,import 语句在运行时被执行,执行时会发生:
- 查找模块
- 加载模块(如果是第一次)
- 创建模块对象
- 将模块绑定到变量名
import math # 运行时:找到 math.py,执行它,创建 math 变量
实际应用场景
# 场景1:可选依赖
try:
import numpy as np
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
# 场景2:版本兼容
try:
import tomllib # Python 3.11+
except ImportError:
import tomli as tomllib # 第三方回退
# 场景3:平台特定导入
if sys.platform == "win32":
import winreg
else:
import fcntl
# 场景4:延迟导入(节省启动时间)
def process_image(path):
from PIL import Image # 只在需要时导入
return Image.open(path)
导入会执行全局代码
user.py
print("this is user.py global script")
test2.py
import user
if __name__ == '__main__':
print(__name__)
导入module时会执行全局代码。一般情况下全局代码用来定义函数,变量等,如果想部分全局代码在导入时不要被执行到,那么就将这些代码放入到每个文件的if __name__ == '__main__':里面(通常是涉及到该模块的一些测试代码), 只有执行 python xxx.py 对应的脚本中的全局变量__name__ 才会赋值为__main__,被导入的文件的__name__值被赋值为"script<文件名>"。

2. Package Vs Directory
最佳实践
Python创建Package时不但会创建一个文件夹,还会创建一个__init__.py的文件,这个文件的作用用于标识此目录是一个package而不是一个普通的文件夹Directory。
用 Python Package 来组织所有核心的功能代码。只要一个文件夹里存放的是你要写、要用的 .py Python 模块,并且这些模块会被其他部分的代码(例如主程序、其他模块)所调用,就应该创建一个 Package。这是 Python 项目模块化和代码复用的基础。
用 Directory 来管理项目中的配置文件、静态资源、测试代码和数据文件。这些文件虽然不是直接被 import 的 Python 模块,但良好的目录结构对项目的可维护性至关重要。
虽然普通目录在特定条件下可以被导入(换个运行方式可能就报 ModuleNotFoundError ),但这不是最佳实践,在实际工作中强烈推荐使用 Package。
| 方面 | Package(有 __init__.py) |
Directory(无 __init__.py) |
|---|---|---|
| 导入可靠性 | ✅ 任何情况都能导入 | ⚠️ 依赖运行方式和路径设置 |
| 子模块导入 | ✅ 支持 from package import submodule |
❌ 需要手动处理 sys.path |
| 相对导入 | ✅ 支持 from . import module |
❌ 会报 attempted relative import beyond top-level package |
| 包级初始化 | ✅ 可在 __init__.py 中统一暴露接口 |
❌ 无法控制包的对外接口 |
| 代码可读性 | ✅ 明确表示这是一个可导入的模块集合 | ⚠️ 语义模糊,容易混淆 |
实际工作建议
my_project/
├── src/ # ✅ 用 Package:存放核心代码
│ ├── __init__.py
│ ├── database/
│ │ ├── __init__.py
│ │ └── connection.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/ # ⚠️ 虽然是目录,但测试代码不需要被其他模块导入
│ └── test_database.py
├── data/ # ❌ 用 Directory:存放数据文件
│ └── sample.csv
└── scripts/ # ⚠️ 独立脚本目录,不作为包使用
└── deploy.py
实际使用场景
__init__.py 远不止是一个"标记文件",它在实际项目中承担着重要的代码组织职责。
1. 简化导入路径(最常用)
my_project/
├── main.py # 主程序入口
│
└── database/ # 数据库包
├── __init__.py # 简化导入:暴露核心接口
├── connection.py # DatabaseConnection 类
├── queries.py # execute(), fetch_all() 函数
└── models.py # User, Product 模型类
database/__ init__.py
from .connection import DatabaseConnection
from .queries import execute, fetch_all
from .models import User, Product
__all__ = ['DatabaseConnection', 'execute', 'fetch_all', 'User', 'Product']
main.py - 使用简化后的导入
from database import DatabaseConnection, execute, User
# 而不是写:
# from database.connection import DatabaseConnection
# from database.queries import execute
# from database.models import User
2. 控制包的公开 API(all)
明确指定当用户使用 from package import * 时会导入哪些内容。
utils/ # 工具包
├── __init__.py # 使用 __all__ 控制暴露的接口
├── strings.py # 字符串处理函数
├── dates.py # 日期处理函数
├── crypto.py # 加密相关函数
├── validators.py # 验证函数
├── internal.py # 内部使用函数(不对外暴露)
└── deprecated.py # 已弃用的函数(不对外暴露)
utils/__ init__.py
from .strings import capitalize, truncate
from .dates import format_date, parse_iso
from .crypto import hash_password, verify_password
# 只暴露这些函数,隐藏内部实现
__all__ = [
'capitalize', 'truncate',
'format_date', 'parse_iso',
'hash_password', 'verify_password'
]
使用者
from utils import * # 只会导入 __all__ 中列出的内容
3.包级别的初始化代码
在包被导入时执行初始化逻辑,例如配置加载、日志设置、环境检查等。
config/__ init__.py
import os
import logging
from pathlib import Path
# 设置包级别的日志
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
# 加载环境变量
ENV = os.getenv('APP_ENV', 'development')
BASE_DIR = Path(__file__).parent.parent
# 根据环境加载不同配置
if ENV == 'production':
from .prod import ProductionConfig as Config
else:
from .dev import DevelopmentConfig as Config
# 对外只暴露 Config
__all__ = ['Config', 'logger']
4. 版本管理
在 init.py 中定义包的版本号,便于统一管理。
# mypackage/__init__.py
__version__ = '2.1.0'
__author__ = 'Your Team'
__license__ = 'MIT'
from .core import main_function
from .utils import helper
# 使用者可以获取版本信息
# >>> import mypackage
# >>> mypackage.__version__
# '2.1.0'
5. 整合第三方库或子包
将多个子包或第三方库整合到一个统一接口下。
# models/__init__.py
# 根据配置选择使用哪个数据库 ORM
from django.conf import settings
if settings.DATABASE_ENGINE == 'postgresql':
from .postgres_models import User, Product
else:
from .sqlite_models import User, Product
__all__ = ['User', 'Product']
总结:实际工作中的建议
| 项目规模 | __init__.py 用法 |
|---|---|
| 小型脚本(< 10 个文件) | 可以是空文件,只起标记作用 |
| 中型项目(10-50 个文件) | 做导入简化 + __all__ 控制 API |
| 大型项目(> 50 个文件) | 做初始化、版本管理、延迟导入、插件注册 |
最重要的一条:即使是空文件,也比没有强——它明确告诉 Python 和所有阅读代码的人:“这是一个包,里面的模块可以被导入”。
3. 文档注释doctest
Python中的文档注释doctest是写在模块的第一行或者函数的第一行使用三个引号写的一段字符串注释,注释中一般包括函数作用的解释以及给出如何使用函数的示例程序。
文档测试的作用:
- 给出一些示例代码,便于快速了解使用
- 通过工具来生成文档,类似于Java中的Javadoc
在大模型开发定义工具函数时常读取函数的文档注释作为工具的功能描述,用于判断是否会触发工具。
def get_wether(city, day):
"""
获取天气信息
:param city: 城市名称
:param day: 日期
:return: 天气信息
"""
return f"{city}的{day}天气是晴天"
if __name__ == '__main__':
import doctest
# 获取文档注释
print(get_wether.__doc__)

4. 函数注释(Function Annotations)
函数注释作用是提高代码可读性,暗示传入参数及返回数据的类型, 注意这只是提示参数和返回值的类型,并不校验参数的值是不是这种类型,因为Python中参数名是没有数据类型的,实际上还是可以任意传任意类型的值,这里只是提示要传入的数据类型,作为一种注释来提示你。
函数注释包括:
- 参数注释:以冒号 : 标记,可以是建议传入的参数数据类型type
- 返回值注释:以箭头 -> 标记,建议函数返回值的数据类型
参数类型和返回值类型注释
def div(a: int, b: int) -> float:
return a / b
帮助字符串注释
def div(a: "除数", b: "被除数") -> float:
return a / b
参数类型注释和帮助字符串注释混用
def div(a: int, b: "被除数") -> float:
return a / b
同时支持参数数据类型和帮助字符串注释
def div(a: dict(type= int, help="除数"), b: dict(type= int, help="被除数")) -> float:
return a / b
# 注释是一种帮助开发人员了解代码的,并不是可执行代码的一部分,
# 函数注释只是一种提示,并不是强制。虽然我们指定了参数的数据类型,
# 但是我们让然虽然传值,我们可以传float类型的值。
print(div(4.4, 2.0))
类型注解,是一种注释,不影响编译,所以说类型注解并没有改变Python弱类型的基本特性。
name = '我就是不报错,我就是能正常运行,类型注解只是一种提示,不影响编译' # type: int
def add(x: int, y: int) -> int:
return x + y
# 正常x,y是int,但是编译不会报错,传入str也能正常运行
add('hello', 'world')
通常注释只用来标注数据类型,具体参数的作用一般都是通过文档注释来描述。
def div(a: int, b: int) -> float:
r"""除法运算
:param a: 除数
:param b: 被除数
:return: 除法结果
"""
return a / b
5. 方法重载
Python中的变量是没有数据类型的,也就是相当于Java中的Object类型,所以同一个参数传值既可以是字符串也可以是其它任意类型,只要程序支持。在Java中方法的参数的数据类型很少是Object类型,一般都是某个具体的类型。而Python中的同一个参数的类型可以是任意的类型,同样返回值的类型也可以是任意类型,这就相当于Java的方法重载了Overload,神奇的是Python就用了一个方法就达到了方法重载的目的。
def foobar(a: int | str | tuple | list | dict = None) -> int | str | tuple | list | dict:
if isinstance(a, int):
print(f'数据类型为int={a}')
elif isinstance(a, str):
print(f'数据类型为str={a}')
elif isinstance(a, tuple):
print(f'数据类型为tuple={a}')
elif isinstance(a, list):
print(f'数据类型为list={a}')
elif isinstance(a, dict):
print(f'数据类型为dict={a}')
return a
print(type(foobar(1)))
print(type(foobar('python')))
print(type(foobar((1, 2, 3))))
print(type(foobar([1, 2, 3])))
print(type(foobar({'k1': 'v1', 'k2': 'v2'})))
6.生成器 generator
生成器是个有意思的功能,调用生成器函数并不执行函数体,而是在调用生成器的__next__()方法时才会真正去执行方法体,而且只有第一次才会执行这个方法体,方法体的for循环只执行一次,第二次再执行也就只执行循环体里面的代码,不会执行循环体外部的代码。
当循环的对象非常占用内存时,或者当真正使用时再去获取对应的值时使用。
def power(x):
print("init code")
for i in range(1, x):
print(f"power.for {i}")
yield i ** i
gen = power(5)
# <generator object power at 0x000001635E025B60>
print(gen)
# init code
# power.for 1
# 1
print(gen.__next__())
# power.for 2
# 4
print(gen.__next__())
生成器是可以循环的。
"""
init code
power.for 1
1
power.for 2
4
power.for 3
27
power.for 4
256
"""
for i in power(5):
print(i)
更多推荐

所有评论(0)