4.20 -- 面向对象及三大特性

7 阅读7分钟

一、面向对象思维

今天把python的类与对象看完了,简单的复习一下

  1. 首先是面向对象(OOP):就是关注谁来做,定义好一个个对象,让对象自己来完成任务

image.png


2.__init__方法是对象的出厂配置流水线,p = Person()时自动触发调用,不是必须的,其中的self就是刚在内存里被开辟出来的空白对象


3 类属性:就是写在类里,所有对象共享

实例属性:写在__init__里,通过self.绑定,各个对象互不干扰

实例方法:必须带self,权限最大,能访问类属性和实例属性

类方法:由装饰器@classmethod装饰器修饰,第一个参数必须是cls,只能操作公共属性,类名直接调用

静态方法: 由装饰器@staticmethod装饰器修饰,不需要self和cls,只是挂在类里的纯工具函数(如时间转换)


4 python中魔法方法(有没有感觉听起来很好玩)

就是像__init__一样前后都有两个下划线的方法,就像自动感应器,有特定的动作发生,它们就会自动触发

__new__: 它是对象实例化时第一个被调用的方法,真正负责在内存中“捏”出这个对象 。它执行完了,才会轮到__init__ 去做初始化打扮。

__del__: 当对象没用了被垃圾回收机制清理时自动调用。手动用 del xxx 删对象时,如果还有别人在使用它(引用计数不为0),这个方法不会被立刻触发 。

__str____repr__: str() 和 repr() 最主要的差别在于⽬标⽤户。 repr() 的作⽤是产⽣机器可读的输出(如Python代码),⽽ str() 则产⽣⼈类可读的输出。

class Cat:
    安装“诞生感应器”
    def __init__(self):
        print("--> 诞生感应器触发:喵!我出生了!")
        
    安装“打印感应器”
    def __str__(self):
        return "--> 打印感应器触发:我是一只可爱的小猫咪。"

# =======================
动作 1:创造一只猫
注意看:我根本没有写 cat.__init__(),但这行代码运行后,会自动打印第一句话!
my_cat = Cat()

动作 2:打印这只猫
注意看:我根本没有写 cat.__str__(),只是用了最普通的 print,它就自动变成了第二句话!
print(my_cat)


5 在类的外面添加实例方法:将普通函数专门绑定给张三对象,必须使用types.MethodType(方法名,实例对象),而且实例方法必须有self这个参数

import types
 
class Person:
    def __init__(self, name=None):
        self.name = name
 
def eat(self):
    print(f"{self.name}在吃饭")
 
p = Person("张三")
p.eat = types.MethodType(eat, p)
p.eat()  # 张三在吃饭

添加类方法/静态方法 : 直接把外面的函数赋值给类名,添加类方法要传入cls

class Person:
home = "earth"
def __init__(self, name=None):
self.name = name
# 定义类⽅法
@classmethod
def come_from(cls):
print(f"来⾃{cls.home}")
# 定义静态⽅法
@staticmethod
def static_function():
print("static function")
# 动态给类添加⽅法
Person.come_from = come_from
Person.come_from() # 输出: 来⾃earth
Person.static_function = static_function
Person.static_function() # 输出: static function

删除属性与方法 : del 对象.属性名 delattr(对象,属性名)


6 __slot__ : 用来限制实例属性和实例方法的,python中每个实例都是有一个字典来存储的,这样子很灵活,但是也很消耗内存,__slot__ = ("name","age","eat"),可以修改底层结构,不用字典,只用一个固定大小的空间,当然类属性、类方法和静态方法还可以添加,对子类也无效。


面向对象三大特性

1.封装

将变量和函数写入类中的操作即为封装,可以把细节隐藏起来,只暴露出必要的接口使调用者调用

私有化:单下划线,只是一个约定,没有强制力,双下划线,原理是改名,使用_类名__x仍然可以访问

属性和方法都可以私有化

class Person:
    def __init__(self, name, age):
        self.name = name
        # 单下划线: 约定当前属性是不想被暴露的
        # self._age = age
        self.__age = age # 底层使用改名的方式实现私有化

    def __cry(self):
        print("我在偷偷哭,别人不可以看到")

p1 = Person("xzx",23)
print(p1.name)
# print(p1._age) # 不具备约束性,依然可以拿到
# print(p1.__age) # 会报错,'Person' object has no attribute '__age'
print(p1._Person__age) # 23

p1._Person__cry()

property装饰器 : 把类里的一个方法,伪装成一个属性,让代码既安全又优雅。

