Python面向对象的学习

面向对象(Object Oriented Programming)编程主要有三大特性:继承、封装、多态。那就从这三个方面来学习一下,也会具体说说Python类的一些特性,如新式类和旧式类,__slots__,super等等。

 

1、封装

封装是最好理解的,它是将内容都封装到类中,隐藏了具体的细节,然后通过属性方式来访问。

如:

 class A:

       name = 's'   
       def __init__(self,a,b):
           self.a = a
           self.b = b

上面一个简单的例子,将a,b两个值封装到A的属性a,b中。我们访问则是通过A().a,A().b访问。

属性也分类属性和实例属性,name是类属性,a,b是实例属性。当我们定义类属性后,就不要在实例属性中定义了,否则会覆盖掉类属性。

现在具体说一下属性。

类属性一般都保存在类的__dict__中,而实力属性一般都保存在实例的__dict__中。我们可以把相应的值打印出来:

class A(object):
   age = 43
   def __init__(self):
       print 'A'
       self.name = 'a'
       self.age = 23

   def test(self):
        print 'test'

a = A()
print a.__dict__
print a.age
print a.__class__.__dict__


输出:
A
{'age': 23, 'name': 'a'}
23
{'__module__': '__main__', 'age': 43, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None, '__init__': <function __init__ at 0x000000000268FEB8>}

具体分析一下上面的例子:

  1. age分别有一个类属性和实例属性,分别保存在实例和类的__dict__中。
  2. 当我们print age的时候,是打印实例属性,而不是类属性,这和点号运算搜索过程有关。
  •      实例本身的dict,a.__dict__;
  •      类的dict,a.__class__.__dict__;
  •    调用定义的__getattr__或者__getattribute__方法(新式类,具体在我的Python内置特性一文中有详细介绍)
  •     子类;

                    如果经过上述几个步骤依然找不到属性,就会抛出ArrtibuteError。

             那现在问题来了,实例的方法到底是什么访问过程呢,即a.test()?其实a.test()其实就相当于:a.__class__.__dict__['test'].__get__(a,A)。这是一个bound method.,就是绑定了实例的方法。这要说几个概念,unbound method,function,bound method。

            当一个函数(function)定义在了class语句的块中(或者由 type 来创建的), 它会转成一个 unbound method , 当我们通过一个类的实例来 访问这个函数的时候,它就转成了 bound method , bound method 会自动把这个实例作为函数的地一个参数。

            所以, bound method 就是绑定了一个实例的方法, 否则叫做 unbound method .它们都是方法(method), 是出现在 class 中的函数。

      3.类的__dict__并不是真正意义的dict,而是一个dictproxy。它是只读的,比如,你想做:

a.__class__.__dict__['s'] = 1


TypeError: 'dictproxy' object does not support item assignment

 

 

说到属性访问,要提一下Python的描述符:property,经过property装饰的方法就可以通过属性的方式访问。比如我定义了一个

class A(object):
   
   @property
   def test(self,x):
     pass

就可以通过A().test访问,此外,我们也可以通过property,.setter以及property.getter等方法来实现一些定制的读写属性。比如:

class A(object):

   @property
   def score(self):
      return self._score

   @score.setter
   def score(self,value):
     if value>100:
        raise ValueError('int must be lower than 100')
     self._score = value


a = A()
a.score = 101

系统会抛出ValueError的异常。

学过Java的都知道Java规定了public,private等属性,但在Python中是没有的,Python也可以分出不同类别的属性,比如可以用双下划线来表示,但这只是规定了一种规则,却不是强制限制,一切凭自觉。嗯,Python开发者都是素质高的一类人,哈哈。

不过,你非要写成这种形式,可以写一些代码实现:

class PrivateExc(Exception):
	pass


class A(object):

	def __setattr__(self, key, value):
		if key not in self.privates:
			raise PrivateExc('{0} not in {1} '.format(key,self))
		else:
			self.__dict__[key] = value

class B(A):
	privates = ['age']

	def __init__(self,age):
		self.age = age

	def __getattr__(self, item):
		return item

