码上问学问代码之理,学编程之道

第 3 章 · 进阶特性

基础会了,就能写程序;但 Python 真正的“优雅”在这一章:切片、列表生成式、生成器、函数式编程。它们让你用更少的代码,处理更多的数据——这正是“写得少、做得对”的开始。

切片:一口气取一段

切片 s[起:止:步长]:含头不含尾,负数从后往前数m0a102031405167798正序下标 0..8负下标 -9..-1s[2:5] → 下标 2、3、4(不含 5)→ '001's[:3]'码00'前 3 个s[-4:]'10179'最后 4 个s[::2]'m0107'隔一个取一个s[::-1]'9710100am'反转(面试常客)
先讲为什么取“前 3 个”“最后 5 个”“每隔一个取”这种需求太常见了。如果靠循环一个一个取,又长又容易错。切片用一条 [start:stop:step] 表达式就搞定。
slice.pypython可运行
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[0:3])     # 前 3 个 → [0,1,2]
print(nums[-3:])     # 最后 3 个 → [7,8,9]
print(nums[::2])     # 隔一个取 → [0,2,4,6,8]
print(nums[::-1])    # 反转 → [9,8,...,0]
运行输出Python 运行结果
运行结果: [0, 1, 2] [7, 8, 9] [0, 2, 4, 6, 8] [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
讲透start 省略表示从开头,stop 省略表示到结尾,step 为负表示倒着取。字符串、列表、元组都能切片,是处理“序列”的万能钥匙。
slice_life.pypython可运行
text = "上海自来水来自海上"
print(text == text[::-1])   # 回文?把句子倒过来和原句比

prices = ["¥12", "¥8", "¥20"]
print(prices[1:])           # 从第 2 个开始取一段
运行输出Python 运行结果
运行结果: True ['¥8', '¥20']
slice_pro.pypython可运行
path = "data/2024_report_final.csv"
name = path.split("/")[-1]
ext = name[-3:]             # 实战:取文件扩展名(后 3 个字符)
print("文件名:", name)
print("扩展名:", ext)
运行输出Python 运行结果
运行结果: 文件名: 2024_report_final.csv 扩展名: csv
避坑切片越界不会报错,而是“能取多少取多少”。这点和用索引取单个元素不同——nums[100] 会报错,但 nums[1:100] 只返回到末尾,新手常误以为自己写错了。

迭代:让自定义对象也能 for 循环

iterate.pypython可运行
# 凡是能用 for 挨个取出来的,都叫“可迭代”
for ch in "知码":        # 字符串可迭代
    print(ch)

# dict 默认迭代“键”,想要“键值对”用 .items()
d = {"a": 1, "b": 2}
for k, v in d.items():
    print(k, "→", v)
运行输出Python 运行结果
运行结果: 知 码 a → 1 b → 2
讲透“迭代”听起来玄,其实就是“能不能被 for 逐个取出来”。记住这句话,后面生成器、pandas 的逐行读取,本质都是迭代。
iterate_life.pypython可运行
class BookShelf:
    def __init__(self, books):
        self.books = books
        self.i = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i >= len(self.books):
            raise StopIteration
        book = self.books[self.i]
        self.i += 1
        return book

shelf = BookShelf(["三体", "活着", "围城"])
for b in shelf:
    print("读:", b)
运行输出Python 运行结果
运行结果: 读: 三体 读: 活着 读: 围城
iterate_pro.pypython可运行
scores = [88, 92, 75]
for idx, s in enumerate(scores, 1):   # 实战:顺便拿到名次
    print(f"第{idx}名:{s}分")
运行输出Python 运行结果
运行结果: 第1名:88分 第2名:92分 第3名:75分
避坑迭代器(包括生成器)只能用一次。遍历完再 for 一遍,里面已经空了,什么都取不出来——常让人以为“代码没生效”。需要重复用时,要么重新创建,要么转成列表存起来。

列表生成式:一行造列表

动手做把“对每个元素做点什么、再收集起来”这个循环,压缩成一行 [表达式 for 变量 in 序列]。读熟它,代码立刻变短变清晰。
comp.pypython可运行
squares = [x * x for x in range(1, 6)]     # 1~5 的平方
print(squares)

# 带条件的生成式:只要偶数
evens = [x for x in range(10) if x % 2 == 0]
print(evens)

# 把字符串列表全变成大写
words = ["python", "data", "ai"]
upper = [w.upper() for w in words]
print(upper)
运行输出Python 运行结果
运行结果: [1, 4, 9, 16, 25] [0, 2, 4, 6, 8] ['PYTHON', 'DATA', 'AI']
讲透对比一下等价循环:生成式把“循环 + 计算 + 收集”浓缩进一个表达式,既快又好读。在数据处理里,它常用来“清洗/转换一列数据"。
comp_life.pypython可运行
celsius = [0, 20, 37]
fahrenheit = [round(c * 9 / 5 + 32, 1) for c in celsius]   # 生活:摄氏转华氏
print(fahrenheit)
运行输出Python 运行结果
运行结果: [32.0, 68.0, 98.6]
comp_pro.pypython可运行
logs = ["OK 200", "ERR 404", "OK 200", "ERR 500"]
err_codes = [line.split()[1] for line in logs if line.startswith("ERR")]   # 实战:从日志揪出错误码
print(err_codes)
运行输出Python 运行结果
运行结果: ['404', '500']
避坑生成式里写 print(...) 只会收集到一堆 None——因为 print 的返回值是 None,不是你打印的内容。想收集“结果”就用表达式(如 x*x),别把 print 当返回值用。

生成器:用到才算,省内存

列表生成式 vs 生成器:一个「一口气全算好」,一个「要一个算一个」列表生成式 [ ][x*x for x in range(5)]立刻把 0 1 4 9 16 全算好放进内存✓ 能反复用、能切片、能 len()✗ 一百万个数 = 一百万人一起站操场生成器 ( )(x*x for x in range(5))只记「怎么算」,next() 要一个才算一个✓ 一百万个数只占一个人的位置✗ 只能往前走一遍,走完就没了大文件逐行处理、无限序列 → 生成器;普通小数据 → 列表生成式,直白优先
先讲为什么如果要生成 1000 万个数,列表会一次性占满内存。生成器用 yield “要一个给一个”,内存几乎不占——处理大文件、大数据流时这是救命特性。
gen.pypython可运行
def count_up(n):
    i = 0
    while i < n:
        yield i        # 每次“暂停”在这里,返回一个值
        i += 1

g = count_up(3)
print(next(g))   # 0
print(next(g))   # 1
print(list(count_up(3)))   # [0, 1, 2]
运行输出Python 运行结果
运行结果: 0 1 [0, 1, 2]
讲透yield 像 return,但不会“结束函数”,而是“交出一个值后暂停”,下次再继续。理解生成器,就理解了 Python 处理大数据的底层心法。
gen_life.pypython可运行
def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

f = fib()
for _ in range(6):
    print(next(f), end=" ")
print()
运行输出Python 运行结果
运行结果: 0 1 1 2 3 5
gen_pro.pypython可运行
import io
fake_file = io.StringIO("hello\n\nworld\n\npython\n")   # 模拟一个大文件

def blank_lines(lines):                 # 实战:逐行扫描,遇到空行才“产出”
    for line in lines:
        if line.strip() == "":
            yield "空行"

blanks = list(blank_lines(fake_file))
print("空行数:", len(blanks))
运行输出Python 运行结果
运行结果: 空行数: 2
避坑生成器惰性:你不调用 next() 或 list(),它就一行都不执行——新手常以为“函数没运行”。另外它同样只能遍历一次,第二次就空了。

函数式:map / filter / lambda

functional.pypython可运行
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))   # 每个都乘以 2
print(doubled)                               # [2,4,6,8,10]

