python中封装 封装

引子

从封装的本身意思去理解,封装就是用一个袋子,把买的水果、书、水杯一起装进袋子里,然后再把袋子的口给封上,照这样的理解来说,封装=隐藏,但是,这种理解是片面的

如何封装

在python中用双下划线开头的方式代表把属性隐藏起来(设置为私有的)

class A:
    __x = 1  # __代表属性隐藏
    def __init__(self,name):
        self.__name = name
    def __foo(self):
        print('run __foo')
    def bar(self):
        self.__foo()
        print('from bar')

慢慢来看

# 打印类的属性
print(A.__x) 
AttributeError: type object 'A' has no attribute '__x'  # 告诉我们没有这个属性

# 打印类的方法
print(A.__foo)
AttributeError: type object 'A' has no attribute '__foo'  # 没有这个方法
    
# 实例化对象,然后访问对象的方法
a = A('xiao')
a.bar()
运行结果为:
run __foo
from bar

为什么直接访问__x属性和__foo方法不行呢?但是实例化对象后就可以访问呢?我们先开看下名称空间

a = A('xiao')
print(A.__dict__)
print(a.__dict__)

运行结果为:
{'__module__': '__main__', '_A__x': 1, '__init__': <function A.__init__ at 0x00000158C37080D0>, '_A__foo': <function A.__foo at 0x00000158C3708158>, 'bar': <function A.bar at 0x00000158C37081E0>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'_A__name': 'xiao
 
 # 原来这些在类定义的时候发生了变形,把__x变成了_A__x,所以调用方式应该是:
 print(A._A__x)  # ---->1
 
 这种方法是可以访问到的,即这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义的变形

这种自动变形的特点

  • 在类的外部无法直接访问到 obj.__AttrName
  • 在类的内部是可以直接使用的,obj.__AttrName,在定义阶段已经改成了正确的调用方式了
    • ​ def bar(self):
      • ​ self.__foo()
        print('from bar')
  • 子类无法覆盖__开头的属性

这种变形需要注意的是

  • 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字
  • 变形的过程中只是在类的定义时发生一次,在定义后的赋值操作,不会变形
class A:
    __x = 1  # __代表属性隐藏
    def __init__(self,name):
        self.__name = name
    def __foo(self):
        print('run __foo')
    def bar(self):
        self.__foo()
        print('from bar')

a = A('xiao')
print(a.__dict__)
a.__Y = 10  # 在定义后的赋值操作不会变形
print(a.__dict__)

# 打印结果为
{'_A__name': 'xiao'}
{'_A__name': 'xiao', '__Y': 10}
  • 在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的
# 正常情况
class foo:
    def func1(self):
        print('from foo')

class bar(foo):
    def func1(self):
        print('from bar')

b = bar()
b.func1()  # 子类重写父类方法

# 运行结果为
from bar

# 如果父类不想让子类覆盖自己的方法,那么就可以将方法定义为私有的
class Foo:
    def __test(self):
        print('From Foo')
    def test(self):
        self.__test()

class Bar(Foo):
    def __test(self):
        print('From Bar')

print(Foo.__dict__)
b = Bar()
b.test()
# 那么这个打印结果就是
From Foo

封装的意义

封装是把属性和方法进行封装,而不是单纯意义上的隐藏。

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

# 封装数据属性:明确的区分内外,控制外部对隐藏属性的操作行为
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

p = People('xiao',22)
p.set_info('xiao',22)
p.tell_info()
此时对属性的修改是间接的修改,在接口上附加操作逻辑,来控制对属性的操作,这就是封装数据属性的意义

封装方法属性目的是隔离复杂度

# 在日常生活中,取款是一个功能,而这个功能有很多功能组成:插卡、密码验证、输入金额、打印账单、取款,但是对于使用者来说,只需要知道取款这个功能就可以了,其余的功能我们都隐藏起来,很明显这样做就隔离了复杂度,同时也提升了安全性

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()

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

封装方法的其他举例:

  • 1.你的身体中没有一处不体现出封装的概念:你的身体把膀胱尿道等等这些尿的功能都隐藏了,然后为你提供了一个尿的接口
  • 2.电视机本身就是一个盒子,隐藏了所有细节,但是一定会对外提供一堆按钮,这些按钮也正是接口的概念,所以说,封装并不是单纯意义上的隐藏
  • 3.快门就是傻瓜相机为相机们提供的方法,该方法将内部复杂的照相功能都隐藏起来了

提示:在编程语言中,对外提供的接口(接口可以理解成一个入口),可以是函数,称为接口函数,这与接口的概念还不一样,接口代表一组接口函数的集合体

封装与可扩展性

# 得到房间的面积
class Room:  # 定义一个房间类
    def __init__(self,name,owner,weight,height):  # 房间的名字、所属者、宽度、高度
        self.name = name
        self.owner = owner

        # 对于使用者来说,我只想知道房间的面积,所以在内部需要隐藏起来
        self.__weight = weight
        self.__height = height

        # 因为我们需要得到房间的面积,所以需要给使用者提供一个接口
    def tell_info(self):
        return self.__weight * self.__height

r = Room('赌场','xiao',10,10)
print(r.tell_info())   # 100

# 但是突然有一天,使用者告诉我想知道房间的体积,那么我们应该还要把length传进去
class Room:  # 定义一个房间类
    def __init__(self,name,owner,weight,height,length):  # 房间的名字、所属者、宽度、高度
        self.name = name
        self.owner = owner

        # 对于使用者来说,我只想知道房间的面积,所以在内部需要隐藏起来
        self.__weight = weight
        self.__height = height
        self.__length = length

    # 因为我们需要得到房间的面积,所以需要给使用者提供一个接口
    def tell_info(self):
        return self.__weight * self.__height * self.__length

r = Room('赌场','xiao',10,10,10)
print(r.tell_info())   # 1000,

1.对于使用者来说,我只需要知道房间的面积、体积是多少,我不需要考虑你们是如何计算的,就像ATM一样,我只需要调用withdraw功能取款就可以了,具体的验证我不用关心
2.使用者根本就不需要改变自己的使用方式,直接就用上了新功能,这就是封装可扩展性

property装饰器

刚刚我们已经讲过了,房间的面积体积是通过计算出来的,也就是我们提供了一个接口,但在使用者看来,这就是一个方法而并非是属性,什么意思呢?我们打一个比喻

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

    def run(self):
        print('%s is run'%self.name)

    def eat(self):
        print('%s is eat'%self.name)

p = Peopon('xiaoyafei',22)
print(p.name)
print(p.age)
p.run()
p.eat()

# 运行结果为:
xiaoyafei
22
xiaoyafei is run
xiaoyafei is eat

在上面的代码中,我们可以很明显的知道了,name和age是名词,是对象的属性,而run和eat属于动词,是方法

但是在这个房间中,房间的面积/体积就是一个名字,就应该成为一个属性,就像我们调用p.name/p.age这样可以便捷的访问到属性,所以在提供面积的接口这里应该加上:

class Room:  # 定义一个房间类
    def __init__(self,name,owner,weight,height,length):  # 房间的名字、所属者、宽度、高度
        self.name = name
        self.owner = owner

        # 对于使用者来说,我只想知道房间的面积,所以在内部需要隐藏起来
        self.__weight = weight
        self.__height = height
        self.__length = length

    # 因为我们需要得到房间的面积,所以需要给使用者提供一个接口
    @property
    def tell_info(self):
        return self.__weight * self.__height * self.__length

r = Room('赌场','xiao',10,10,10)
print('房间的面积是:',r.tell_info)

# 运行结果为
房间的面积是: 1000

让我们举一个例子,BMI指数

    成人的BMI数值:

    过轻:低于18.5

    正常:18.5-23.9

    过重:24-27

    肥胖:28-32

    非常肥胖, 高于32

    体质指数(BMI)=体重(kg)÷身高^2(m)

    EX:70kg÷(1.75×1.75)=22.86

BMI的指数和人有关,所以需要出创建一个人的类

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

p = People('xiaoyafei',75,1.81)
p.bmi = p.weight / (p.height ** 2)
print(p.bmi)  # 22.89307408198773

我们如果是计算一个人的BMI指数,那么这个代码就够了,如果要计算很多人的话:

p = People('xiaoyafei',75,1.81)
p1 = People('xiaoyafei',76,1.87)
p2 = People('xiaoyafei',77,1.86)
p3 = People('xiaoyafei',78,1.84)
p4 = People('xiaoyafei',79,1.83)
p5 = People('xiaoyafei',75,1.81)

p.bmi = p.weight / (p.height ** 2)
p1.bmi = p.weight / (p.height ** 2)
...

print(p.bmi)
print(p1.bmi)
...

如果每次都要写一次这样的话,就会非常麻烦,所以我们就要在类里给用户提供一个接口:

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

    def bmi(self):
        return p.weight / (p.height ** 2)
    
p = People('xiaoyafei',75,1.81)
print(p.bmi())  # 22.89307408198773

这样的话,已经实现了需求,但是现在就需要加上(),bmi是属于身体的一个名词,如:身高、体重等,是人的一个属性,但是呢?想得到bmi指数,就需要去调用bmi函数,那么我们就想要查询属性这样简单的查询bmi

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

    @property
    def bmi(self):
        print('------->')
        return p.weight / (p.height ** 2)

p = People('xiaoyafei',75,1.81)
print(p.bmi)

# 运行结果为
------->
22.89307408198773

只需要使用p.bmi,就触发了这个方法的执行,对于使用者来说,使用者是可以像访问数据属性一样访问函数属性,本质上是触发了bmi方法的执行,把返回值拿了回来,实现了统一访问的原则,看起来是一个名词的东西,就不要去调用方法,给使用者造成了误解,让使用者能够感觉到就像是在访问函数属性一样

注意:

  • property方法必须要有返回值

property补充

那么我们刚刚说了就像访问属性一样去触发方法的执行,那么我们能不能对bmi赋值

p.bmi = 111

AttributeError: can't set attribute

答案是不能的,因为bmi方法实际上是一个方法,我们只是通过某种手段让他变的像一个属性,它真正还是方法

为什么要用property

将一个类的函数定义成特性后,对象再去调用的时候obj.name,根本无法察觉到自己的name是执行了一个函数然后计算出来的,这种特性的使用方法遵循了统一的访问原则

除此之外,看下

ps:面向对象的封装有三种方式:
【public】
这种其实就是不封装,是对外公开的
【protected】
这种封装方式对外不公开,但对朋友(friend)或者子类(形象的说法是“儿子”,但我不知道为什么大家 不说“女儿”,就像“parent”本来是“父母”的意思,但中文都是叫“父类”)公开
【private】
这种封装对谁都不公开

python并没有在语法上把它们三个内建到自己的class机制上,在c++里一般会将所有的数据都设置为私有的,然后提供set和get方法(接口)去设置和获取,在python中通过property方法可以实现

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

    @property  # 添加一个装饰器
    def name(self):
        print('getter 方法')
        return self.__name

    @name.setter  # setter方法
    def name(self,val):
        print('name.setter方法')
        if not isinstance(val,str):
            print('名字必须是字符串类型')
            return
        self.__name = val

    @name.deleter  # delter方法
    def name(self):
        print('name.delter方法')
        print('不允许删除')

p = People('xiao')

那么我们现在如果想获取name属性的值,就需要

print(p.name)

# 运行结果
getter 方法
xiao

如果想要修改name的值,就需要

p.name = 'XIAOYAFEI'
print(p.name)

# 运行结果
name.setter方法  # 先修改了name属性
getter 方法  # 然后获取name属性
XIAOYAFEI

如果想删除name属性的话

del p.name  # del代表删除

# 运行结果
name.delter方法
不允许删除

property可以把计算才能得到的属性封装成一个让用户访问就像访问数据属性一样的,比如访问name,不添加装饰器是p.name(),调用的是一个方法,而添加了装饰器,就是p.name

绑定方法和非绑定方法

在类内部的定义就是变量和函数的定义,在类内部定义的函数分为两大类:绑定方法与非绑定方法

绑定到对象的方法,在类内部定义的函数不经过任何的装饰,那么这个函数就是绑定给对象使用的,它是类的函数属性,但不是给类用的,如果类一定要用,就没有自动传值的说法了。给对象用的话就是把对象作为第一个参数传递进去

绑定方法又分为两种:绑定到对象的方法和绑定到类的方法

绑定方法

绑定到对象的方法:在类里定义的没有被任何装饰器装饰过的

class Foo:
    def __init__(self,name,age):
        self.name = name
        self.age = age
    def tell_info(self):  # 没有被任何装饰器装饰过的方法是绑定到对象的方法,谁来调用就把谁当做第一个参数传递进去
        print('Name:<%s>  Age:<%s>'%(self.name,self.age))

f = Foo('肖亚飞',22)
f.tell_info()
print(Foo.tell_info)
print(f.tell_info)  

# 此时的运行结果是:
Name:<肖亚飞>  Age:<22>  
<function Foo.tell_info at 0x000001BB0C960D08>  # 代表这只是一个函数,那么如果是函数的话,那么就需要手动传值
<bound method Foo.tell_info of <__main__.Foo object at 0x0000028515EDEB70>>  绑定方法


如果代表一个函数听不懂,看下面的代码
def func1():
    print('aaaaaaaaa')

print(func1)

结果为:
<function func1 at 0x0000022134102E18>

绑定到类的方法:在类内部定义的被classmethod修饰的方法

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

    def tell_info(self):
        print('名字是%s' % (self.name))

    @classmethod
    def func(cls):
        print(cls)

print(People.func)
People.func()

# 运行结果如下
<bound method People.func of <class '__main__.People'>>  # 绑定到类的方法
<class '__main__.People'>

非绑定方法

非绑定方法就没有自动传值这么一说了,就是在类中定义的一个普通工具,对象和类都可以使用

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

    def tell_info(self):
        print('名字是%s' % (self.name))

    @classmethod
    def func(cls):
        print(cls)

    @staticmethod # 在类里面写函数,没有自动绑定
    def func1(x,y): 
        print(x+y)

p = People('肖亚飞')
print(p.func1)
print(People.func1)

p.func1(1,2)
People.func1(1,2)

# 运行结果是:
<function People.func1 at 0x000001E0D02A9158>  # 非绑定方法
<function People.func1 at 0x000001E0D02A9158>
3
3

那么,讲完了绑定方法非绑定方法的概念,现在来讲讲什么情况应该使用什么方法吧!

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

    # 我们现在想查看这个人的信息,根据函数体的逻辑来看看传递什么进去
    def tell_info():
        print('Name:%s Age:%s Sex:%s' )
        
	在这段代码中,我还没有写完,我现在有一个需求,我要查看这个人的信息,那么应该要把什么传递进去?如果绑定到对象的方法,那么就应该由对象来调用,自动会将本身当做第一个参数传递进去,而绑定到类的方法,需要手动传值。
	看一下我们的重点,我们想要的到的是这个人的信息,那么这个人在哪里?当然是实例化后生成的对象,所以应该使用绑定给对象的方法
class People:
    def __init__(self,name,age,sex):
        self.name = name
        self.age = age
        self.sex = sex

    # 我们现在想查看这个人的信息,根据函数体的逻辑来看看传递什么进去
    def tell_info(self):
        print('Name:%s Age:%s Sex:%s' % (self.name, self.age, self.sex))

p = People('肖亚飞',22,'male')
p.tell_info()

# 运行结果为:
Name:肖亚飞 Age:22 Sex:male
    
我们首先在settings.py文件配置用户信息
name = '肖亚飞'
age = 18
sex = 'male'

# 代码开始
import settings

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

    def tell_info(self):
        print('Name:%s Age:%s Sex:%s' % (self.name, self.age, self.sex))
        
    #现在我们想从配置文件里读取配置进行实例化,应该怎么做呢
    def from_conf():
        obj = People(
            settings.name,
            settings.age,
            settings.sex
        )

那么我们想一下,我们之前实例化是什么样的?p = People('aaa',18,'male'),但是现在需要去配置文件里面去找,所以,我们就需要这个类去找,因为我们现在已经不是手动传值了
import settings

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

    def tell_info(self):
        print('Name:%s Age:%s Sex:%s' % (self.name, self.age, self.sex))

    #现在我们想从配置文件里读取配置进行实例化,应该怎么做呢
    @classmethod  # 表示会把类作为第一个参数传递进去
    def from_conf(cls):
        obj = cls(  # 因为类名可能会改,所以就需要一个参数,外部能把这个类传进来
            settings.name,
            settings.age,
            settings.sex
        )
        return obj

# 绑定给类,就应该由类来调用,会把类作为第一个参数传递进去
p = People.from_conf()
p.tell_info()

# 运行结果为
Name:肖亚飞 Age:18 Sex:male
            
绑定到类,实例化的时候读取配置进行实例化,那么这就是一个功能,就可以写在类里面
import settings

import time
import hashlib

class People:

    def __init__(self,name,age,sex):
        self.id = self.create_id()
        self.name = name
        self.age = age
        self.sex = sex

    def tell_info(self):
        print('Name:%s Age:%s Sex:%s' % (self.name, self.age, self.sex))

    #现在我们想从配置文件里读取配置进行实例化,应该怎么做呢
    @classmethod
    def from_conf(cls):
        obj = cls(  # 因为类名可能会改,所以就需要一个参数,外部能把这个类传进来
            settings.name,
            settings.age,
            settings.sex
        )
        return obj

    def create_id():
        m = hashlib.md5(str(time.time()).encode('utf-8'))
p = People.from_conf()
p.tell_info()

我们在类中创建了一个函数create_id,然后在初始化时调用了这个函数,现在问题来了,我需要指定是绑定到类的方法还是绑定到对象的方法吗?当然不是,因为这就是一个时间戳然后转化成md5码,谁与谁调用没有关系

import settings

import time
import hashlib

class People:

    def __init__(self,name,age,sex):
        self.id = self.create_id()
        self.name = name
        self.age = age
        self.sex = sex

    def tell_info(self):
        print('Name:%s Age:%s Sex:%s' % (self.name, self.age, self.sex))

    #现在我们想从配置文件里读取配置进行实例化,应该怎么做呢
    @classmethod
    def from_conf(cls):
        obj = cls(  # 因为类名可能会改,所以就需要一个参数,外部能把这个类传进来
            settings.name,
            settings.age,
            settings.sex
        )
        return obj
    @staticmethod
    def create_id():
        m = hashlib.md5(str(time.time()).encode('utf-8'))
        return m.hexdigest()  # 返回值在实例化对象的初始化方法时已经存在了这个对象的名称空间里了
p = People.from_conf()
p.tell_info()

p1 = People('egon',18,'male')
print(p1.id,time.time())

# time.sleep(2)

p2 = People('egon1',98,'male')
print(p2.id,time.time())

此时不管是类还是对象,获取的都是时间戳的md5,所以这就是非绑定方法的作用, 类和对象都可以调用,没有自动传值的说法

# 运行结果为:
Name:肖亚飞 Age:18 Sex:male
a7666f1d3ab41cdf1d91369bec0fabe0 1527674079.019101
16d9ab79bb41990df733c0db5053a1cd 1527674079.019101