• 面向对象三大特性——封装(含property)


    一、封装概念

      封装是面向对象的特征之一,是对象和类概念的主要特性。

      封装就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

    二、隐藏属性

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

      其实这仅仅这是一种变形操作,类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式。

    class A:
        __x = 1  # _A__x = 1
    
        def __init__(self, name):
            self.__name = name  # self._A__name='egon'
    
        def __foo(self):   # _A__foo
            print('%s foo run' % self.__name)
    
        def bar(self):
            self.__foo()  # self._A__foo()
            print('from bar')
    
    # 无法找到类的属性和函数:
    # print(A.__x)
    # print(A.__foo)
    
    print(A.__dict__)   # 可以查看到_A__foo;bar这两个函数
    # 输出:{'__module__': '__main__', '_A__x': 1, '__init__': <function A.__init__ at 0x101f211e0>, '_A__foo': <function A.__foo at 0x101f21378>, 'bar': <function A.bar at 0x101f212f0>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
    
    a = A('egon')
    a._A__foo()  # 通过这种方式可以访问类隐藏函数
    # 输出:egon foo run
    a.bar()
    """
    egon foo run
    from bar
    """

      可以看到类的属性和函数在前面加'__',在类定义阶段就发生了变形,变形后在外部就无法通过.__x或.__func来调用。

    1、自动变形的特点

      1)类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。

      2)这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。

      3)在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。

    class Foo:
        def __func(self):   # _Foo_func
            print('from foo')
    
    
    class Bar(Foo):
        def __func(self):   # _Bar__func
            print('from bar')
            
    b = Bar()
    # b.func()   # AttributeError:没有这个属性
    b._Bar__func()  # 输出:from bar

    2、变形需要注意的问题

      1)知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了

    class B:
        __x = 1
    
        def __init__(self, name):
            self.__name = name
    
    print(B._B__x)
    """
    1
    """

      2)变形的过程只在类的定义时发生一次,定义后的赋值操作,不会变形

    >>> class A:
    ...     def __init__(self):
    ...             self.__X=10
    ... 
    >>> a=A()
    >>> a.__dict__
    {'_A__X': 10}
    >>> a.__Y=2131
    >>> a.__dict__
    {'_A__X': 10, '__Y': 2131}    # __Y没有变形

      3)在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的

    class A:
        def __foo(self):  # _A__foo
            print('A foo')
    
        def bar(self):
            print('A.bar')
            self.__foo()  # self._A__foo()
    
    class B(A):
        def __foo(self):  # _B__foo
            print('B.foo')
    
    b = B()
    b.bar()
    """
    A.bar
    A foo  # 只在自己类找方法不去其他类查找,子类不覆盖父类方法
    """
    #正常情况
    >>> class A:
    ...     def fa(self):
    ...         print('from A')
    ...     def test(self):
    ...         self.fa()
    ... 
    >>> class B(A):
    ...     def fa(self):
    ...         print('from B')
    ... 
    >>> b=B()
    >>> b.test()
    from B
    
    
    #把fa定义成私有的,即__fa
    >>> class A:
    ...     def __fa(self): #在定义时就变形为_A__fa
    ...         print('from A')
    ...     def test(self):
    ...         self.__fa() #只会与自己所在的类为准,即调用_A__fa
    ... 
    >>> class B(A):
    ...     def __fa(self):
    ...         print('from B')
    ... 
    >>> b=B()
    >>> b.test()
    from A
    方法私有的情况 

    三、封装的意义

      封装不是单纯意义的隐藏

    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
    
    
    p = People('egon', 18)
    
    # p.tell_info()
    """
    Name:<egon> Age:<18>  # 封装数据,开放接口给外部访问
    """
    
    # p.set_info('Egon', 38) # 修改数据只能通过接口来完成,可以通过接口完成各种限制
    # p.tell_info()
    """
    Name:<Egon> Age:<38>
    """
    
    # p.set_info(123, 38)
    """
    名字必须是字符串类型
    """
    p.set_info('egon', '38')
    p.tell_info()
    """
    年龄必须是数字类型
    Name:<egon> Age:<18>
    """
    封装数据属性

    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, weight, length, height):
            self.name = name
            self.owner = owner
    
            self.__weight = weight
            self.__length = length
            self.__height = height
    
        def tell_area(self):
            return self.__weight * self.__length
    
    
    r = Room('卫生间', 'alex', 10, 10, 10)
    
    print(r.tell_area())  # 不管是求面积还是体积,用户调用的方式不变

      由上例可以看出,只要接口这个基础约定不变,就不用担心代码的改动。

    #类的设计者
    class Room:
        def __init__(self,name,owner,width,length,high):
            self.name=name
            self.owner=owner
            self.__width=width
            self.__length=length
            self.__high=high
        def tell_area(self): #对外提供的接口,隐藏了内部的实现细节,此时我们想求的是面积
            return self.__width * self.__length
    
    
    #使用者
    >>> r1=Room('卧室','egon',20,20,20)
    >>> r1.tell_area() #使用者调用接口tell_area
    
    
    #类的设计者,轻松的扩展了功能,而类的使用者完全不需要改变自己的代码
    class Room:
        def __init__(self,name,owner,width,length,high):
            self.name=name
            self.owner=owner
            self.__width=width
            self.__length=length
            self.__high=high
        def tell_area(self): #对外提供的接口,隐藏内部实现,此时我们想求的是体积,内部逻辑变了,只需求修该下列一行就可以很简答的实现,而且外部调用感知不到,仍然使用该方法,但是功能已经变了
            return self.__width * self.__length * self.__high
    
    
    #对于仍然在使用tell_area接口的人来说,根本无需改动自己的代码,就可以用上新功能
    >>> r1.tell_area()
    封装与扩展

    五、特性(property)

    1、property概念

      property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值。

    2、计算BMI指数示例

      下面以计算BMI指数为例:(bmi是计算而来的,但很明显它听起来像是一个属性而非方法,如果我们将其做成一个属性,更便于理解)

      BMI指数:(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

    (1)普通解决办法

    class People:
        def __init__(self, name, weight, height):
            self.name = name
            self.weight = weight
            self.height = height
    
    p = People('jack', 48, 1.65)
    p.bmi = p.weight / (p.height ** 2)
    
    print(p.bmi)
    """
    17.63085399449036
    egon
    dragon
    名字必须是字符串类型
    dragon
    不允许删除
    """

    (2)添加函数改写的方法

    class People:
        def __init__(self, name, weight, height):
            self.name = name
            self.weight = weight
            self.height = height
    
        def bmi(self):
            print('===>')
            return self.weight / (self.height ** 2)
    
    p = People('SH', 53, 1.70)
    print(p.bmi())  # bmi是一个名词,却使用bmi(),容易误解为一个动作

    (3)添加property装饰器的方法

    class People:
        def __init__(self, name, weight, height):
            self.name = name
            self.weight = weight
            self.height = height
    
        @property    # 应用场景:有一个值是通过计算得来的,首选定义方法,运用property让使用者感知不到
        def bmi(self):
            print('===>')
            return self.weight / (self.height ** 2)  # 必须有返回值
    
    p = People('SH', 53, 1.70)
    print(p.bmi)   # 使用者像访问数据属性一样访问bmi,方法被伪装
    """
    ===>
    18.339100346020764
    """
    p.height = 1.82
    print(p.bmi)
    """
    ===>
    16.000483033450067
    """
    p.bmi = 333  # 报错,看起来像数据属性,其实还是一个方法,不能赋值

    3、property好处

      将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后计算出来的,这种特性的使用方式遵循了统一访问的原则。总结来说:把一些计算得到的属性伪装得像数据属性一样被用户访问。

    六、property其他用法(setgetdelete方法)

      在C++里一般会将所有的所有的数据都设置为私有的,然后提供set和get方法(接口)。Python中通过property来实现这个功能:

    class People:
        def __init__(self, name):
            self.__name = name
    
        def get_name(self):
            return self.__name
    
    p = People('egon')
    print(p.get_name())  # 输出:egon

      在上面的代码中,People的name属性被封装,不能直接访问,因此给类添加了一个get_name()方法来查看内部已经封装的属性。

      但是这种这种情况下,用户的属性调用方式发生了改变。可以通过@property解决该问题。

    class People:
        def __init__(self, name):
            self.__name = name
    
        @property
        def name(self):
            return self.__name
    
    p = People('egon')
    print(p.name)   # 输出:egon

      调用方式已经和普通属性相同,但是在上面bmi的代码中可以看到,这种情况下是不能对隐藏属性赋值的。要实现赋值,需要运用@name.setter装饰器(name被property装饰后才可用)进行如下修改:

    class People:
        def __init__(self, name):
            self.__name = name
    
        @property
        def name(self):
            # print('getter')
            return self.__name
    
        @name.setter
        def name(self, val):
            # print('setter', val)
            if not isinstance(val, str):
                print('名字必须是字符串类型')
                return
            self.__name=val
    
        @name.deleter
        def name(self):
            # print('deleter')
            print('不允许删除')
    
    p = People('egon')
    print(p.name)    # 输出:egon
    p.name = 'dragon'
    print(p.name)   # 输出:dragon  # name修改成功
    
    p.name = 123   # 输出:名字必须是字符串类型(报错)
    print(p.name)  # 输出:dragon
    
    del p.name  # 输出:不允许删除(报错)

      如代码所示,实现了name属性的修改和删除,@name.deleter和@name.setter都是基于name被@property装饰才可用的

  • 相关阅读:
    MySQL数据库的创建&删除&选择
    JS实现异步的几种方式
    十种排序算法实例说明总结
    常用的bug管理工具
    Bootstrap+Hbuilder
    从菜鸟的视角看测试!
    安装numpy和matplotlib
    Eclipse在线安装svn
    重新打个招呼
    <USACO09JAN>气象测量/气象牛The Baric Bovineの思路
  • 原文地址:https://www.cnblogs.com/xiugeng/p/8932429.html
Copyright © 2020-2023  润新知