Python面向对象编程知识及其特性

Python与之前接触过的Java一样都是面向对象编程的编程语言,当然也和Java有着很大的区别与编程思想。就直观而已,Python是一门动态强类型语言,在定义函数时候,都不需要限制参数数据类型,当然Python也有很多自己的优点。

python-oop

0x00 Python中的面向对象

与Java一样,Python中认识事物也是上帝模式,即万物皆为对象,不存在的东西也可以创造出来。大家都说面向对象好,当然它也有自己的缺点:

  • 优点:解决了程序的拓展性。
  • 缺点:可控性差,无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一单开始就是对象与对象的江湖解决问题。

在Python中的面向对象编程中主要有3个术语。

  1. 类:具有同一特征的一类事物
  2. 对象:类下具体的某一个事物
  3. 实例化:将抽象的类描述为一个具体对象的过程

0x01 类的定义及对象的实例化

在Python3.x中所有定义的类均为新式类,而在Python2.x中有新式类和经典类,我暂时在学习的是Python3.5,所以以下类均为新式类。

类的定义非常简单可以通过关键字class来定义,类名首字母一般大写,至于括号可以加也可以不加,不过个人比较推荐即使没有参数的情况下也还是写上吧!!

class NewClass():
    static_test = 'NewClass111'
    def __init__(self,test):
        self.test = test
    def myFunction(self):
        pass

nc = NewClass('!!!!')
print(nc.test)  #!!!!
print(nc.static_test)   #NewClass111
print(nc.__dict__)  #{'test': '!!!!'}

上面示例代码中,第1行就定义了一个类;第2行定义了该类的静态属性(类属性),第4行定义了动态属性,第5~6行定义了该类所拥有的方法。

第8行代码实例化了NewClass类,生成一个名为nc的对象,在实例化的过程中,会先调用__init__方法,因而在里面传入了参数。第9、10行分别是查看了该对象的静态属性与动态属性。仔细想想这个__init__函数是不是和Java里的构造函数的意义一样啊!!

从第11行代码,可以看出类在存储属性的过程其实是存储在一个字典中。当然也可以通过执行NewClass.__dict__来查看,类中方法、属性均存储在字典中。

仔细观察,发现无论是__init__方法还是自定义方法,甚至动态属性中,都有一个self,self是什么呢?

默认情况下方法的调用为NewClass.myFunction(nc),但是这样写很繁琐,而不是nc.myFunction()。这个self的作用就是将对象自己传给该处,因而我们在写方法及动态属性时候都应该加上self。

0x02 一些重要的类属性

上面的代码中已经有一个__dict__了,其可以查看到一个类的字典信息。

Classname.__name__    #查看类名

Classname.__doc__    #查看类的文档

Classname.__base__   #查看类的第一个父类

Classname.__bases__    #查看类的所有父类

Classname.__module__    #类定义所在的模块

Classname.__class__    #实例对应的类

0x03 类命名空间与对象命名空间

对于类的静态属性的静态属性,在修改类的静态属性后对象的静态属性是否会变化呢?或者修改了对象的静态属性,类的静态属性是否会变化呢?那么先来看看下面的代码。

class NewClass():
    static_test = 'xzymoe'
    def __init__(self,test):
        self.test = test
    def myFunction(self):
        pass

nc1 = NewClass('1')
nc2 = NewClass('2')
NewClass.static_test = 'blog'
print(id(NewClass.static_test),NewClass.static_test)
print(id(nc1.static_test),nc1.static_test)
print(id(nc2.static_test),nc2.static_test)
'''
2682704714416 blog
2682704714416 blog
2682704714416 blog
'''

直接修改类的静态属性,对象的静态属性也是跟着修改的!!这里还是可以想象得到的!

那么修改下对象的静态属性吧!

class NewClass():
    static_test = 'xzymoe'
    def __init__(self,test):
        self.test = test
    def myFunction(self):
        pass

nc1 = NewClass('1')
nc2 = NewClass('2')
nc1.static_test = 'blog'
print(id(NewClass.static_test),NewClass.static_test)
print(id(nc1.static_test),nc1.static_test)
print(id(nc2.static_test),nc2.static_test)
'''
2128203804600 xzymoe
2128203831984 blog
2128203804600 xzymoe
'''