传统的面向对象思路: 加双下划线,然后使用get和set方法,好用但是啰嗦

@property解决这个问题

class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    # 1. Getter 获取器:读取 age 时触发
    @property
    def age(self):
        if self.__age > 18:
            return 18
        return self.__age

    # 2. Setter 设置器:给 age 赋值时触发
    @age.setter
    def age(self, age):
        # 只有传入的年龄 <= 18 时,才允许修改底层真实数据
        if age <= 18:
            self.__age = age

p1 = Person(name="zsh", age=60)
print(p1.age)  # 输出: 18

# 直接给属性赋值:
p1.age = 20

但是property装饰器只能够读属性,如果想要修改属性,还需要用到setter装饰器,方法名必须一致(就是把私有化的方法去掉双下划线),Python 会根据它们头顶上的装饰器,自动区分它们是用来获取、设置还是删除的。

2.继承

子类(派生类)可以继承父类(基类)中的属性和方法,实现代码重用,可以新增方法,也可以重写父类的方法

子类不能继承私有方法和属性,但是可以通过改名原则去调用,但是不建议(违背了封装原则)


单继承 : 创建实例时,python先会去当前类找 __init__方法,找不到向上去父类寻找

class Person:
    home = "earth"  # 公共类属性

    def __init__(self, name):
        self.name = name  # 公共实例属性

    def eat(self):
        print(f"{self.name} 正在吃饭...")  # 公共方法

# 子类继承父类 Person
class YellowRace(Person):
    color = "yellow"

class WhiteRace(Person):
    color = "white"

class BlackRace(Person):
    color = "black"

# 简化后的测试代码:批量实例化并打印
people = [
    YellowRace("张三"),
    WhiteRace("李四"),
    BlackRace("王五")
]

for p in people:
    print(f"姓名: {p.name} | 肤色: {p.color} | 故乡: {p.home}")
    p.eat()
    print("-" * 25)

多继承 : 调用方法时先在子类中查找,若不存在则从左到右依次查找父类中是否包含方法。

class Person:
    """人的类(爷爷辈)"""
    home = "earth"

    def __init__(self, name):
        self.name = name

    def eat(self):
        print("eating...")

class YellowRace(Person):
    """黄种人(父类2)"""
    color = "yellow"

    def run(self):
        print("running...")

class Student(Person):
    """学生(父类1)"""
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def study(self):
        print("studying...")

# ================= 核心:多继承 =================
class ChineseStudent(Student, YellowRace):  
    """中国学生(同时继承了 Student 和 YellowRace)"""
    country = "中国"

# 测试代码
y1 = ChineseStudent("张三", "三年级")
print(f"老家:{y1.home}, 肤色:{y1.color}, 国家:{y1.country}, 姓名:{y1.name}, 年级:{y1.grade}")

y1.eat()   # 来自 Person
y1.run()   # 来自 YellowRace
y1.study() # 来自 Student

复用父类方法

使用super().方法名(),它会自动根据MRO(方法解析顺序向上去寻找父类中的eat方法)

MRO(方法 :ChineseStudent -> Student (左爸爸) -> YellowRace (右爸爸) -> Person (爷爷) -> object (所有类的老祖宗,Python 自动加的)。

def study(self):
    print("先吃再学")
    super().eat()  # 魔法就在这行
    print("studying...")

父类名.方法名(self):直接找到具体的类名,然后调用里面的方法

def study(self):
    print("先吃再学")
    Person.eat(self)  # 指名道姓让 Person 执行
    print("studying...")

方法重写(override) : 在子类里定义一个和父类一模一样名字的方法。

重写__init

  1. 父类 Person 的 init 负责给对象发“身份证”(绑定 self.name = name)。

  2. 子类 Chinese 为了增加一个“籍贯”属性,重写了 init(self, name, area)。

  3. 灾难发生: 因为子类重写了 init,Python 执行子类的初始化后就下班了。父类的 init 没有被执行!结果就是:这个 Chinese 对象成功拥有了籍贯(area),但失去了名字(self.name 没有被绑定)。

  4. 解决办法:在子类的方法内部,使用super()调用一下父类的init方法


多态

本质 : “同一个指令,发给不同的对象,会产生完全不同的反应。”

好处:下面代码中,如果直接用duck.speak()也行,但是如果新增一个小兔子,那么很多地方都要改代码,但是如果使用let_it_speak(),只需要新建一个小兔类,重写一下speak方法就行。