面向对象三大特性

发布时间 2024-01-05 19:23:52作者: ssrheart

面向对象三大特性

  • 面向对象编程有三大特性:
    • 封装、继承、多态
  • 其中最重要的一个特性就是封装。
    • 封装指的就是把数据与功能都整合到一起
  • 除此之外,针对封装到对象或者类中的属性,我们还可以严格控制对它们的访问,分两步实现:隐藏与开放接口

【一】封装

(1)什么是封装

  • 在程序设计中,封装(Encapsulation)是对具体对象的一种抽象
    • 即将某些部分隐藏起来,在程序外部看不到,其含义是其他程序无法调用。
    • 要了解封装,离不开“私有化”,就是将类或者是函数中的某些属性限制在某个区域之内,外部无法调用。
  • 封装其实就是将数据隐藏起来,不让外界发现或使用
  • 接口:将可以允许外界使用的内容通过接口开发,让用户通过接口使用

(2)为什么要封装

  • 封装数据的主要原因是:保护隐私(把不想别人知道的东西封装起来)

(3)如何隐藏属性

  • 隐藏属性的方法是通过 __变量名
class Person:
    name = 'heart'
    age = 18
    location = 'earth1'
    _location = 'earth2'
    __location = 'earth3'

    def show(self):
        # 在类内部,隐藏起来的属性可以调用到
        print(self.location)
        print(self._location)
        print(self.__location)

    def _show(self):
        # 在类内部,隐藏起来的属性可以调用到
        print(self.location)
        print(self._location)
        print(self.__location)

    def __show(self):
        # 在类内部,隐藏起来的属性可以调用到
        print(self.location)
        print(self._location)
        print(self.__location)


p = Person()
# 在类外部,调用不到,两个下划线开头的属性无法直接访问
print(p.location)  # earth1
print(p._location)  # earth2
print(p.__location)  # AttributeError: 'Person' object has no attribute '__location'. Did you mean: '_location'?

p.show() # earth1  earth2 earth3
p._show() # earth1  earth2 earth3
p.__show() # AttributeError: 'Person' object has no attribute '__show'. Did you mean: '_show'?
  • 在类第一次实例化得到对象的时候会将 __school 进行变形,只会在实例化的阶段
class Heart():
    __school = '北京大学'

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

# 类第一次实例化
heart = Heart(name='heart', age=18, gender='male')
print(Heart.__dict__)  # __school 变形成: '_Heart__school': '北京大学'
# print(heart.__school) # 访问不到
print(heart.name)  # heart
  • 如果后续通过对象新增了一个属性,那这个属性和原来封装起来的属性不是同一个
heart.__school = '清华大学'
print(heart.__school)  # 清华大学

(1)隐藏属性访问

class Heart():
    __school = '北京大学'

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

    def __run(self):
        print(f'{self.name} 正在跑步!')

    #接口,暴露给别人
    def run(self):
        self.__run()

# 第一种访问方法
heart = Heart(name='heart', age=18, gender='male')
heart._Heart__run() # heart 正在跑步!

# 第二种访问方法(调用接口)
heart.run() # heart 正在跑步!

(4)装饰器property

  • property 装饰器可以使得定义的方法可以像访问属性一样被访问。
  • 加property之前:
class Heart():
    def __init__(self,height,weight):
        self.height = height
        self.weight = weight

    def bmi(self):
        result = self.weight / (self.height ** 2)
        print(result)

heart = Heart(height=1.8,weight=80)
heart.bmi()
  • 加property之后:
class Heart():
    def __init__(self,height,weight):
        self.height = height
        self.weight = weight

    @property
    def bmi(self):
        result = self.weight / (self.height ** 2)
        return result

heart = Heart(height=1.8,weight=80)
print(heart.bmi)

(1)为什么要用property

  • 将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后计算出来的,这种特性的使用方式遵循了统一访问的原则
class Heart:
    def __init__(self, name):
        # 将属性隐藏起来
        self.__name = name

    @property
    def name(self):
        print(f'我在获取的时候被触发了')
        return self.__name

    @name.setter
    def name(self, name):
        # 在设定值之前进行类型检查
        print(f'我在修改的时候被触发了')
        if not isinstance(name, str):
            raise TypeError('%s 必须是字符串' % name)
        # 通过类型检查后,将值value存放到真实的位置self.__name
        self.__name = name

    @name.deleter
    def name(self):
        print(f'我在删除的时候被触发了')
        raise PermissionError('不能删除')


f = Heart('heart')
print(f.name)  # heart

# 触发name.setter装饰器对应的函数name(f,'god')
f.name = 'god' #我在修改的时候被触发了
print(f.__dict__) # {'_Heart__name': 'god'}

# 触发name.setter对应的的函数name(f,123),抛出异常TypeError
# f.name = 123 #我在修改的时候被触发了

# 触发name.deleter对应的函数name(f),抛出异常PermissionError
del f.name # PermissionError: 不能删除