可以看见对于赋值操作,修改对象的静态属性,其实独立的,既不会影响到类,也不会影响到其他对象的!那么原理是为什么呢?类的代码不变,仅仅修改下方的操作代码。

nc1 = NewClass('1')
print(nc1.__dict__)
print(NewClass.__dict__)
nc2 = NewClass('2')
nc1.static_test = 'blog'
print(nc1.__dict__)

'''
{'test': '1'}
{'__doc__': None, '__dict__': <attribute '__dict__' of 'NewClass' objects>, '__weakref__': <attribute '__weakref__' of 'NewClass' objects>, '__module__': '__main__', 'static_test': 'xzymoe', 'myFunction': <function NewClass.myFunction at 0x000001285A92A7B8>, '__init__': <function NewClass.__init__ at 0x000001285A92A730>}
{'static_test': 'blog', 'test': '1'}
'''

首先,需要明白的是,在实例化出一个对象的时候,通过查看__dict__的时候,可以从字典里看出并没有静态属性的键值对,因而在调用nc1.static_test的时候,会先在自己的空间里查找有没有该属性,没有的话到类里去找,因而可以调用类的静态属性。而nc1.static_test = ‘blog’并不是调用静态属性,类似于给字典添加了一个新的键值对,因而既不会影响NewClass也不会影响nc2。因而对于静态属性为一个可哈希对象(不可变类型),最好不要通过用对象去调用,而直接通过类去修改。

上面的代码静态属性是一个可哈希对象(不可变类型),那么如果是一个是一个可变类型呢?

class NewClass():
    static_test = ['xzymoe']
    def __init__(self,test):
        self.test = test
    def myFunction(self):
        pass

nc1 = NewClass('1')
print(nc1.__dict__)
nc2 = NewClass('2')
nc1.static_test = ['blog']
print(nc1.__dict__)
print(id(NewClass.static_test),NewClass.static_test)
print(id(nc1.static_test),nc1.static_test)
print(id(nc2.static_test),nc2.static_test)
'''
{'test': '1'}
{'test': '1', 'static_test': ['blog']}
1842381536712 ['xzymoe']
1842381534280 ['blog']
1842381536712 ['xzymoe']
'''

这里可以看出,修改对象的静态属性,也是不影响类与其他对象的静态属性的!这类需要的注意的,这里的“修改”可以通过上面打印__dict__看出,这里并不是修改静态属性而是重新复制静态属性,nc1内并没有static_test,因而也是添加了一个键值对。

那么来试一试直接修改列表里值看看吧!

class NewClass():
    static_test = ['xzymoe']
    def __init__(self,test):
        self.test = test
    def myFunction(self):
        pass

nc1 = NewClass('1')
print(nc1.__dict__)
nc2 = NewClass('2')
nc1.static_test[0] = 'blog'
print(nc1.__dict__)
print(id(NewClass.static_test),NewClass.static_test)
print(id(nc1.static_test),nc1.static_test)
print(id(nc2.static_test),nc2.static_test)
'''
{'test': '1'}
{'test': '1'}
2334403345864 ['blog']
2334403345864 ['blog']
2334403345864 ['blog']
'''

这里可以看出如果修改不可哈希对象(可变对象)内部的直时,类及其所有对象的值都会受到影响,也就是说他们是共用一个静态属性。其原理是,nc1.static_test[0]是修改一个列表,nc1在自己的命名空间里没有发现一个static_test列表,因而去类中寻找。列表的内存地址是固定了的2334403345864,而修改列表里的东西时候,只是列表内部的指针变为了一个新的值,而列表的内存地址依旧没有改变,所以才有了修改不可哈希对象(可变对象)内部的直时,类及所有对象的静态属性都都变化了。

0x04 面向对象中的组合

定义:在一个类中以另外一个类的对象作为数据属性,称之为类的组合。

通过下面的例子来看看吧!

from math import pi

class Circle():
    #定义一个园,并实现一个求面积的方法
    def __init__(self,r):
        self.r = r
    def area(self):
        return pi * self.r**2

class Ring():
    #定义一个圆环,并实现求其面积的方法
    def __init__(self,out_r,inner_r):
        self.out_c = Circle(out_r)
        self.inner_c = Circle(inner_r)
    def area(self):
        return self.out_c.area() - self.inner_c.area()

