面向对象:多态(多态性)封装(隐藏属性)绑定方法与非绑定方法

Posted neozheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象:多态(多态性)封装(隐藏属性)绑定方法与非绑定方法相关的知识,希望对你有一定的参考价值。

多态:

  多态指的是一类事物有多种形态;比如 动物有多种形态:人、狗、猪

如下代码:

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

class People(Animal): #动物的形态之一:人
    def talk(self):
        print(say hello)

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

class Pig(Animal): #动物的形态之三:猪
    def talk(self):
        print(say aoao)

"""
多态动态绑定(在继承的背景下使用时,也称为多态性): 多态性是指在不考虑实例(对象)类型的情况下使用实例
多态性分为静态多态性和动态多态性
静态多态性:如任何类型都可以用运算符“+”进行计算(站在“+”的角度,无需考虑加的是什么类型的实例)
动态多态性:如调用方法(功能、函数)
"""

# 多态性:不考虑实例类型的情况下使用实例,如下分析:
# 不用考虑是人、是狗还是猪,只要是动物,都可以调用 talk()方法
people = People()
pig = Pig()
dog = Dog()

people.talk()
pig.talk()
dog.talk()

# 上面调用talk()功能还能进一步优化,如下:
def func(obj):
    obj.talk()   # 这种方式的好处:只需要写这一个接口就行,想让谁talk()传入谁就行,无需考虑是人、猪、狗(不用再考虑对象的类型)

func(people)

 

 多态性的好处:

  1. 增加了程序的灵活性: 不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如:func(obj);但需要注意的是,多态性是建立在多态的基础上

  2. 增加了程序的可扩展性: 通过继承Animal类创建一个新的类,使用者无需更改自己的代码,还是用func(obj)去调用,如下代码:

class Cat(Animal):
    def talk(self):
        print("say miaomiao")

def func(obj):
    obj.talk()   # 这种方式的好处:只需要写这一个接口就行,想让谁talk()传入谁就行,无需考虑是人、猪、狗(不用再考虑对象的类型)

cat = Cat()
func(cat)


# 运行结果:
# say miaomiao

"""这样我们新增了一个形态Cat,由Cat类产生的实例cat,使用者可以在完全不需要修改自己代码的情况下。使用和人、狗、猪一样的方式调用cat1的talk方法,即func(cat1)"""

 

这种方式虽然也增加了程序的可扩展性,但却不是python崇尚的方式;Python崇尚的是鸭子类型,即“如果看起来像、叫声像而且走路像鸭子,那它就是鸭子”

python程序员通常根据这种行为来编写程序。例如,如果想编写现有对象的自定义版本,可以继承该对象(如上述的Cat类),也可以创建一个外观和行为像、但与它没有任何关系的全新对象;后者通常用于保存程序组合的松耦合度

鸭子类型:

"""
class File:
    def read(self):
        pass

    def write(self):
        pass
"""

class Disk:  # Disk(硬盘)不是File(文件),但却跟文件的方法很像;不用继承File,创建一个跟File相似的类
    def read(self):
        print("disk read")

    def write(self):
        print("disk write")

class Txt:
    def read(self):
        print("txt read")

    def write(self):
        print("txt write")

disk = Disk()
txt = Txt()

disk.read()  #把disk当文件对象去使用
disk.write()
txt.read() 
txt.write()
# 序列类型:列表list、元祖tuple、字符串str, 它们都有一个统计长度 .__len__() 的方法,其实现原理也是鸭子类型(只要是序列类型,就不用在乎是列表、元祖还是字符串,而且没有继承同一个父类)

 鸭子类型:不需要专门制作父类(或抽象类)来约束子类,只要做的像一点就能调用多态性

 

封装:

隐藏类的属性:

"""在属性前加上两个下划线,就把属性变成了隐藏属性;(属性前后都加两个下划线是python的内置函数)"""
class A:
    __x = 1  #  通过 A.__dict__ 能够看出,隐藏属性发生了变形:由 __x 变成了 _A__x

    def __init__(self,name):
        self.__name = name  # 通过 a.__dict__ 能看出,__name 变成了:_A__name

    def __foo(self):   # 同理, _A__foo
        print("run foo")