b = B(12)
b.age = 30
print b.age
b.name = 'h'

30
Traceback (most recent call last):
  File "D:/Study/Programming/Python/myworks/weiborobot/__init__.py", line 32, in <module>
    b.name = 'h'
  File "D:/Study/Programming/Python/myworks/weiborobot/__init__.py", line 15, in __setattr__
    raise PrivateExc('{0} not in {1} '.format(key,self))
__main__.PrivateExc: name not in <__main__.B object at 0x00000000025A7780> 

更多关于描述符的可参考我的另外一篇文章:Python描述符

2、继承

在Python中,继承分为单继承和多重继承(Java只有单继承),单继承比较好理解,单纯地继承父类的属性和方法。 这里只说一下多重继承。有的时候我们定义一个子类,并不是希望定义非常复杂的父类进行单继承来实现多种功能,Python支持 多继承,用的最多的就是Minxin,这在Django中也经常用到。即我希望可以实现多种功能。比如一个视图类CreateView,我既希望继承父类View的方法,又想继承表单的一些功能,那:

class BaseCreateView(ModelFormMixin, ProcessFormView):

    pass

 

多重继承主要考虑的问题是假如一个子类的父类有两个,如果两个父类都有相同的方法,那到底该继承哪一个父类的方法呢?这就涉及到了方法的查找顺序。

对于你定义的每一个类,Python会计算出一个所谓的方法解析顺序(MRO)列表。 当你使用 super() 函数时,Python会在MRO列表上继续搜索下一个类。 只要每个重定义的方法统一使用 super() 并只调用它一次, 那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次 。 这个MRO列表就是一个简单的所有基类的线性顺序表。为了实现继承,Python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。

不同形式的类的多重继承方法也不同,类主要分为经典类和新式类。新式类指的是继承自object的类,其他的就叫做经典类。 经典类使用的继承查找顺序是深度优先,新式类使用的继承方法是C3广度优先。 深度优先和广度优先是算法中两个比较重要的搜索算法。

 

现在看经典类的例子:

class A:
    def output(self):
        print 'A'


class B(A):
    pass

class C(A):
    def output(self):
        print 'C'


class D(B,C):
    pass


d = D()
d.output()

输出是A

这是因为它的查找顺序是D->B->A->C,完全符合深度优先。画一下树形图

     A

B        C

    D

那么其实到这儿是有问题的,对于子类D,我希望它继承C的output,但是它最后是继承了A的output方法。对于这种问题,就产生了新式类。

新式类的写法主要是要继承object.

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


class B(A):
    pass

class C(A):
    def output(self):
        print 'C'


class D(B,C):
    pass


d = D()
d.output()

输出C

        这是因为它使用C3广度优先算法,它的查找顺序是D->B->C->A。它完美解决了经典类遇到的问题。

        既然说到了新式类,那新式类除了继承带来的优势,还有什么新的特性呢?

  1. MRO 该表 
  2.   __slots__等新属性;
  3. super;
  4. descriptor;

(1)MRO表

上面已经说过了,即关于多重继承时候,搜索过程。

(2)__slots__等新属性;

    在新式类中加入了很多的新特性。你定义完一个新式类后,打印属性,会有如下这些:

