Python中的面向对象特性

张彤 2021年10月22日 292次浏览

前言

众所周知,Python是非常典型的面向对象语言,一切都是对象。
那么面向对象有如下三个特性

  1. 封装性
  2. 继承性
  3. 多态性

最近在学习Java的过程中,对这三个问题进行了反思。

封装性

示例

  • python的封装性没有Java那么好,甚至可以说是随便,它的私有属性和方法是通过前置下划线实现的。
class Student():
    def __init__(self):
        self._age = 10
        self._name = 'Bob'
    def sayMyName(self):
        return "My name is %s"%(self._name)
    def __sayMyNameQuietly(self):
        return "My name is %s"%(self._name)

if __name__ == '__main__':
    a = Student()
    a.sayMyName()

上面的语句执行后,会返回 My Name is Bob,但是你通过调用a这个实例的方法,是无法访问到age和name属性及sayMyNameQuietly方法的。
这就是python的障眼法封装,
但是你可以通过a._agea._name去访问私有变量,
可以通过a._Student__sayMyNameQuietly() 去访问私有方法。

  • Java这点做的比python好,至少你不能直接访问私有属性和方法。那Java的封装就完美了吗?很明显,你可以通过反射去得到私有变量,这点曾经让我困惑了一段时间,私有变量就是为了不让人访问,为什么python和Java都做不到非常完美呢?

经过学习和思考,我对封装的认识是有误的

封装的目的不是保密,而是隐藏复杂性

在谈封装前,我们需要区分一下抽象(Abstraction)和封装(Encapsulation)的区别,这两点是面向对象非常重要的概念

  1. 封装隐藏了类中经常更改的变量或者方法,防止直接访问或变更这些变量和方法,使得类的鲁棒性更强,在Java中则以getter和setter方法访问。
  2. 抽象也用于隐藏某些东西,但是往往程度更高,比如抽象类(abstract class),接口(interface)等概念。抽象不关心对象具体是什么,只需要知道他能干什么就可以了(定义一类或多类对象的共同特点)

了解了这个区别和他们的目的,我们就可以很明确的知道,把封装和数据保密混为一谈是错误的

Python中的抽象类及getter,setter方法如何实现

  • 作为一款以代码简洁明了的语言,事实上,并不需要抽象类这样的东西,当然,如果你的公司强制要求工程中使用抽象类,那么如何不陷入复杂的getter和setter方法中呢,如何不让那么优雅的代码显得Ja里Ja气呢?
from abc import ABCMeta, abstractmethod
class Person(metaclass=ABCMeta):
    @abstractmethod
    def walk(self):
        pass
    
    @abstractmethod
    def eat(self):
        pass

    @abstractmethod
    def introduceMyself(self):
        pass

class Men(Person):
    def __init__(self,name,age):
        self.__name = name
        self.__age = age
    
    def walk(self):
        print("男人走方步,虎虎生风")

    def eat(self):
        print("男人大块吃肉,大碗喝酒")

    def introduceMyself(self):
        print("俺叫%s"%self.__name)

    @property
    def age(self):
        return self.__age
    
    @age.setter
    def age(self,inputAge):
        if inputAge >= 200:
            print("年龄有误,请重新输入")
        self.__age = inputAge
        return self.__age
    
    @property
    def name(self):
        return self.__name
    
    @name.setter
    def name(self,inputName):
        if not isinstance(inputName,str):
            print("输入的数据类型有误,请重新输入")
        self.__name = inputName
        return self.__name
    
    @name.deleter
    def name(self):
        print("%s 已经人间蒸发了"%self.__name)
        del self.__name

if __name__ == '__main__':
    a = Men("Bob",30)
    a.introduceMyself()
    a.name
    del a.name
    a.name = '李二狗'
    a.name

pythonic !

继承

Python中类的多重继承不像Java中是通过更高的接口实现的,至于这样作的利弊,这里不做讨论了,各有各的好吧,其实我本人更喜欢Java这样的设计风格,避免了不必要的麻烦,比如会飞的旺柴(哮天犬)🐶🐶🐶

继承,在继承机制下形成有层级的类,使得低层级的类可以延用高层级类的特征和方法。继承的实现方式有两种:实现继承、接口继承。

  • 实现继承:直接使用基类公开的属性和方法,无需额外编码。只对延申出的方法和属性编码。

  • 接口继承:仅使用接口公开的属性和方法名称,需要子类实现。比如上面例子中对抽象类的继承,需要自己实现逻辑。(python这样的动态语言,没有严格的接口定义,当然你可以把抽象类写成接口风格)