print(A.__dict__)

# 运行结果:
# {‘__module__‘: ‘__main__‘, ‘_A__x‘: 1, ‘__init__‘: <function A.__init__ at 0x0000006AD34BB9D8>, ‘_A__foo‘: <function A.__foo at 0x0000006AD34BBA60>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘A‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘A‘ objects>, ‘__doc__‘: None}

a = A("neo")
print(a.__dict__)

# 运行结果:
# {‘_A__name‘: ‘neo‘}

 

属性变形的特点:

  1. 在类的外部无法直接调用类的隐藏属性(如 无法通过 A.__x 的方式调用__x)

  2. 在类的内部可以直接调用类的隐藏属性(如 能够通过 .__foo(self)的方式调用 __foo(self)); 原理如下:

class A:
    """类在定义阶段就会运行"""
    __x = 1  # 运行到这一步的时候把 __x = 1 变成了 _A.__x = 1
    
    def __init__(self,name):  # 运行到这一步的时候,程序不会执行 __init__(self,name) 函数,但会检测 __init__()函数中的语法
        self.__name = name    # 所以,程序检测函数语法的时候,会把 self.__name = name 变成 self._A__name = name

    def __foo(self):  # 同理, 经过语法检测, __foo(self) 会变成 _A__foo(self)
        print("run foo")

    def bar(self):
        self.__foo()   #  同理,经过语法检测,self.__foo() 也会变成 self._A__foo(), 这个函数名和 __foo(self)变形后的函数名一样的,所以能够调用
        print("from Bar")

a = A("neo")
a.bar()

# 运行结果:
# run foo
# from Bar

   3. 子类无法覆盖父类 __ 开头的属性(隐藏属性)

"""子类无法覆盖父类隐藏的属性的原因"""
class Foo:
    def __func(self):  # __func 已经变成了 _Foo__func
        print("from Foo")

class Bar(Foo):
    def __func(self):  # __func 变成了 _Bar__func ; 变性后跟Foo中的 __func 名字已经不一样了  
        print("from Bar")

 这种变形需要注意的问题:

  1. 这种隐藏机制并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性就可以拼出名字: _类名__属性,然后就可以直接访问了,如 a._A__x

  2. 变形的过程只在类的定义阶段发生一次,在定以后的赋值操作,不会变形

class B:
    __x = 1  # 在类的定义阶段发生变形: _B__x = 1

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

B.__y = 2   # 这已经是在类的定义之后,所以不会再发生变形

  3. 在继承中,父类如果不想让子类覆盖自己的方法(函数等),可以将自己的方法定义为私有的(隐藏属性)

"""情况1:(前面讲过的知识点)"""
class A:
    def foo(self):
        print("A.foo")

    def bar(self):
        print("A.bar")
        self.foo()

class B(A):
    def foo(self):
        print("B.foo")

b = B()
b.bar()

# 运行结果:
# A.bar
# B.foo

"""情况2"""

class A:
    def __foo(self):  # 类定义阶段变成了 _A__foo
        print("A.foo")

    def bar(self):
        print("A.bar")
        self.__foo()  # 定义阶段变成了 _A__foo, 所以调用的是A的 __foo函数

class B(A):
    def __foo(self): # 定义阶段变成了 _B__foo
        print("B.foo")

b = B()
b.bar()

# 运行结果:
# A.bar
# A.foo

封装的意义:

  1. 封装数据属性:将数据隐藏起来不是目的。隐藏起来后对外提供该数据的接口,然后我们可以在接口上附加上对该数据操作的限制,以此完成对数据属性操作的严格控制

""" 能够明确区分类内类外,控制外部对隐藏属性的操作行为 """