(5)封装总结

  • 封装数据和函数属性,在类内部,没有用 _ 或者用一个 _ 封装起来的属性,在类内部可以任意访问,在类外部(对象)也可以随意访问

  • 封装数据和函数属性,在类内部,用两个 _ 封装起来的属性,在类内部可以任意访问,在类外部(对象)不可以随意访问

【二】继承

  • 在面向对象编程中,继承(Inheritance)是一种机制,允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。这意味着子类可以重用父类的代码,而无需重新实现相同的功能。继承是面向对象编程中的重要概念之一,它有助于实现代码的重用性和层次结构。

(1)继承的一些关键概念

  1. 父类(基类): 父类是被继承的类。它定义了一组通用的属性和方法。

  2. 子类(派生类): 子类是继承父类的类。它可以使用父类的属性和方法,并可以通过添加新的属性和方法来扩展或修改父类的功能。

  3. 单一继承和多重继承: 单一继承表示一个类只能继承自一个类,而多重继承允许一个类同时继承自多个类。Python 支持多重继承。

  4. 派生: 派生是指创建一个新的类,使用现有类的属性和方法,但可以添加新的特性或重写现有的方法。

  5. 继承的优势:

    • 代码重用: 可以在不重复编写代码的情况下使用父类的功能。

    • 扩展性: 可以通过在子类中添加新的功能来扩展父类。

    • 维护性: 修改父类的功能将自动影响所有继承自该父类的子类。

(2)单继承和多继承

class zoo:
    zoo_name = 'heart动物园'

class Animal:

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

    def speak(self):
        pass

class Dog(zoo,Animal):
    def speak(self):
        return "汪!"

class Cat(Animal):
    def speak(self):
        return "喵!"

dog = Dog("旺财")
cat = Cat("三花")

print(dog.speak())  # 输出: 汪!
print(cat.speak())  # 输出: 喵!
print(dog.zoo_name) # heart动物园

(3)查看继承

  • 查看父类
    • __base__查看第一个父类
    • __bases__查看所有父类
class zoo:
    zoo_name = 'heart动物园'

class Animal:

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

    def speak(self):
        pass

class Dog(zoo,Animal):
    def speak(self):
        return "汪!"

class Cat(Animal):
    def speak(self):
        return "喵!"

print(Dog.__base__) # 查看第一个父类 <class '__main__.zoo'>
print(Dog.__bases__) # 查看所有父类 (<class '__main__.zoo'>, <class '__main__.Animal'>)

(4)继承代码实现和代码重用

  • 在开发程序的过程中,如果我们定义了一个类A,然后又想新建立另外一个类B,但是类B的大部分内容与类A的相同时
  • 我们不可能从头开始写一个类B,这就用到了类的继承的概念。
  • 通过继承的方式新建类B,让B继承A,B会‘遗传’A的所有属性(数据属性和函数属性),实现代码重用
class Animal:

    def eat(self):
        print(f'{self.name} 吃东西!')

    def drink(self):
        print(f'{self.name} 喝水!')

    def shit(self):
        print(f'{self.name} 拉!')

    def pee(self):
        print(f'{self.name} 撒!')


class Cat(Animal):

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

    def cry(self):
        print('喵喵叫')


class Dog(Animal):

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

    def cry(self):
        print('汪汪叫')


cat = Cat(name='三花')
cat.eat()  # 三花 吃东西!
cat.drink()  # 三花 喝水!

dog_one = Dog('旺财')
dog_one.eat()  # 旺财 吃东西!

(5)属性查找顺序

(1)不隐藏属性

  • 属性查找是从下往上找
  • 继承的顺序是从上往下继承
class Foo:
    def f1(self):
        print('Foo.f1')

    def f2(self):
        print('Foo.f2')
        self.f1()


class Bar(Foo):
    def f1(self):
        print('Bar.f1')


b = Bar()
b.f2()
# Foo.f2
# Bar.f1
  • b.f2()会在父类Foo中找到f2
    • 先打印Foo.f2
    • 然后执行到self.f1(),即b.f1()
  • 仍会按照:对象本身->类Bar->父类Foo的顺序依次找下去
    • 在类Bar中找到f1
    • 因而打印结果为Foo.f1

(2)隐藏属性

  • 父类如果不想让子类覆盖自己的方法,可以采用双下划线开头的方式将方法设置为私有的
class Foo:
    # 变形为_Foo__fa
    def __f1(self):
        print('Foo.f1')

    def f2(self):
        print('Foo.f2')
        # 变形为self._Foo__fa,因而只会调用自己所在的类中的方法
        self.__f1()


class Bar(Foo):
    # 变形为_Bar__f1
    def __f1(self):
        print('Bar.f1')


b = Bar()
# 在父类中找到f2方法,进而调用b._Foo__f1()方法,同样是在父类中找到该方法
b.f2()
# Foo.f2
# Foo.f1

(6)派生

  • 派生是指,子类继承父类,派生出自己的属性与方法,并且重用父类的属性与方法
  • 子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找
  • 例如每个老师还有职称这一属性
  • 我们就需要在Teacher类中定义该类自己的__init__覆盖父类的
  • 当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。