big = list(filter(lambda x: x > 2, nums))     # 只保留 >2 的
print(big)                                   # [3,4,5]

# 其实用列表生成式更直观,二者等价:
print([x * 2 for x in nums])
运行输出Python 运行结果
运行结果: [2, 4, 6, 8, 10] [3, 4, 5] [2, 4, 6, 8, 10]
讲透lambda 是“一次性小函数”,map 把函数作用到每个元素,filter 按条件筛选。但在 Python 里,往往列表生成式更直白;map/filter 的价值在于和“函数作为参数”的组合思想——这是通往更高级抽象的台阶。
func_life.pypython可运行
prices = ["10.5", "3.0", "20.0", "8.5"]
floats = list(map(float, prices))            # 生活:字符串价格 → 数字
print(floats)
cheap = list(filter(lambda p: p < 10, floats))   # 只要便宜的
print(cheap)
运行输出Python 运行结果
运行结果: [10.5, 3.0, 20.0, 8.5] [3.0, 8.5]
func_pro.pypython可运行
students = [("小明", 88), ("小红", 95), ("小刚", 72)]
ranked = sorted(students, key=lambda s: s[1], reverse=True)   # 实战:按成绩排名
print(ranked)
运行输出Python 运行结果
运行结果: [('小红', 95), ('小明', 88), ('小刚', 72)]
避坑map / filter 返回的是“惰性对象”,不是列表。忘了用 list() 包裹就去索引(如 result[0])会报错。记住:要看结果,先 list()。