class
People: def __init__(self,name,age): self.__name = name # 类内能够直接调用,但类外不能 self.__age = age def tell_info(self): # 通过隐藏属性,能够自己建一个接口,外部想调用隐藏的属性,必须调用我的接口; print("Name<%s> Age<%s>"%(self.__name,self.__age)) def set_info(self,name,age): # 而且我还能对这个接口加上自己的逻辑 if not isinstance(name,str): print("名字必须是字符串格式") return if not isinstance(age,int): print("年龄必须是数字") return self.__name = name self.__age = age people = People("neo",18) people.tell_info() # 运行结果: # Name<neo> Age<18> people.set_info(123,"NEO") people.tell_info() # 运行结果: # 名字必须是字符串格式 # Name<neo> Age<18> people.set_info("NEO",28) people.tell_info() # 运行结果: # Name<NEO> Age<28>

 

   2. 封装函数属性(方法):隔离复杂度

"""
取款时功能,而这个功能有很多功能组成:插卡、密码认证、输入金额、打印账单、取钱
对使用者来说,只需要知道取款这个功能即可,其余功能我们都可以隐藏起来,这么做就隔离了复杂度,同时提升了安全性
"""
class ATM:

    def __card(self):
        print("插卡")
    def __auth(self):
        print("用户认证")
    def __input(self):
        print("输入取款金额")
    def __print_bill(self):
        print("打印账单")
    def __take_money(self):
        print("取款")

    def withdraw(self):
        self.__card()
        self.__auth()
        self.__input()
        self.__print_bill()
        self.__take_money()

a = ATM()
a.withdraw()

# 运行结果:
# 插卡
# 用户认证
# 输入取款金额
# 打印账单
# 取款

附:封装(隐藏)起来的属性外部无法直接访问,你需要在内部为其建一个接口(函数)

封装的扩展性:

"""定义一个房间的类"""
class Room:
    def __init__(self,name,owner,length,width):
        self.name = name
        self.owner = owner

        self.__length = length
        self.__width = width

    def tell_area(self):
        return self.__length * self.__width

room = Room("501房间","neo",10,9)
print(room.tell_area())
# 运行结果:
# 90

"""现在这个房间想知道它的体积,只需要在__init__中再添加一个参数 height, tell_area 中也添加 height,用户想知道房间的体积继续调用 tell_area就行(即用户不用改变使用功能,直接就能用上扩展后的新功能),这样就实现了封装的扩展"""
class Room:
    def __init__(self,name,owner,length,width,height):
        self.name = name
        self.owner = owner

        self.__length = length
        self.__width = width
        self.__height = height

    def tell_area(self):
        return self.__length * self.__width * self.__height

room = Room("501房间","neo",10,9,3)
print(room.tell_area())
# 运行结果:
# 270

 

property用法:

  property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值 (一种“伪装”方法,把函数属性伪装成数据属性,并能够用调用数据属性的方式去调用这个函数属性)

示例1:

"""
例一:BMI指数(bmi是计算而来的,但很明显它听起来像是一个属性而非方法,如果我们将其做成一个属性,更便于理解)
体质指数(BMI)=体重(kg)÷身高^2(m)
"""
"""常规做法"""
class People:

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

    def bmi(self):  # 在类中把BMI定义一个BMI的功能
        return self.weight / (self.height ** 2)

people = People("egon",1.8,75)
print(people.bmi())

# 运行结果:
# 23.148148148148145

# 想要得到BMI就需要利用 bmi() 这个方法(函数);类中的数据属性的含义是 “什么是什么”,函数属性的含义是“什么去干什么”,
# 但BMI听起来应该是一个数据属性,而不是一种方法,这样使用者也更容易理解。so 如下修改后的代码:

class People:

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

    @property  # property能够让方法当数据属性一样去调用,给使用者的感觉是他在调用一种数据属性而非方法;被property装饰的函数需要有return值
    def bmi(self):
        return self.weight / (self.height ** 2)

people = People("egon",1.8,75)
print(people.bmi)   # 此时想要调用bmi函数 直接people.bmi就行,跟数据属性的调用方式一样

# 运行结果:
# 23.148148148148145