(1)super()

  • super 将所有父类里面的__init__方法加载到当前类中

  • 调用super()会得到一个特殊的对象

  • 该对象专门用来引用父类的属性

  • 且严格按照MRO规定的顺序向后查找

class Animal(object):
    def __init__(self):
        self.animal_class = ['Cat', 'Dog']

    def eat(self):
        print(f'{self.name} 正在吃饭!')


class Cat(Animal):
    def __init__(self, name):
        # super 将父类里面的init方法加载到当前类中
        super().__init__()
        self.name = name


cat = Cat(name='三花')
print(cat.animal_class) # ['Cat', 'Dog']

(2)指名道姓的加载 指定类里面的方法

  • 如果继承了很多的类的话,就可以指名道姓的加载
class Animal(object):
    def __init__(self):
        self.animal_class = ['Cat', 'Dog']


    def eat(self):
        print(f'{self.name} 正在吃饭!')


class Cat(Animal):
    def __init__(self, name):
        Animal.__init__(self)
        self.name = name

cat = Cat(name='三花')
print(cat.animal_class) # ['Cat', 'Dog']

(3)补充

  • 如果子类和父类有相同名字的属性,super是放在定义的后面的话,父类里面的初始化数据会把子类里的初始化数据给覆盖掉,就相当于全用的是父类里的属性
class Animal(object):
    def __init__(self,name):
        self.animal_class = ['Cat', 'Dog']
        self.name = '小花'

    def eat(self):
        print(f'{self.name} 正在吃饭!')


class Cat(Animal):
    def __init__(self, name):
        self.name = name
        super().__init__(name)


cat = Cat(name='三花')
print(cat.animal_class) # ['Cat', 'Dog']
print(cat.name)#  小花
  • 如果这个地方,super是放在定义的前面的话,那么会先加载父类里的方法然后子类里替换掉
class Animal(object):
    def __init__(self,name):
        self.animal_class = ['Cat', 'Dog']
        self.name = '小花'

    def eat(self):
        print(f'{self.name} 正在吃饭!')


class Cat(Animal):
    def __init__(self, name):
        # super 将父类里面的init方法加载到当前类中
        super().__init__(name)
        self.name = name

cat = Cat(name='三花')
print(cat.animal_class) # ['Cat', 'Dog']
print(cat.name)#  三花

【三】多态

  • 多态指的是一类事务有多种形态
import abc


# 同一类事物:动物
class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def talk(self):
        pass


# 动物的形态之一:人
class People(Animal):
    def talk(self):
        print('说话')


# 动物的形态之二:狗
class Dog(Animal):
    def talk(self):
        print('汪汪汪')


# 动物的形态之三:猪
class Pig(Animal):
    def talk(self):
        print('哼唧哼唧')

(1)多态性

  • 多态性允许对象以多种形式表现或具有多种行为。多态性有两种主要形式:编译时多态性(静态多态性)和运行时多态性(动态多态性)。

(1)静态多态性

  • 任何类型都可以用运算符+运行运算

(2)动态多态性

  • 利用多态性,你可以编写可以处理多种类型的函数或方法,而不关心实际对象的类型。
  • 总体而言,在Python中,多态性是通过动态类型和动态绑定来实现的。这使得代码更加灵活,能够处理各种不同类型的对象。
import abc


class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def talk(self):
        pass


class People(Animal):
    def talk(self):
        print(f'说话')


class Pig(Animal):
    def talk(self):
        print(f'哼哼')


class Duck(Animal):
    def talk(self):
        print(f'嘎嘎')


people = People()
pig = Pig()
duck = Duck()

people.talk()  # 说话
pig.talk()  # 哼哼
duck.talk()  # 嘎嘎


# 相当于一个接口,不需要知道里面的具体逻辑,只知道调用talk就能实现具体的功能
def talk(obj):
    obj.talk()


talk(people)

补充

鸭子类型

  • 鸭子类型是一种动态类型语言中的编程风格,它强调对象的特征和行为,而不是明确的继承或接口。这种编程风格的名字来自于“鸭子测试”:如果它走起来像鸭子、叫起来像鸭子,那么它就是鸭子。

  • 在Python中,鸭子类型的概念是指,一个对象的适用性不是基于继承层次结构或显示实现接口,而是基于对象的方法和属性。只要对象具有正确的方法和属性,就可以在不显式指定类型的情况下使用它。

class Dog:
    def sound(self):
        return "汪汪汪!"

class Cat:
    def sound(self):
        return "喵喵喵!"

def make_sound(animal):
    return animal.sound()

dog = Dog()
cat = Cat()

print(make_sound(dog))  # 输出: 汪汪汪!
print(make_sound(cat))  # 输出: 喵喵喵!
  • 在这个例子中,DogCat 类都有一个名为 sound 的方法。尽管它们没有共同的基类,但在 make_sound 函数中,我们可以使用它们而不需要显式检查类型。这就是鸭子类型的体现,只要对象有正确的方法,它就能被用作参数,而不必关心其具体类型。

  • 鸭子类型在Python中促使代码更加灵活,能够适应各种对象,而不需要强制使用特定的接口或继承关系。