['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'output']

        __getattribute__就是新式类的一种用法,要比__getattr__更灵活。       

         当我们在新式类中定义了__slots__后, Python就会为实例使用一种更加紧凑的内部表示。 实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似。比如,假设你不使用slots直接存储一个Date实例, 在64位的Python上面要占用428字节,而如果使用了slots,内存占用下降到156字节。 如果程序中需要同时创建大量的日期实例,那么这个就能极大的减小内存使用量了。

目前存在的最大误区是__slots__可作为封装工具防止添加新属性,虽然它的确可实现这个目的,但这不是__slots的初衷,它就是用来优化数据结构的,不过我们还是拿个例子来显示说一下用__slots__后会产生哪些效果:

class Haibo(object):
    __slots__ = ('name','height')

d = D()
d.age = 23
print d.age

        我们对Haibo这个类,只限定它拥有name和height两个属性,当我们试着要给实例添加age属性时,就会抛出ArributeError异常.这隐士地表明了,定义__slots__之后,就不具有__dict__特性了。

        __slots__很有用,它能够为程序节省很多的内存消耗,尤其是对于那些只需要简单数据结构的类。但也不要轻易就用它,它会使类失去很多功能,比如多重继承。

(3)super

        super在我们编写代码时,做类继承时用得非常多,可以说遍地都是,这也证明了它给带来的优势。

          一般当写子类时,我们要重构父类的方法,并也希望继承父类的方法,就要用super,用了super,减少了代码的编写量,当然你也可以说,不用super,直接显示引用父类的方法,这也可以,但还是遇到问题,加入你再两个子类中显示调用父类A的__init__。那D中会调用两次A的__init__,而用super就不会出现那种情况。

此外,super最大最大的优势是在多重方面的应用。它为实例提供了不同的mro策略。

  简单例子:

class A(object):

   def __init__(self):
       print 'A'

class B(A):

    def __init__(self):
        print 'B'
        super(B,self).__init__()

b = B()

 super其实是一个类,不是函数。 super 包含了两个非常重要的信息: 一个 MRO 以及 MRO 中的一个类。

def super(cls, inst):
    mro = inst.__class__.mro()
    return mro[mro.index(cls) + 1]

两个参数 cls 和 inst 分别做了两件事:
1. inst 负责生成 MRO 的 list
2. 通过 cls 定位当前 MRO 中的 index, 并返回 mro[index + 1]

当我们使用了super类的时候,就会向mro表中写入。还是拿个例子:

class A(object):

   def __init__(self):
       print 'A'


class B(A):

    def __init__(self):
        print 'B'
        super(B,self).__init__()


class C(A):

    def __init__(self):
        print 'C'
        super(C,self).__init__()

class D(B,C):
    pass

d = D()
print d.__class__.__mro__

输出:

B
C
A
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)

搜索原则:

  • 子类会先于父类被检查
  • 多个父类会根据它们在列表中的顺序被检查
  • 如果对下一个类存在两个合法的选择,选择第一个父类

(4)描述符

   我自己测试的时候发现旧式类也能用啊, 把我整懵逼了。

3、多态

多态即多种形态,在编译阶段无法确定类型,在运行时确定状态。Python不像Java那样需要声明变量类型,需要在运行时确定变量的类型,其实这就是多态的体现。

多态本质上是对不同的对象可以实现相同的操作,通常是为了接口重用,即不同的类实现相同的方法。(运算符重载其实也是一种多态。)

上面的例子中其实就是一种多态,子类进行了运算符重载,不同的子类实现了相同的方法output,但输出的结果是不一样的,这就是多态。 Python中引用了“鸭子类型”,它就是一种多态,什么是鸭子类型? “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。” 我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。 比如一些像文件的对象,如StringIO,可以像文件一样操作,使用文件的方法。

例子:

class Duck:
    def quack(self):
        print 'gua gua'

class Bird:
    def quack(self):
        print 'bird is like bird'


def speak(obj):
    obj.quack()


obj1 = Duck()
obj2 = Bird()


speak(obj1)
speak(obj2)

speak函数输入了两个不同的参数,他们输出的结果是不同的,这就是多态。

类的方法:

1、普通方法
普通方法很好理解,大多数都是普通方法,即第一个参数是类本身,即self.

2、类方法
classmethod装饰的方法,传入的第一个参数是cls

3、静态方法
staticmethod装饰的方法,不需要传入参数。

class Animal():

    def mth1(self):
        print 'normal method'

    @classmethod
    def mth2(cls):
        print 'class method'

    @staticmethod
    def mth3():
        print 'static method'


a = Animal()
a.mth1()
Animal.mth2()
Animal.mth3()
a.mth2()
a.mth3()

 

 

 

 

 

--------EOF---------
微信分享/微信扫码阅读