myRing = Ring(10,5)
#圆环的属性myRing.out_c,myRing.inner_c的值就是园的一个对象,这种就是组合
print(myRing.out_c,myRing.inner_c)
print(myRing.area())
'''
<__main__.Circle object at 0x000001F405DB5D30> <__main__.Circle object at 0x000001F405DBC710>
235.61944901923448
'''

其实这种操作你可能经常用,只是不知道这个就是面向对象中的组合了!!

0x05 继承

单继承

作为面向对象三大特性中在Python最容易理解的就是继承了。这里首先要明确的是与Java里的只能单继承不同,Python支持多继承。

在Python中被继承的类成为父类、超类或者基类;而继承的类成为子类或者派生类。其特点为一类可以被多个类继承,一类也可以继承多个类。

class P():
    def printp(self):
        print('Class P')

class C(P):
    def __init__(self,arg):
        super().__init__()
        self.arg = arg

    def printc(self):
        print('Class C')

p = P()
c = C('a')
p.printp()  #Class P
c.printc()  #Class C
c.printp()  #Class P

在子类的括号中输入父类的名称就就完成了继承的工作。继承后子类拥有一切父类的特性,若在子类中需要实现父类的方法或者属性可以通过super().xxxx来完成,也可以通过P.xxxx(self,args)来完成。P类的括号是空的,在Python3.x中,没有继承的父类,则默认继承Object类。

# super().__init__()
        P.__init__(self)

这两行代码实现的功能完全一样,不过可以发现如果使用父类类名调用的话需要加上self,而使用super()调用的话,则可以不用加。另外super()这个关键字写法,只有在新式类中才有。

这里可以看出,继承的有点就是子类继承的父类的属性与方法,这样可以在子类里少写这些重复的代码,从而实现了减少代码的重复性。

多继承

多继承主要是用在设计模式下,因为Python没有单独的抽象类和接口类,可以通过多继承的方法在部分模块下实现接口的特性。

class A():
    def show(self):
        print('Class A')

class B():
    def show(self):
        print('Class B')

class C():
    def show(self):
        print('Class C')

class D(A,B,C):
    pass

d = D()
d.show()    #Class A

上面的例子,如果D类中有show()方法的话,那么调用d.show()肯定是调用自己的show()方法,而d自己本身没有,其又有3个父类,因而通过结果可以看出,其会默认调用括号中最前面的类。

上面的继承关系很简单,如果理解了,可以参看下经常的钻石继承

diamond-inheritance从图中可以看出钻石继承,钻石继承为D继承了BC,而BC继承了A。

若在D中条用一个ABCD都有的方法时候,我们知道其肯定会调用自己的方法,而如果调用一个只有AC中有的方法呢,是会调用A的方法还是C的方法呢?

class A():
    def show(self):
        print('Class A')

class B(A):
    pass
    # def show(self):
    #     print('Class B')

class C(A):
    def show(self):
        print('Class C')

class D(B,C):
    pass

d = D()
d.show()    #Class C

从结果可以看出,其调用的是C里的方法,为什么呢?在新式类中,默认搜素方法的时候都是采用广度优先,即先找自己上一层的类,一个一个顺着找,如果都没有找到才向更上一层的类中寻找。

Ps.虽然我学习的是Python3.x只有新式类,不过在Python2.x中存在的经典类呢?其调用顺序和新式类的完全相反,如果继承方式同上,那么其最终结果为Class A,因为其搜索方式为深度优先。其会先搜索括号中最近的一个类B,如果没有就搜索类B的上次一层类A,如果A也没有到顶了,那么在找括号中第二个类C的方法。

经典的self的本质

一个复杂的继承关系,在方法中不断的调用supe()方法,看看super()到底是谁?

class A(object):
    def func(self): print('A')

class B(A):
    def func(self):
        super().func()
        print('B')

class C(A):
    def func(self):
        super().func()
        print('C')

class D(B,C):
    def func(self):
        super().func()
        print('D')

b = D()
b.func()
print(B.mro())
'''
A
C
B
D
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
'''

在不看结果之前,想必猜测猜测结果是ABD(不是Android的adb( >﹏<。)~呜呜呜…… )吧!!但是为什么是ACBD呢?其实super()的本质类似于方法搜索一样,在新式类中都是先广度搜索,class D中的super()就是class B,class B中的super()就是class C,class C中的super()就是class A,因为新式类的广度搜索的算法,因而才有了这样的结果。!!!

