当我不满足于只写几行小脚本,希望用 Python 写点“像样的代码”时,需要的就是这里这几块——函数、模块、字符串和那几种最常用的数据结构。
一、用函数消灭重复代码(这点和 Java 完全共通)
在 Java 里,我们早就习惯了“别写重复代码”。在 Python 里也是一样,只不过写法更轻量一点。
1.1 定义与调用
def fac(num):
"""计算 num 的阶乘"""
result = 1
for n in range(1, num + 1):
result *= n
return result
m = int(input("m = "))
n = int(input("n = "))
print(fac(m) // (fac(n) * fac(m - n)))
我给自己记了三个关键点:
- 用
def 函数名(参数列表):开头,下面缩进就是函数体; -
return返回结果,不写return默认返回None; - 函数第一行的三引号字符串是文档,可以被
help()直接看到。
1.2 默认参数 & 可变参数:不用靠重载也能很灵活
from random import randint # 导入模块里的函数random 是 Python 自带的随机数模块
def roll_dice(n=2): # n=2表示调用该函数没有传值,默认传2
"""摇 n 颗骰子""" # 三引号 """ ... """ 是函数文档字符串(docstring)也能被 help(roll_dice) / IDE 提示看到
total = 0
for _ in range(n): # _ 默认写法 在 Python 里常被当作“我不使用这个循环变量”的标记
# range(n) 生成一个从 0 到 n-1 的序列
total += randint(1, 6)
return total
def add(*args): # *args 表示“可变参数”(variable-length arguments),它会把传进来的多参数打包成一个元组(tuple)
"""任意个参数求和"""
total = 0
for v in args:
total += v
return total
print(roll_dice()) # 使用默认值 n=2
print(roll_dice(3)) # 显式传入 3
print(add(1, 2, 3, 4)) # 可变参数
对比 Java,我觉得最爽的一点是:Python 根本不需要写一堆重载,直接靠:
- 默认参数,把 80% 的场景兜住;
-
*args/**kwargs,搞定“参数个数不定”的情况。
再加上 关键字参数 之后,可读性也很高:
def add3(a=0, b=0, c=0):
return a + b + c
print(add3(c=10, a=1)) # 通过名字传参,顺序可变
二、模块化与入口:写脚本也要有“项目感”
大白话版理解:
- 一个
.py文件就是一个模块; - 想用谁,就
import谁。
常见的导入方式:
import math
from math import sqrt
import module1 as m1
我现在基本强迫自己所有脚本都用这种入口写法,避免“导入就乱跑”的尴尬:
def main():
# TODO: 你的主流程
pass
if __name__ == "__main__":
main()
记忆点:“谁当入口跑,谁的 __name__ 就等于 "__main__"”。被别的脚本 import 时,就安静地只暴露函数 / 类,不去乱执行。
三、字符串:我用得最频繁的内建类型
3.1 定义与转义
s1 = 'hello, world'
s2 = "hello, world"
s3 = """
多行
字符串
"""
s4 = 'I\'m OK'
s5 = r"C:\Users\name\Desktop" # 原始字符串,不处理转义
多行字符串 + 原始字符串这两个特性,对写配置、写正则等场景非常友好,比 Java 里各种转义舒服太多。
3.2 常用操作
s = "hello, world!"
print(len(s)) # 长度
print(s.upper()) # 全大写
print(s.title()) # 每个单词首字母大写
print(s.startswith("hel"))
print(s.endswith("!"))
print("world" in s) # 子串判断
print(s[0:5]) # 切片: hello
print(s[7:]) # world!
print(s[::-1]) # 反转
切片语法一开始有点不习惯,但习惯之后非常顺手,尤其是那些“一行搞定”的小操作。
3.3 格式化(再次强调 f-string)
name = "张三"
age = 18
print(f"{name} 的年龄是 {age}")
对比 Java 里 String.format 或拼接字符串,f-string 基本可以盖棺定论:能用就用它。
四、列表 / 元组 / 集合 / 字典:Python 的“容器四件套”
我脑子里现在已经给这四个东西固定了一个“印象位”:
- 列表 list:可变、有序,类比 Java 的
ArrayList; - 元组 tuple:不可变、有序,适合“一组固定结构的数据”-我自己认为有点类似枚举
- 集合 set:无序、不重复,做去重和集合运算超顺手;
- 字典 dict:键值对,类比 Java 的
HashMap<K, V>。
4.1 列表 list(可变、有序)
nums = [1, 3, 5, 7, 100]
nums.append(200)
nums.insert(1, 400)
nums += [1000, 2000]
print(nums)
print(len(nums)) # 长度
print(nums[0], nums[-1])
# 遍历
for x in nums:
print(x)
# 切片
print(nums[1:4])
print(nums[::-1]) # 反转
# 删除
nums.remove(3) # 按值删
nums.pop(0) # 按下标删
nums.clear() # 清空
列表推导式是 Python 的“味道”之一:
squares = [x * x for x in range(1, 11)]。 # (左闭右开)
evens = [x for x in range(1, 101) if x % 2 == 0]
写多了之后,会自然从“for 循环造列表”转成这种更简洁的写法。
4.2 元组 tuple(不可变、有序)
适合作为“只读数据结构”或“多值返回”。对我来说,有两个典型用法:
- 函数一次性返回多值:
return x, y; - 表示一个不会改的固定结构,比如坐标、配置项。
person = ("张三", 18, "北京")
name, age, city = person # 拆包
# person[0] = "李四" # 会报错,因为元组不可修改
4.3 集合 set(无序、不重复)
set1 = {1, 2, 3, 3, 2}
set2 = set(range(1, 5))
set1.add(4)
set1.discard(2)
print(set1 & set2) # 交集
print(set1 | set2) # 并集
print(set1 - set2) # 差集
print(set1 ^ set2) # 对称差
非常适合:
- 去重;
- 快速判断成员是否存在(期望 O(1))。
4.4 字典 dict(键值对)
类似 Java 的 HashMap<K, V>,但语法轻量很多,也没有泛型那套负担。
scores = {"Alice": 95, "Bob": 80}
scores["Bob"] = 88
scores["Tom"] = 70
print(scores["Alice"])
print(scores.get("Jack", 60)) # 不存在给默认值
for name, score in scores.items():
print(name, score)
scores.pop("Alice", None) # 删除键
scores.clear() # 清空
字典推导式也很常用:
squares = {x: x * x for x in range(1, 6)}
五、生成器与列表生成式:写出高效又优雅的代码
5.1 列表生成式 vs 生成器表达式
# 列表生成式:一次性生成全部元素,占用内存
f = [x * x for x in range(1, 1000)]
# 生成器表达式:按需生成,节省内存
g = (x * x for x in range(1, 1000))
for val in g:
print(val)
简单理解:列表生成式换成圆括号,就变成生成器表达式。
如果数据很多,但我只是顺序消费一遍,那用生成器更省内存。
5.2 用 yield 定义生成器:斐波那契数列
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
yield a
for v in fib(10):
print(v)
yield 的直观感觉是:函数不会一次性把结果全算完,而是每次“吐”一个值出来。
对流式处理、懒加载这种场景非常自然。
六、作用域与全局变量:尽量少用“全局大杀器”
6.1 作用域查找顺序(LEGB)
Python 的变量查找顺序可以用一个缩写记:LEGB
- L:局部作用域(Local)
- E:嵌套作用域(Enclosing)
- G:全局作用域(Global)
- B:内置作用域(Built-in)
配合一个小例子记一下就够用了:
def outer():
b = "outer"
def inner():
c = "inner"
print(a) # 全局
print(b) # 嵌套
print(c) # 局部
inner()
a = "global"
outer()
6.2 修改全局变量:global(一般不推荐多用)
a = 100
def foo():
global a
a = 200
foo()
print(a) # 200
我的给自己定的“底线”是:
- 尽量少用全局变量,多用参数传递和返回值;
- 如果真的要改全局,就显式写
global,提醒一下自己; - 这点和我在 Java 里控制静态全局状态的思路是完全一致的:越少越安全。
七、这一篇我给自己的小结
这一篇主要是把“写脚本”升级为“写可维护代码”需要的那几块打包了一下:
- 函数 + 模块:代码有名字、有边界、有入口;
- 字符串 + 四大容器:绝大多数业务数据都能直接用它们搞定;
- 推导式 + 生成器:在可读性的前提下,让代码简洁一点;
- 作用域意识:少用全局状态,别给未来的自己挖坑。
对我来说,一个很实用的练习方式是:
挑几个平时用 Java + Shell 写的小工具(比如解析日志、整理配置、抓个简单页面),强行用 Python 重写一遍,
并刻意要求自己做到:
- 所有逻辑都收敛在函数里;
- 用 list []/ dict {}/ set {} /元组 ()把数据结构表达清楚;
- 入口统一放在
if __name__ == "__main__":下面。
等这些习惯自然成型之后,我再回头看这篇笔记,基本就是一个“快速刷新记忆”的 cheatsheet 了。