装饰器(轻量):给函数“加功能”不改它

先讲为什么想给很多函数统一加上“计时”“登录检查”“打印日志”,又不想改每个函数内部?装饰器就是“在不改动原函数代码的前提下,包一层新行为”的机制。
decorator.pypython可运行
from functools import wraps

def log_calls(func):              # 装饰器:接收原函数
    @wraps(func)                  # 保住原函数的名字、文档
    def wrapper(*args, **kw):
        print(f"调用 {func.__name__},参数 {args} {kw}")
        return func(*args, **kw)
    return wrapper

@log_calls                     # 用 @ 把装饰器“贴”在函数上
def add(a, b):
    return a + b

print(add(3, 4))
运行输出Python 运行结果
运行结果: 调用 add,参数 (3, 4) {} 7
讲透@log_calls 本质就是 add = log_calls(add):把原函数交给装饰器,拿回一个“加强版”。这是“函数也是普通对象、可以传来传去”这一思想的集大成,初学不必深究实现,先记住“@ 能批量给函数加功能"即可。
decor_life.pypython可运行
def retry(times):
    def deco(func):
        def wrapper(*args, **kw):
            for i in range(times):
                try:
                    return func(*args, **kw)
                except Exception:
                    print(f"第{i+1}次失败,重试…")
            return None
        return wrapper
    return deco

@retry(2)
def unstable():
    raise ValueError("网络抖了一下")

print(unstable())
运行输出Python 运行结果
运行结果: 第1次失败,重试… 第2次失败,重试… None
decor_pro.pypython可运行
cache = {}
def memo(func):
    def wrapper(n):
        if n in cache:
            return cache[n]
        cache[n] = func(n)
        return cache[n]
    return wrapper

@memo
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print(fib(10))   # 第 11 个斐波那契数
运行输出Python 运行结果
运行结果: 55
避坑自己写装饰器时,原函数的名字 __name__ 会变成 wrapper(调试时一脸懵)。用 functools.wraps 包住内层函数即可保住原名:
decor_wraps.pypython可运行
import functools
def tag(func):
    @functools.wraps(func)        # 保住原函数元信息
    def wrapper():
        return func()
    return wrapper

@tag
def hello():
    return "hi"

print(hello.__name__)   # hello(不再是 wrapper)
运行输出Python 运行结果
运行结果: hello

案例:用生成式清洗一批文件名

综合 切片 / 生成式 / 函数式

假设有一批带空格和大小写混乱的文件名,用一行生成式把它们统一成“小写、去空格、下划线连接”。

clean_names.pypython可运行
raw = ["  My Photo.JPG", "Report Final.pdf", "DATA_2024.csv"]
clean = ["_".join(name.strip().lower().split()) for name in raw]
print(clean)     # ['my_photo.jpg', 'report_final.pdf', 'data_2024.csv']
运行输出Python 运行结果
运行结果: ['my_photo.jpg', 'report_final.pdf', 'data_2024.csv']
讲透name.strip().lower().split() 是“方法链”:先去空格、再转小写、再按空白切开,每一步都返回新字符串接着用。这种“一串小操作拼成一条流水线”的写法,在真实数据清洗里天天见。

第 3 章小结

动手练习

1用切片把字符串 "Hello, Python" 反转,并取出最后 6 个字符(应为 "Python")。

2用列表生成式生成 1~10 中所有的偶数平方,结果应为 [4, 16, 36, 64, 100]。

提示:切片 [::-1] 反转;生成式里用 if x % 2 == 0 筛选,再算平方。

practice.pypython可运行
s = "Hello, Python"
print(s[::-1])        # 反转
print(s[-6:])         # 最后 6 个字符
evens_sq = [x * x for x in range(1, 11) if x % 2 == 0]
print(evens_sq)
运行输出Python 运行结果
运行结果: nohtyP ,olleH Python [4, 16, 36, 64, 100]