"""被property装饰的函数的函数名不能再被当做变量去添加到其他对象(如people)的独有属性里面"""
people.bmi = 24
print(people.bmi)  # 会报错,因为此时bmi对应的是一种方法

# 运行结果:
# AttributeError: can‘t set attribute

示例2:

# 以下代码没有实际意义,仅作为分析使用
class People:

    def __init__(self,name):
        self.__name = name  # 把self.name隐藏起来;此时外部无法调用name属性,所以需要再创建一个调用隐藏属性的接口

    def get_name(self):
        return self.__name

people = People("egon")
print(people.get_name())   # .get_name()给人的感觉还是在调用一种方法

# 运行结果:
# egon

# 为上述代码添加property
class People:

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

    @property
    def name(self):
        return self.__name

people = People("egon")
print(people.name)   # 加上property后直接利用 people.name 调用

# 运行结果:
# egon

"""
people.name = "EGON"  也不能用这种方式去赋值; 如果想对people.name 进行重新赋值,可用 @name.setter 方法, 想删除people.name,可用 @name.deleter 方法(了解性知识)
"""

 

绑定方法与非绑定方法:

在类内部定义的函数,分为两大类:

  一: 绑定方法:绑定给谁,就应该由谁来调用,谁来调用就会把调用者当做第一个参数(self)自动传入

      1. 绑定到对象的方法: 在类内定义的没有被任何装饰器修饰的

      2. 绑定到类的方法: 在类内定义的没有被任何装饰器修饰的

  二:非绑定方法: 不与类或者对象绑定;所以就不能自动传入参数,它只是类中定义的一个普通工具而已,并且对象和类都可以使用

绑定方法示例解析:

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

    def tell(self):  # 这是绑定方法中的绑定到对象的方法
        print("名字是%s" %self.name)

    @classmethod  # 加上 @classmethod  被装饰的函数就变成了绑定到类的方法
    def func(cls):
        print(cls)

f = Foo("neo")

print(Foo.tell)  # <function Foo.tell at 0x0000009F6354BA60>  # Foo.tell是一个函数,既然是函数就需要按照函数的方式执行,你需要自己传参
Foo.tell(f)  #  手动传入f

print(f.tell)  # <bound method Foo.tell of <__main__.Foo object at 0x0000009F6354AB38>>  # 是一种绑定方法,绑定方法会自动帮你传参
f.tell()  # 把调用者自动传入self中

print(Foo.func)  # <bound method Foo.func of <class ‘__main__.Foo‘>>  # 加了 @classmethod 后,Foo.func也变成了一种绑定方式
Foo.func()  # 跟 print(Foo) 效果一样

# 运行结果:
# <function Foo.tell at 0x0000009F6354BA60>
# 名字是neo
# <bound method Foo.tell of <__main__.Foo object at 0x0000009F6354AB38>>
# 名字是neo
# <bound method Foo.func of <class ‘__main__.Foo‘>>
# <class ‘__main__.Foo‘>

 

非绑定方法示例解析:

还拿上面的示例代码分析:

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

    @staticmethod  # 加上 @staticmethod 被装饰的函数就变成了 非绑定函数;只是类中定义的一个普通工具而已,类和对象都能使用
    def func1(x,y):
        print(x+y)


f = Foo("neo")

print(Foo.func1)
print(f.func1)
# 运行结果:
# <function Foo.func1 at 0x00000009A482BA60>
# <function Foo.func1 at 0x00000009A482BA60>


Foo.func1(1,2)  # 跟普通函数的调用传参方式是一样的
f.func1(1,3)
# 运行结果:
# 3
# 4

绑定方法和非绑定方法的使用:

  1.  """绑定到对象的使用"""

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

    def tell(self):   # 先把函数定义为 tell(),因为这个时候我还不确定需要往 tell里面传入什么参数;我需要根据函数体的功能代码去决定传入什么参数
        """我想实现的功能是 打印对象的name,age,gender 的信息"""
        print("Name:%s  Age:%s  Gender:%s" %(self.name,self.age,self.gender))  # %后面的内容是对象的name、age、gender,对象是有可能发生改变的,所以我需要把对象设置成参数传进来,所以tell内的参数应该是self, 即 tell(self)

