Day12 别再把代码堆成山:类型注解、拆模块,再让对象自己干活
摘要:今天的重点,是把"能跑"往"好维护"推进一步:先用Type Hints把变量和函数的意图写清,再用模块与包把代码拆开,最后进入类、对象、
self和魔法方法。结尾用一套能增删改查、会校验成绩的教务系统,把零散知识点串成真正能落地的代码。
一、类型注解 Type Hints
1.1 变量类型注解
Python 是动态类型语言,变量本身不限制类型。但项目一大,光看代码很难判断某个变量到底该装什么。类型注解就是来解决"可读性"和"提前发现问题"的。
语法很简单:
变量名:数据类型=值fromtypingimportAny a:int=695score:float=98.5hobby:str="Python"flag:bool=Truepic:str|None=Nonedata:Any=Nonenames:list[str]=["A","B"]phones:set[str]={"13309091111"}options:dict[str,int]={"count":0,"total":0}numbers:tuple[int,...]=(10,20,30)goods:tuple[str,int,int]=("手机",5999,1)str | None表示"字符串或 None",list[str]表示"元素是字符串的列表",tuple[int, ...]表示"任意长度的整数元组"。Any则表示这里可以放任意类型,通常用在你确实无法提前确定数据结构的场景。
要记住:类型注解只是提示,不是强制约束。Python 运行时不会因为你写了int就拒绝字符串,但 PyCharm 这类编辑器会提前给你警告。
1.2 类型推断
如果变量是直接赋值、参与运算或由推导式生成,Python 会根据值得到对象的实际类型,编辑器也能据此做类型推断,这时不用再手写注解:
a=695# 推断为 intscore=98.5# 推断为 floatflag=True# 推断为 bool1.3 函数类型注解
函数的参数和返回值也能注解:
defcount_vowel(string:str)->int:count=0forchinstring:ifchin"aeiouAEIOU":count+=1returncountstring: str约束参数,-> int约束返回值。多个参数同理,每个参数后面加上: 类型。
配合 Docstring 使用效果更好:
defcalc_max_min_avg(scores:list[int|float])->tuple[int|float,...]:""" 计算最高分、最低分、平均分 :param scores: 成绩列表 :return: 最高分、最低分、平均分 """max_score=max(scores)min_score=min(scores)avg_score=round(sum(scores)/len(scores),1)returnmax_score,min_score,avg_score二、模块与包
2.1 模块是什么
在 Python 里,一个 .py 文件就是一个模块。模块是程序的基本组织单位,里面可以定义变量、函数、类,也可以写可执行代码。
比如circle_fun.py里定义了两个函数,另一个main.py想用,就必须先导入:
# circle_fun.pydefcircle_area(r):return3.14*r*rdefcircle_len(r):return2*3.14*r# main.pyimportcircle_funprint(circle_fun.circle_area(10))2.2 导入模块的几种写法
常用的导入方式有五种:
| 写法 | 示例 | 调用方式 |
|---|---|---|
import 模块 | import random | random.randint() |
import 模块 as 别名 | import random as rd | rd.randint() |
from 模块 import 功能 | from random import randint | randint() |
from 模块 import 功能 as 别名 | from random import randint as rint | rint() |
from 模块 import * | from random import * | randint() |
importrandomasrdprint(rd.randint(1,100))fromrandomimportrandint,shuffle nums=[1,2,3,4,5]shuffle(nums)print(nums)import *写起来省事,但容易污染命名空间,实际项目里不推荐大面积使用。
2.3 自定义模块与__all__
自己写的.py文件也是模块。可以在模块里定义常量和函数:
# my_fun.pyPI=3.1415926deflog_separator1():print("- "*30)deflog_separator2():print("+ "*30)# main.pyimportmy_funprint(my_fun.PI)my_fun.log_separator1()还可以用__all__控制from module import *到底导入哪些内容:
# my_fun.py__all__=["log_separator1","PI"]不写__all__时,import *会导入所有不以下划线开头的名字;写了之后,就只导入列表里的。
2.4__name__与模块入口
模块里经常要写一些测试代码。如果这些测试在模块被导入时也自动执行,就会影响其他文件。这时可以用内置变量__name__判断当前模块是不是被直接运行:
# my_fun.pydeflog_separator():print("- "*30)if__name__=="__main__":# 只有直接运行 my_fun.py 时,这段代码才会执行log_separator()- 直接运行
python my_fun.py时,__name__的值是"__main__"; - 在别的文件里
import my_fun时,__name__的值是"my_fun"。
所以把测试代码放进if __name__ == "__main__":里,就能做到"直接运行会测试,被导入时不打扰别人"。
2.5 软件包 package
当模块多起来,就把它们放进文件夹里管理,这个文件夹就是包。包里通常有一个__init__.py,用来描述包的信息和import *的行为。
utils/ ├── __init__.py ├── my_var.py └── my_log.py# utils/__init__.py__all__=["my_var","my_log"]# main.pyfromutilsimportmy_varprint(my_var.PI)# 或者importutils.my_log utils.my_log.log()注意:用from 包名 import *导入全部模块时,必须在__init__.py里用__all__指定允许导入的模块列表。
包的导入方式可以按需要选择:
| 写法 | 示例 | 使用方式 |
|---|---|---|
import 包名.模块名 | import utils.my_var | utils.my_var.PI |
from 包名 import 模块名 | from utils import my_var | my_var.PI |
from 包名.模块名 import 功能 | from utils.my_log import log_separator1 | log_separator1() |
from 包名 import * | from utils import * | 需先用__all__指定模块 |
三、面向对象
3.1 面向过程 vs 面向对象
前面写的购物车管理系统、教务系统,基本都是面向过程:把需求拆成一步步的流程,然后按顺序执行。
这种方式直接、适合简单任务,但需求一复杂,所有逻辑都堆在一起,维护会越来越难。
面向对象换了个思路:关注"谁有什么能力",把数据和操作打包成对象,让对象各司其职。
还是盖房子的例子:
- 面向过程:设计 → 采购 → 施工 → 验收,一步一步来;
- 面向对象:找设计师、找施工队、找监理,让不同对象完成各自的工作。
3.2 类与对象
类是模板、图纸,描述一类事物共同的属性和方法;对象是根据类创建出来的具体实例。
classCar:passc1=Car()c1.brand="小米"c1.model="SU7"c1.price=238888print(c1.brand,c1.model,c1.price)这种方式是先创建对象再动态加属性,能跑,但不推荐:属性没有在类里声明,代码可读性和可维护性都差。
类名建议用大驼峰命名法,比如UserInfo、EduManagement。
3.3__init__与self
推荐写法是在类里定义__init__,创建对象时自动调用,用来给属性赋初始值:
classCar:def__init__(self,brand,model,price):self.brand=brand self.model=model self.price=price c1=Car("小米","SU7",238888)print(c1.__dict__)self表示"当前正在创建或使用的对象"。调用Car("小米", ...)时,Python 会自动把c1传给self,所以我们调用时不需要手动传。
c1.__dict__会把对象当前的属性以字典形式展示出来,调试时很方便。比如这里会看到brand、model、price三个键。
__init__属于魔法方法:以双下划线开头和结尾,不需要手动调用,Python 会在合适时机自动执行。
3.4 实例方法
定义在类里的函数叫方法,第一个参数通常是self,通过对象调用:
classCar:def__init__(self,brand,model,price):self.brand=brand self.model=model self.price=pricedefstart(self):print(f"{self.brand}{self.model}启动了")deftotal_cost(self,discount,tax_rate):returnself.price*discount*(1+tax_rate)c1=Car("小米","SU7",238888)c1.start()print(c1.total_cost(0.8,0.01))方法内部可以通过self访问对象自己的属性,这是实例方法最核心的能力。
3.5 魔法方法
常见魔法方法:
| 方法 | 作用 |
|---|---|
__init__ | 初始化对象 |
__str__ | print/str()时的友好描述 |
__repr__ | 面向开发者的精确描述 |
__eq__ | 定义==比较规则 |
__lt__/__le__/__gt__/__ge__ | 定义大小比较规则 |
classCar:def__init__(self,brand,model,price):self.brand=brand self.model=model self.price=pricedef__str__(self):returnf"{self.brand}{self.model}{self.price}"def__eq__(self,other):returnself.brand==other.brandandself.model==other.modelandself.price==other.pricedef__lt__(self,other):returnself.price<other.price没有__str__时,直接print(c1)打印的是对象内存地址;有了之后,输出就变成可读的属性描述。
如果类里没有定义__str__,print()和str()会退而使用__repr__。所以想同时照顾用户和开发者,可以两个都定义。
3.6 实例属性与类属性
属性分两种:
- 实例属性:属于每个对象,各对象独立;
- 类属性:属于类本身,所有对象共享。
classCar:wheel=4# 类属性tax_rate=0.05# 类属性def__init__(self,brand,model,price):self.brand=brand# 实例属性self.model=model self.price=price c1=Car("小米","SU7",238888)c2=Car("宝马","X7",438888)print(Car.wheel)# 4Car.wheel=6print(c1.wheel)# 6,实例没有时找类属性c1.wheel=8# 这不是改类属性,而是给 c1 新增实例属性print(c2.wheel)# 6print(c1.wheel)# 8查找顺序是:先找实例属性,找不到再找类属性。
3.7 案例:面向对象版教务管理系统
用面向对象重构教务系统,至少需要两个类:
Student:存学生姓名和三科成绩;EduManagement:管理学生列表,提供增删改查方法。
classStudent:def__init__(self,name,chinese,math,english):self.name=name self.chinese=chinese self.math=math self.english=englishdef__str__(self):returnf"{self.name}语文:{self.chinese}数学:{self.math}英语:{self.english}"classEduManagement:def__init__(self):self.stu_list=[]deffind_by_name(self,name):forstuinself.stu_list:ifstu.name==name:returnstureturnNonedefinput_score(self,subject):score=float(input(f"{subject}:"))ifscore<0orscore>100:print("成绩必须在 0 到 100 之间")returnNonereturnscoredefadd_stu(self):name=input("姓名:")ifself.find_by_name(name)isnotNone:print("学生已存在")returnchinese=self.input_score("语文")math=self.input_score("数学")english=self.input_score("英语")ifNonein(chinese,math,english):returnself.stu_list.append(Student(name,chinese,math,english))print("添加成功")defupdate_stu(self):name=input("姓名:")stu=self.find_by_name(name)ifstuisNone:print("学生不存在")returnchinese=self.input_score("语文")math=self.input_score("数学")english=self.input_score("英语")ifNonein(chinese,math,english):returnstu.chinese=chinese stu.math=math stu.english=englishprint("修改成功")defdelete_stu(self):name=input("姓名:")stu=self.find_by_name(name)ifstuisNone:print("学生不存在")returnself.stu_list.remove(stu)print("删除成功")deffind_one(self):name=input("姓名:")stu=self.find_by_name(name)ifstuisNone:print("学生不存在")returnprint(stu)defshow_all(self):ifnotself.stu_list:print("暂无学生信息")returnforstuinself.stu_list:print(stu)defstart(self):whileTrue:op=input("1添加 2修改 3删除 4查询 5全部 6退出:")ifop=="1":self.add_stu()elifop=="2":self.update_stu()elifop=="3":self.delete_stu()elifop=="4":self.find_one()elifop=="5":self.show_all()elifop=="6":print("已退出")breakelse:print("不支持的操作")if__name__=="__main__":em=EduManagement()em.start()这个案例把类、对象、__init__、self、实例方法、实例属性、模块入口全部用了一遍。input_score()负责统一校验成绩,find_by_name()负责按姓名定位学生,添加、修改、删除、查询再分别调用它们。以后功能继续增加,也不需要把所有逻辑都塞进start()里。
四、Day12 技术栈总结
把今天的内容压缩成几条:
- 类型注解:
变量: 类型、参数: 类型 -> 返回类型,只是提示,不强制; - 模块:一个
.py文件就是一个模块,用import复用; __name__ == "__main__"用来区分直接运行和作为模块导入;- 包:文件夹 +
__init__.py,用__all__控制导入; - 类:模板;对象:实例;
__init__初始化对象,self指向当前对象;- 实例方法通过对象调用;
- 魔法方法让对象支持打印、比较等操作;
- 实例属性各对象独立,类属性所有对象共享;
- 教务系统案例把增删改查、成绩校验和模块入口串成了一套完整流程。
五、写在最后
面向对象刚接触时容易觉得抽象,但它的核心其实就一句话:把数据和行为打包到一起,让对象自己负责自己的事。
写代码时可以多问自己两个问题:
- 这段数据和操作,是不是应该属于同一个对象?
- 这个方法是"谁"的动作?如果是某个对象自己的行为,就应该放进类里。
等你能自然地设计出Student、EduManagement这类类,并且知道它们之间怎么协作,面向对象就算真正入门了。
感谢阅览
如果这篇对你有帮助,欢迎点赞、收藏、关注。
面向对象建议多写几个小系统练手,教务管理、购物车都是很好的题材。