在新式类中可以通过.mro()方法查看自己的父类。

0x06 接口类与抽象类

由于Java是单继承特性,因而其有两个关键字interface和abstract,而Python是没有的,因为多继承的特性可以让Python实现这两个东西。接口其实是一种设计模式,而在原生Python中是没有接口这个概念的,不过可以通过模块来实现。

from abc import abstractmethod,ABCMeta

class Pay(metaclass=ABCMeta):
    @abstractmethod
    def pay(self,money):pass

class WeChat(Pay):
    def pay(self,money):
        print('WeChat Paid'+ str(money))

class AliPay(Pay):
    def pay(self,money):
        print('AliPay Paid'+ str(money))

class ApplePay(Pay):
    def pay(self,money):
        print('ApplePay Paid'+ str(money))

#在实现各种Pay的时候,我们了可以不用知道具体是哪一个类
def pay(obj_pay,money):
    obj_pay.pay(money)

wc = WeChat()
pay(wc,50)  #WeChat Paid50

这里可以看出通过abc模块,定义了一个类,其中署名了方法名称,而没有实现。通过在metaclass和@abstractmethod语法糖的约束性,继承该类的类都必须实现pay方法。

接口类支援多继承,而抽象类只支持单继承。接口类的使用需要记住默认的接口隔离原则:使用多个专门接口,而不使用单一的总接口,即为客户端不应该以来那些不需要的接口。主要意思就是,接口最好分开写,不要写在一块,最终子类继承该接口中,有一些没有用的方法。

在接口类中,方法只需定义,而不必实现。而抽象类中方法可以实现部分。由于Python的多继承特性,所以在Python中抽象类与接口类的概念不是那么明显,翻看了下《Python核心编程(第二版)》,并没有在其中发现有接口类与抽象类的概念。

0x07 多态

多态是指一类事物有多种形态,如上面的Pay类,下面有ApplePay和WeChat等。其好处是在不用考虑其实例类型的情况下,使用该实例。例子同上。

0x08 封装

在默认情况下Python中类的属性与方法都是“公开的”,Python中通过使用__来定义一个仅仅内部可以访问的属性或者方法即为封装。尽管这样提供了某种层次上的私有化,但算法处于公共域中,因而依然可以被调用(_类名__属性或者方法)。

封装的好处:

  • 将变化隔离
  • 便于使用
  • 提供复用性
  • 提高安全性

其依赖原则为:将不需要对外提供的内容都隐藏起来;把属性都隐藏了,提供公共方法对其访问。

from math import pi

class Circle():
    def __init__(self,r):
        self.__r = r

    def area(self):
        return pi*self.__r**2

c = Circle(5)
print(c.area()) #78.53981633974483
print(c.__r)    #AttributeError

比如上面的例子,可以看出,被封装的属性在外部是无法被对象给调用的,其实方法也一样。

那么父类的被封装的属性或方法在子类中可以使用否?

from math import pi

class Circle():
    def __init__(self,r):
        self.__r = r

    def area(self):
        return pi*self.__r**2

class DoubleCircle(Circle):
    def doubleArea(self):
        return 2*pi*Circle.__r**2   #调用父类的私有属性

c = DoubleCircle(2)
print(c.doubleArea())   #AttributeError

同样是抱错了,可以看出被封装的属性或者方法是无法被子类给继承的!

总结了一下,能在以下三种场合里可以使用到封装。

  1. 隐藏一个属性,不让类外部调用。
  2. 保护这个属性,不让被随意改变。(可以配合get或者set方法来修改这个属性)
  3. 保护这个类属性,不让其被子类给继承。

比如下面定义了一个人类,其有属性name,人可以改名,但是不能直接xxx.name = ‘1234’,因为不能有名字是数字吧!!那么可以配合get和set方法!

class Person():
    def __init__(self,name):
        self.__name = name
    def setName(self,newName):
        if type(newName) is str and newName.isdigit() == False:
            self.__name = newName
        else:
            print('姓名不合法')
    def getName(self):
        return self.__name

moe = Person('xzymoe')
print(moe.getName())    #xzymoe
moe.setName(123)    #姓名不合法
print(moe.getName())    #xzymoe
moe.setName('test')
print(moe.getName())    #test

通过将name属性进行封装,那么我们就不可以给这个实例化的对象随便命名那些不符合规范的名字了!

发表评论