
零基础学Python:Day12!面向对象进阶:魔法方法、属性与继承进阶
昨天我们入门了面向对象编程,学习了类和对象的基本概念,还有三大特性。老规矩,先公布昨天的作业答案,看看你写对了没~
📋 昨日作业答案
1. 定义一个Rectangle(矩形)类,有长和宽两个属性,定义两个方法:计算面积和计算周长,然后创建对象测试
class Rectangle:def __init__(self, length, width):self.length = lengthself.width = widthdef area(self):"""计算面积"""return self.length * self.widthdef perimeter(self):"""计算周长"""return 2 * (self.length + self.width)# 测试rect = Rectangle(5, 3)print(f"矩形面积:{rect.area()}") # 15print(f"矩形周长:{rect.perimeter()}") # 16非常简单,就是定义属性和两个计算方法,逻辑很清晰。
2. 定义一个Person类,有姓名、年龄属性,定义一个introduce方法,输出"我叫xx,今年xx岁"
class Person:def __init__(self, name, age):self.name = nameself.age = agedef introduce(self):print(f"我叫{self.name},今年{self.age}岁")# 测试p = Person("张三", 20)p.introduce() # 输出:我叫张三,今年20岁就是对属性的基本使用,没什么难度。
3. 定义一个Animal父类,有move方法,然后定义Fish(鱼)子类继承Animal,重写move方法输出"鱼在水里游",定义Bird(鸟)子类,重写move方法输出"鸟在天上飞"
class Animal:def move(self):passclass Fish(Animal):def move(self):print("鱼在水里游")class Bird(Animal):def move(self):print("鸟在天上飞")# 测试fish = Fish()fish.move() # 鱼在水里游bird = Bird()bird.move() # 鸟在天上飞考察继承和方法重写,完美实现了多态。
4. 拓展练习:用面向对象写一个简单的计算器类,实现加减乘除四个功能
class Calculator:def add(self, a, b):return a + bdef subtract(self, a, b):return a - bdef multiply(self, a, b):return a * bdef divide(self, a, b):if b == 0:raise ZeroDivisionError("除数不能为零")return a / b# 测试calc = Calculator()print(calc.add(10, 5)) # 15print(calc.subtract(10, 5)) # 5print(calc.multiply(10, 5)) # 50print(calc.divide(10, 5)) # 2.0还加了除数不能为零的判断,非常严谨,这样一个简单的计算器类就写好了。
对完答案,我们继续今天的内容:面向对象进阶!昨天我们学了面向对象的基础,今天我们来学习更深入的知识点,这些知识点在实际开发中非常常用,学会了你的面向对象功底就更扎实了。
🎩 Python中的魔法方法
你有没有注意到,我们之前用的__init__方法,两边都有两个下划线,这种方法在Python中被称为魔法方法,它们有特殊的作用,可以让我们自定义类的行为,让我们的类用起来更自然。
除了__init__,我们再来认识几个最常用的魔法方法:
1. __str__:自定义对象的字符串表示
当你打印一个对象的时候,默认输出的是<__main__.Cat object at 0x...>,这对我们调试代码非常不友好,我们可以用__str__魔法方法自定义打印对象时输出的内容:
class Cat:def __init__(self, name, color):self.name = nameself.color = colordef __str__(self):# 自定义打印输出return f"Cat(name={self.name}, color={self.color})"cat = Cat("橘橘", "橘黄色")print(cat) # 输出:Cat(name=橘橘, color=橘黄色)是不是友好多了?调试的时候打印对象就能直接看到属性值,非常方便,记住:一般来说,每个类最好都写一个__str__方法,方便调试。
2. __repr__:和__str__类似,主要用于调试
__repr__和__str__功能类似,区别是:
__str__给用户看的,更简洁友好__repr__给开发看的,更准确,一般能直接重新创建这个对象
如果只写了__repr__没写__str__,那么打印的时候会默认用__repr__,所以很多时候我们直接写__repr__就行:
class Cat:def __init__(self, name, color):self.name = nameself.color = colordef __repr__(self):return f"Cat(name={self.name!r}, color={self.color!r})"cat = Cat("橘橘", "橘黄色")print(cat) # 输出:Cat(name='橘橘', color='橘黄色')3. __len__:自定义len()函数的行为
如果你对一个对象用len()函数,默认会报错,我们可以用__len__自定义它的行为:
class MyList:def __init__(self, data):self.data = datadef __len__(self):return len(self.data)my_list = MyList([1, 2, 3, 4, 5])print(len(my_list)) # 输出:5这样我们自定义的类就支持len()函数了,非常自然。
4. __call__:让对象可以像函数一样被调用
如果我们写了__call__魔法方法,那么对象就可以像函数一样调用:
class Adder:def __init__(self, n):self.n = ndef __call__(self, x):return self.n + xadd5 = Adder(5)# 对象直接当函数用print(add5(10)) # 输出:15print(add5(20)) # 输出:25这个非常有用,在很多装饰器或者回调函数场景中经常用到,是不是很神奇?
除了这几个,Python还有很多魔法方法,比如__add__让对象支持加号运算符,__getitem__让对象支持索引访问,我们后面遇到再讲,先把这几个常用的记住。
🔒 属性(property):优雅地访问和修改属性
昨天我们讲封装的时候,说了我们可以写getter和setter方法来访问私有属性,比如:
class Person:def __init__(self, age):self._age = agedef get_age(self):return self._agedef set_age(self, new_age):if 0 < new_age < 120:self._age = new_ageelse:print("年龄不合法")但是这样调用的时候需要写p.get_age()和p.set_age(20),有点麻烦,Python给我们提供了property装饰器,可以让我们像访问普通属性一样调用getter和setter,非常优雅:
class Person:def __init__(self, age):self._age = age @propertydef age(self):# getter方法,获取年龄return self._age @age.setterdef age(self, new_age):# setter方法,设置年龄,带校验if 0 < new_age < 120:self._age = new_ageelse:raise ValueError("年龄不合法,必须在1-119之间")# 使用起来就像普通属性一样p = Person(20)print(p.age) # 20,自动调用getterp.age = 25# 自动调用setterprint(p.age) # 25p.age = 150# 抛出 ValueError:年龄不合法哇,是不是优雅太多了?既保留了getter/setter的数据校验功能,又能像普通属性一样访问,不用写get_和set_,这就是property的魅力,实际开发中非常常用!
如果你只写了getter没写setter,那么这个属性就是只读的,不能修改:
class Circle:def __init__(self, radius):self.radius = radius @propertydef area(self):# 计算属性,根据半径计算面积,只读return 3.14159 * self.radius ** 2c = Circle(5)print(c.area) # 输出:78.53975c.area = 100# 报错:can't set attribute这个用法非常适合那种根据其他属性计算出来的属性,完美!
👪 继承进阶:super()调用父类方法
当我们子类重写了父类的方法,但是还想调用父类原来的方法怎么办?这时候我们就需要用super()函数,它可以帮我们调用父类的方法,非常常用,我们来看例子:
假设我们有一个父类Person:
class Person:def __init__(self, name):self.name = namedef introduce(self):print(f"我叫{self.name}")现在我们写子类Student,继承自Person,添加一个school属性,我们需要重写__init__方法,但是我们不想重复写name的初始化代码,我们可以用super()调用父类的__init__:
class Student(Person):def __init__(self, name, school):# 调用父类的__init__方法初始化namesuper().__init__(name)# 子类自己初始化schoolself.school = schooldef introduce(self):# 先调用父类的introducesuper().introduce()# 再添加子类自己的内容print(f"我在{self.school}上学")# 测试s = Student("小明", "清华大学")s.introduce()输出结果:
我叫小明我在清华大学上学完美!我们不用重复写父类已经写过的代码,直接调用父类的方法就行,这就是super()的作用,在继承中非常常用,特别是重写初始化方法的时候基本都要用。
多继承
Python是支持多继承的,也就是一个子类可以继承多个父类,我们来看基本语法:
class A:def method_a(self):print("方法A")class B:def method_b(self):print("方法B")class C(A, B):# C同时继承A和Bpassc = C()c.method_a() # 方法A,继承自Ac.method_b() # 方法B,继承自B多继承的方法查找顺序遵循MRO算法,Python会自动处理,一般来说我们不需要太担心,日常开发中其实很少用到多继承,最常用的就是单继承,所以我们只要掌握单继承和super()的用法就足够了。
🔐 私有属性和私有方法
在Python中,我们可以用双下划线开头来定义真正的私有属性和私有方法,它们不能被外部直接访问,也不能被子类继承:
class Person:def __init__(self, name, password):self.name = name # 公有属性,外部可以直接访问self.__password = password # 私有属性,双下划线开头,外部不能访问def __private_method(self):# 私有方法,双下划线开头,外部不能访问print("这是私有方法")def get_password(self):# 类内部可以访问私有属性return self.__passwordp = Person("张三", "123456")print(p.name) # 正常访问:张三# print(p.__password) # 报错:AttributeError,不能直接访问print(p.get_password()) # 123456,通过类内部方法访问那这个特性什么时候用呢?一般来说,如果你想强调这个属性或者方法是类内部使用的,不希望外部修改或者访问,就用双下划线开头私有,一般场景下单下划线开头的约定就够了,双下划线私有用得不多,但是你要知道它的存在。
🎯 实战小案例:面向对象实现一个图书管理系统
我们今天学了这么多知识点,我们来做一个稍微大一点的实战案例,巩固一下,我们用面向对象写一个简单的图书管理系统:
class Book:"""图书类,记录图书信息"""def __init__(self, book_id, title, author):self.book_id = book_id # 图书IDself.title = title # 书名self.author = author # 作者self.is_borrowed = False# 是否被借出def __str__(self): status = "已借出" if self.is_borrowed else "在馆"return f"ID: {self.book_id}, 书名: {self.title}, 作者: {self.author}, 状态: {status}"class Library:"""图书馆类,管理图书"""def __init__(self):# 存储所有图书,私有属性,不希望外部直接修改self.__books = []def add_book(self, book):"""添加图书"""self.__books.append(book)print(f"添加图书成功:《{book.title}》")def show_all_books(self):"""显示所有图书"""if not self.__books:print("图书馆还没有图书")returnprint("\n当前图书馆所有图书:")for book in self.__books:print(book)def find_book_by_title(self, title):"""根据书名查找图书"""for book in self.__books:if book.title == title:return bookreturn Nonedef borrow_book(self, title):"""借书""" book = self.find_book_by_title(title)if not book:print(f"没有找到名为《{title}》的图书")return Falseif book.is_borrowed:print(f"《{title}》已经被借出了")return False book.is_borrowed = Trueprint(f"成功借出《{title}》")return Truedef return_book(self, title):"""还书""" book = self.find_book_by_title(title)if not book:print(f"没有找到名为《{title}》的图书")return Falseif not book.is_borrowed:print(f"《{title}》没有被借出,不需要还")return False book.is_borrowed = Falseprint(f"成功归还《{title}》")return True# 测试使用if __name__ == "__main__": library = Library()# 添加几本图书 book1 = Book(1, "Python入门", "张三") book2 = Book(2, "Java编程思想", "李四") book3 = Book(3, "算法导论", "王五") library.add_book(book1) library.add_book(book2) library.add_book(book3)# 显示所有图书 library.show_all_books()# 借书测试 library.borrow_book("Python入门") library.show_all_books()# 还书测试 library.return_book("Python入门")运行测试输出:
添加图书成功:《Python入门》添加图书成功:《Java编程思想》添加图书成功:《算法导论》当前图书馆所有图书:ID: 1, 书名: Python入门, 作者: 张三, 状态: 在馆ID: 2, 书名: Java编程思想, 作者: 李四, 状态: 在馆ID: 3, 书名: 算法导论, 作者: 王五, 状态: 在馆成功借出《Python入门》当前图书馆所有图书:ID: 1, 书名: Python入门, 作者: 张三, 状态: 已借出ID: 2, 书名: Java编程思想, 作者: 李四, 状态: 在馆ID: 3, 书名: 算法导论, 作者: 王五, 状态: 在馆成功归还《Python入门》完美!一个简单的面向对象图书管理系统就写好了,我们用到了今天学的:魔法方法__str__、私有属性__books、封装,把图书和图书馆两个类分开,每个类负责自己的功能,逻辑清晰,易于维护,这就是面向对象开发的优势。
📝 今日小结
今天我们学习了面向对象的进阶知识点,总结一下核心内容:
魔法方法:两边带双下划线的特殊方法,用来自定义类的行为,常用的有 __init__(初始化)、__str__(字符串表示)、__len__(支持len())、__call__(对象可调用)property属性:用装饰器优雅实现getter和setter方法,既能做数据校验,又能像普通属性一样访问,非常适合计算属性 super()调用父类方法:子类重写父类方法后,还能调用父类原来的方法,避免重复代码,继承中非常常用 私有属性和方法:双下划线开头,不能被外部直接访问,用来隐藏内部实现,保证数据安全
掌握了今天这些内容,你就可以应对大部分面向对象开发场景了,Python大部分类和库都是用这些知识点实现的。
🏋️ 今日作业
动手练习才能掌握,今天的作业:
1. 给你昨天写的Rectangle类添加__str__魔法方法,打印对象的时候输出长和宽,更友好
2. 给Person类的年龄属性添加property,设置年龄的时候做校验(必须1-120之间),像普通属性一样访问年龄
3. 定义一个Shape父类,有计算面积area方法(默认返回0),然后定义Rectangle子类和Circle子类,继承自Shape,分别实现自己的area方法,用super()调用父类的方法
4. 拓展练习:基于我们今天写的图书管理系统,添加一个功能:根据ID删除图书,试试用面向对象的方式实现
好啦,今天我们学习了面向对象进阶知识点,明天我们来学习Python中非常常用的文件操作,学会怎么读写文件,处理本地数据,敬请期待~
夜雨聆风