p = People("neo",22,"male")
"""绑定给对象,就应该由对象来调用,自动将对象本身当做第一个参数传入"""
p.tell()
# 运行结果:
# Name:neo  Age:22  Gender:male

 

  2. """绑定给类的使用"""

# 当前路径下有一个配置文件 setting.py
# 内容如下:
# name = "苍老师"
# age = 22
# gender = "female"

import setting  # 导入配置文件
class People:
    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender

    def tell(self):
        print("Name:%s  Age:%s  Gender:%s" %(self.name,self.age,self.gender))

    @classmethod  # 根据功能体的代码分析出应该使用 @classmethod
    def read_config(cls):  # 先不往 read_config 函数里面定义参数,我需要根据函数体的功能目标来决定传入什么参数
        """我想实现的功能是 从配置文件中读取个人信息,然后读取到的name、age、gender传入到People中实例化"""
        obj = cls(
            setting.name,
            setting.age,
            setting.gender
        )   # 现在我需要的是传入到People这个类中进行实例化,假如现在有很多类,有可能需要把setting中的信息传入其他的类中进行实例化;如果现在写上People,就相当于把类写死了;所以应该把类当做变量传进来;而绑定给类的方法能够把类当做第一个参数自动传入,所以应该用 @classmethod 这种方法
        # 实例化这个功能写入了People这个类中成为了People的一种方法
        return obj

# 绑定给类的, 就应该由类来调用,自动将类本身当做第一个参数传入
people = People.read_config()  # People这个类 直接调用 read_config(),然后把People自己当做第一个参数传入read_config() # 接下来的效果:People实例化了setting中的name、age、gender,并实例化的对象是obj,把obj这个对象返回并赋值给people, 即people成了实例化后的对象
people.tell()

# 运行结果:
# Name:苍老师  Age:22  Gender:female

  3. """非绑定方法"""

import hashlib
import time

class People:
    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender
        self.id = self.create_id()  # 为__init__ 添加一个 id的属性, id属性的值来源于 create_id()函数的返回值

    """功能目标:根据时间的不同利用hashlib,给对象自动生成一个id"""
    @staticmethod
    def create_id():  # 同理,create_id函数中先不写参数,后面功能体的代码再决定写入什么参数
        m = hashlib.md5(str(time.time()).encode("utf-8"))  # 再把m hexdigest就得到了想要的结果,可以发现这个函数不需要传参,也不需要对象或者类的自动传入,所以应该用 @staticmethod
        """
        hashlib.md5(str(time.time()).encode("utf-8")) 分析:
        这句代码的效果相当于: 
        m = hashlib.md5()
        m.update(str(time.time()).encode("utf-8"))   # m.update只能处理bytes格式,而time.time()是一个数字,先将它str,再利用 .encode("utf-8") 将其变成bytes格式
        """
        return m.hexdigest()

people1 = People("neo",22,"male")
time.sleep(0.1)
people2 = People("苍老师",20,"female")
time.sleep(0.1)
people3 = People("美奈子",18,"female")

print(people1.id)
print(people2.id)
print(people3.id)

# 运行结果:
# 972d042a8c4ab3f6647764d6acf685b6
# ad91c2e65d01f0d1059178a587ca0342
# 96bb3ba6d752fd04b4d09516a951cb39

 

以上是关于面向对象:多态(多态性)封装(隐藏属性)绑定方法与非绑定方法的主要内容,如果未能解决你的问题,请参考以下文章

面向对象三大特性之封装与多态

13.面向对象(多态/(性)/封装)

面向对象三大特征——封装多态接口

面向对象编程三大特性-封装、继承、多态

Python入门-6面向对象编程:07面向对象三大特征(封装继承多态)-继承

python学习8_1 面向对象(继承多态封装)以及零散概念(组合,property,绑定方法与非绑定方法,反射,内置函数)