子类全部继承父类所有的方法和属性以及继承后的方法重写,这里就不再复述了,这里主要谈谈多重继承中的深度优先和广度优先

深度优先和广度优先

  • 多继承带来的一个问题,就是在多代的复杂继承下,当前的子类对于父类们的调用路径,这有点类似树数据结构中的深度优先(depth-first) DST,和广度优先(breadth-first)BST问题。
  • 许多文章将python的多重继承调用方法简单的归结于单一的深度优先或单一的广度优先是不正确的。
  • Python中的多重继承,采用的是C3算法。

C3算法主要用在多重继承中,确定子类应该继承哪一个父类的方法。换句话说,C3超类线性化的输出是确定性的方法解析顺序(MRO:Method Resolution Order)
C3算法得名于它实现了一致性于三种重要特性:
1, 扩展的优先图,
2, 局部优先原则(比如A继承B和C,C继承B,那么A读取父类方法,应该优先使用C的方法而不是B的方法),
3, 单调性原则(即子类不改变父类的方法搜索顺序)。

由上可见,Python多重继承的父类调用规则是,在遇到继承相同的父类的类之前,都是深度优先,之后则是广度优先
首先是一个简单的例子

class A1(object): pass

class A2(object): pass

class B1(A1): pass

class B2(A2): pass

class C(B1, B2): pass


print([x.__name__ for x in C.__mro__]) # 类的调用顺序

>>> ['C', 'B1', 'A1', 'B2', 'A2', 'object']

结果可以看出,先深度优先,直到遇到相同父类object,然后返回来广度优先B2

共享基类(Share Base)的例子

class A(object): pass
class B(A): pass
class C(A): pass
class D_0(B, C): pass
class D_1(B, C): pass
class E_0(D_0): pass
class E_1(D_1): pass
class F(E_0, E_1): pass

print([x.__name__ for x in F.__mro__])
>>> ['F', 'E_0', 'D_0', 'E_1', 'D_1', 'B', 'C', 'A', 'object']

多态

谈到继承,必须先说多态,多态使得继承更具有工程上的意义。
多态:就是指一个类实例的相同方法在不同情形有不同表现形式。

# 一个简单的例子
def a_and_b(a,b):
    return a + b

print(a_and_b(1,2))
print(a_and_b('a','b')

上面代码执行的结果,第一个是3,第二个则是ab.
没有指定a,b的类型,那么一样的函数对于不同类型的执行结果就产生了不同类型的结果,这样的情况就是python的多态性。

鸭子类型

所谓 鸭子类型 就是:如果一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就是鸭子。这个概念的名字来源于 James Whitcomb Riley 提出的鸭子测试。

  • Python的多态和Java的多态还不太一样,Java的多态要求比较严格,Python的多态则秉承协议和鸭子类型,所以只要是你遵循了协议,即使是一个人走路像鸭子,游泳像鸭子,叫起来也像duck,那么他就是鸭子。(一不小心开车了~)

多态的好处是,可以使你的工程更加简洁高效。

# wiki example
class Animal:
    def __init__(self, name):    # Constructor of the class
        self.name = name
    def talk(self):              # Abstract method, defined by convention only
        raise NotImplementedError("Subclass must implement abstract method")

class Cat(Animal):
    def talk(self):
        return 'Meow!'

class Dog(Animal):
    def talk(self):
        return 'Woof! Woof!'

animals = [Cat('Missy'),
           Cat('Mr. Mistoffelees'),
           Dog('Lassie')]

for animal in animals:
    print(animal.name + ': ' + animal.talk())

上面的例子中,你不需要再具体的调用阿猫阿狗了,只需要调用动物这个抽象类就好了。
当然,既然是动态语言,当然要有发散思维,比如

class Person:
    def __init__(self, name):    
        self.name = name
    def talk(self):              
        raise NotImplementedError("Subclass must implement abstract method")

class Men(Person):
    def talk(self):
        return '我,%s,作有钱人的狗有什么不好!'%self.name
    
 
def talk_twice(animal):
    print(animal.talk())
    print(animal.talk())

# 没事叫两声
talk_twice(Cat('Missy'))
talk_twice(Men('大聪明'))

好了,人不如狗,是常有的事情,多态才是对这个复杂世界进行简化的犀利手段。

至此,python的面向对象三大特性全部介绍完毕!