☰
Day12 别再把代码堆成山:类型注解、拆模块,再让对象自己干活
2026/11/4 12:15:12 网站建设 项目流程

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# 推断为 bool

1.3 函数类型注解

函数的参数和返回值也能注解:

defcount_vowel(string:str)->int:count=0forchinstring:ifchin"aeiouAEIOU":count+=1returncount

string: 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 randomrandom.randint()
import 模块 as 别名import random as rdrd.randint()
from 模块 import 功能from random import randintrandint()
from 模块 import 功能 as 别名from random import randint as rintrint()
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_varutils.my_var.PI
from 包名 import 模块名from utils import my_varmy_var.PI
from 包名.模块名 import 功能from utils.my_log import log_separator1log_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指向当前对象;
  • 实例方法通过对象调用;
  • 魔法方法让对象支持打印、比较等操作;
  • 实例属性各对象独立,类属性所有对象共享;
  • 教务系统案例把增删改查、成绩校验和模块入口串成了一套完整流程。


五、写在最后

面向对象刚接触时容易觉得抽象,但它的核心其实就一句话:把数据和行为打包到一起,让对象自己负责自己的事。

写代码时可以多问自己两个问题:

  1. 这段数据和操作,是不是应该属于同一个对象?
  2. 这个方法是"谁"的动作?如果是某个对象自己的行为,就应该放进类里。

等你能自然地设计出Student、EduManagement这类类,并且知道它们之间怎么协作,面向对象就算真正入门了。


感谢阅览

如果这篇对你有帮助,欢迎点赞、收藏、关注。
面向对象建议多写几个小系统练手,教务管理、购物车都是很好的题材。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询