python面向对象编程

什么是面向对象编程(OOP)?

面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范例,它将属性与行为绑定在独立的单个对象上,提供了一种结构化程序的方法。

OOP的设计思想来自自然,自然界中的对象如学生都有自己的属性与行为,学生是一个抽象的概念(类,class),而实例(Instance)则是一个个具体的学生。

另外,Python也支持面向过程的编程,面向过程的编程把计算机程序视为一系列的命令集合,即一组函数的顺序执行。面向过程编程通常将一个复杂的函数继续切分成多个小函数,以降低程序的复杂度。

注意:Python支持多种编程范例,面对不同的问题,可选择一个或多个编程范例进行编程。

比较其他的编程语言,python的类机制添加了类最低的新语法与语义。它是c++类与Moudula-3类机制的混合。Python类提供了所有的基于对象编程(Object Oriented Programming )的标准特性:类的继承机制允许继承多个基类(父类),子类(a derived class)可以覆盖任何基类的方法,子类中的方法也可以调用基类的同名方法。对象可以包含任何数量与类型的数据对于模块,类带有Python的动态属性(as is true for modules, classes partake of the dynamic nature of Python):他们可以被动态创建,并且创建后可以被进一步修改。

使用C++术语, Python中通常的类成员(包括数据成员)都是公开的(除了下面将介绍的私有变量),所有的成员函数都是虚拟的。正如Modula-3语言那样,there are no shorthands for referencing the object’s members from its methods:类的方法函数被声明时需要显示声明第一个参数,其代表方法函数所在对象。方法函数被调用时将隐式提供对象信息。正如SmallTalk语言那样,类本身也是对象。这为导入与重命名(Importing and renaming)提供了语义。与C++与Modula-3语言不同的是,用户可以将Python中内建的类型作为基类进行扩展。与C++相同的是,大多是内建的操作可以通过特殊语法被重新定义。

(因为缺乏普遍被接受的描述类的术语,我将偶尔使用SmallTalk与C++中的术语。当Modula-3中面向对象的语义比C++更接近Python时,我也会使用Modula-3语言中的术语, 但是可能很少有人听说过它。)

关于名称与对象的讨论

Python中一切皆对象,对象具有个别性,多个名字可以绑定到同一个对象上。这就是其他语言中所谓的别名(alias)。在处理不变的基本类型时(数值,字符串,元组),这一点可以安全的被忽略。但是在涉及可变对象(lists,dictionary)时,别名可以起到惊人的效果,其在某些方面表现的像指针

Python命名空间与作用域

类的定义用命名空间玩了一些巧妙的把戏,为了完全理解发生了什么,就必须理解命名空间与作用域是怎么工作的。

命名空间是从名称到对象的映射。大多数的命名空间都通过字典实现(但这点并不明显,而且在将来可能发生改变。)

命名空间是在不同的时刻被创建,有着不同的生命周期。如:

  • 内置名称的命名空间是在Python解析器启动时创建的,而且它们永远不被删除(内置名称存在于一个叫 builtins的模块中。);
  • 一个模块的全局命名空间在模块的定义被读取时被创建,一直持续到解析器退出;
  • 被最高级别的解释器调用的语句, 不论是从脚本还是从交互读取的, 都被认为是一个名叫 main 的模块的一部分, 所以它们有自己的全局命名空间;
  • 函数的局部命名空间在函数调用时被创建,在函数返回时或者发生异常而终止时被删除。当然,递归调用会有它自己的局部命名空间。

作用域只是一个结构上的区域,在这里命名空间可以直接访问。“直接访问”就意味者无须特殊的指明引用。

作用域是静态的,但其使用却是全局的。
在执行的任何时刻,作用域的搜索顺序如下:

  • 最内层的作用域,包含局部变量名;
  • 任意函数的作用域,包括非局部的与非全局的名称;
  • 紧邻最后的作用域,包括了当前模块的全局变量;
  • 最外层的作用域,包含内置名字的命名空间;

Python中赋值总是会使用最里层作用域的值,赋值并没有拷贝数据,其仅仅是绑定名称到对象上。
global语句可以将全局作用域的名字绑定到当前作用域,从而使赋值作用到全局变量,否则赋值将作用或产生新的局部变量。
nonlocal语句将一个闭合作用域的名字绑定到当前作用域。

例如:

def scope_test():
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

输出的结果是:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

可见,局部作用域的赋值并没有改变闭合作用域scope_test中变量spam的值,nonlocal将闭合域的名称spam绑定到了当前函数中,global则将模块级的全局作用域名称绑定到了当前作用域中。

小结
理解python的命名空间需要掌握三层规则:
第一,赋值(包括显式赋值和隐式赋值)产生标识符,赋值的地点决定标识符所处的命名空间。
第二,函数定义(包括def和lambda)产生新的命名空间。
第三,python搜索一个标识符的顺序是“LEGB”

global关键字用来在函数或者其他局部作用域中使用全局变量。但是如果不修改全局变量也可以不使用global关键字。
nonlocal关键字用来在函数或者其他作用域中使用外层(非全局)变量

类定义的语法

类的定义与函数的定义一样,必须在使用前执行。定义的语法如下:

class ClassName:
    
    
    
    ......

当解析器进入一个类定义时,新的命名空间就被创建;
当离开一个类定义时后,一个class object就被创建。

类对象

类对象支持两种操作:属性引用和实例化。

例如,如下类定义:

class Myclass:
    """A simple example class"""
    i = 123456
    def f(self):
        return 'hello world'

Myclass.i 和 Myclass.f都是合法的属性引用。
类属性也可以被指定,可以给Myclass.i赋值以改变其数值。
doc也是一个合法的属性,表示Myclass的文档字符串。

类的实例化采用函数的形式:

x = Myclass()

实例化的操作创建了一个空对象。在创建实例的过程中,很多类可能都需要有特定的初始状态。
所以一个类可以定义一个特定的方法,称为__init__,如:

def __init__(self, realpart, imagpart):
    self.r = realpart
    self.i = imagpart

实例化如下,=:

x = Myclass(3.0, -4.5)
x.r, x.i

方法对象

通常一个方法在绑定后就可以使用,

x.f()

你可能注意到 x.f() 调用时并没有参数, 尽管 f() 定义时是有一个参数的. 那么这个参数怎么了? 当然, Python 在一个参数缺少时调用一个函数是会发生异常的 — 就算这个参数没有真正用到…

事实上, 你会猜想到: 关于方法, 特殊的东西就是, 对象作为参数传递给了函数的第一个参数. 在我们的例子中, x.f() 是严格等价于 MyClass.f(x). 在多数情况下, 调用一个方法 (有个 n 个参数), 和调用相应的函数 (也有那 n 个参数, 但是再额外加入一个使用该方法的对象), 是等价的.

方法对象的创建过程如下:
当一个实例属性被引用时, 但是不是数据属性, 那么它的类将被搜索. 如果该名字代表一个合法的类属性并且是一个函数对象, 一个方法对象就会被创建, 通过包装 (指向) 实例对象, 而函数对象仍然只是在抽象的对象中: 这就是方法对象. 当方法对象用一个参数列表调用, 新的参数列表会从实例对象中重新构建, 然后函数对象则调用新的参数列表.

方法可以通过使用 self 参数调用其他的方法:

class Bag:
    def __init__(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)

方法可以引用全局变量, 就像普通函数中那样. 与这个方法相关的全局作用域, 是包含那个类定义的模块。

类与实例变量

类变量被类的所有实例共享,实例变量为每个实例独有。
Python中实例变量的解析顺序为:instance-> class-> base classes as determined by the MRO (method resolution order)

类变量

  • 类变量定义:在类的内部,类方法的外部定义。
  • 访问方式:在类的内部或外部通过类名.类变量名的方式访问。注意,根据Python中实例变量的解析顺序,若实例中无同名实例变量,通过self.类变量名的方式也可以访问类变量,但不同通过此方法给类变量赋值,否则将产生同名实例变量。

实例变量

实例变量是绑定在特定实例空间的变量,在类的定义时使用self进行绑定,在类的外部,使用实例名绑定。

  • 实例变量定义:在类中使用self.实例变量方式定义,或在类的外部使用实例名.实例变量方式定义。
  • 访问或赋值:同上。

例如,

>>>
>>> class Dog:
...     kind = 'canine'    # 类变量,被所有实例共享
...     def __init__(self, name):
...         self.name = name    # 实例变量,每个实例独有
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind
'canine'
>>> e.kind
'canine'
>>> d.kind = 'water'
>>> d.kind
'water'
>>> e.kind
'canine'
>>> d.name
'Fido'
>>> e.name
'Buddy'

另外,使用可变变量作为类变量,可能产生非类变量的特性

class Dog:

    tricks = []             # mistaken use of a class variable

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

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

正确的设计是使用实例变量代替

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

小结

  • 类变量与实例变量区别(The difference between Class Variable and Instances Variable)
  • 类变量被所有实例共享,无论一个类有多少实例,其只占用一份内存空间;
  • 实例变量被每个实例独有,与实例名绑定在一起,同一类的不同实例占用不同内存空间;
  • 类变量与实例变量与局部变量一样,都不需要声明,而是在赋值时刻产生.
  • 不要尝试使用实例引用对类变量进行赋值操作,这个赋值操作将无法作用于类变量,Python会在你进行赋值操作时产生一个同名实例变量。
>>> class Cat:
 ...     trick = "Class Variable"
 ...     def __init__(self, name):
 ...         self.name = name
...
>>> a = Cat('Water')
>>> a.__dict__
{'name': 'Water'}
>>> a.trick = "Instance Variable"
>>> a.__dict__
{'trick': 'Instance Variable', 'name': 'Water'}
>>> Cat.__dict__
{'trick': 'Class Variable', '__module__': '__main__', '__doc__': N one, '__init__': }

继承

派生类的定义如下:

class DerivedClassName(BaseClassName):
    
    .
    .
    .
    

BaseClassName的定义对于派生类必须可见,在基类的地方,任意的表达式都是允许的。如:

class DerivedClassName(modname.BaseClassName)

  • 派生类会覆盖基类的方法。 在派生类中覆写基类方法后,基类方法被覆盖。若要执行基类方法,需显式调用。调用基类的方式有两种:(1)若使用父类名调用,需要显式添加self参数;(2)若使用super(cls, inst)调用,则不需要在方法中添加self参数。
  • 当一个属性在这个类中没有被找到, 那么就会去基类中寻找. 若有多个基类,则查找顺序称为方法解析顺序(Method Resolution Order, MRO)

Python有两个内置函数用于继承:

  • 使用isinstance()检查实例的类型,如isinstance(obj, int),返回True或False
  • 使用issubclass(son, parent)检查son是否为parent的子类,如issubclass(bool, int)会返回True,因为bool是int的派生类。

多重继承

class DerivedClassName(Base1, Base2, Base3):
    
    .
    .
    .
    

经典类

Python2.2以前的版本只支持经典类,派生类MRO采用深度优先算法,存在两种情况,如下图所示:

  • 正常情况下:经典类的正常方法解析顺序采用深度优先,即A->B->D->C->E
  • 当存在菱形继承时:仍采用深度优先,但去掉重复出现的解析,即A->B->D->C。注意:使用深度优先算法,在菱形继承问题时,共用父类的部分基类的覆盖方法无法被访问到,如C类中若重写了D中的方法,但父类D的MRO顺序在C的前面,C类中的覆写方法将不会被MRO访问,导致C只能被继承,无法被重写

新式类

Python2.2以后显示的继承object的类为新式类,Python3中所有的类都是新式类。,新式类中的MRO用分两种情况:

  • Python2.2版本:新式类MRO使用广度优先算法
  • Python2.3及以后版本:新式类MRO采用C3算法

广度优先算法

广度优先算法解决了菱形继承时部分基类方法无法重写问题,但存在单调性问题,因此只在Python2.2版本中使用过。所谓单调性是指:方法解析顺序应该按照基类到基类的父类依次进行。

广度优先算法也分两种情况:

  • 正常情况下:采用广度优先的新式类算法的解析顺序是A->B->C->D->E
  • 当派生类存在菱形继承时:其方法解析顺序为A->B->C->D

C3算法

可以模拟拓扑排序,按照拓扑顺序减掉度为0的类节点
例如,如下继承关系:

class D(object):
    pass

class E(object):
    pass

class F(object):
    pass

class C(D, F):
    pass

class B(E, D):
    pass

class A(B, C):
    pass

if __name__ == '__main__':
    print A.__mro__

那么模拟一下例子的拓扑排序:首先找入度为0的点,只有一个A,把A拿出来,把A相关的边剪掉,再找下一个入度为0的点,有两个点(B,C),取最左原则,拿B,这是排序是AB,然后剪B相关的边,这时候入度为0的点有E和C,取最左。这时候排序为ABE,接着剪E相关的边,这时只有一个点入度为0,那就是C,取C,顺序为ABEC。剪C的边得到两个入度为0的点(DF),取最左D,顺序为ABECD,然后剪D相关的边,那么下一个入度为0的就是F,然后是object。那么最后的排序就为ABECDFobject。

MRO函数super

其中MRO是方法解析顺序(Method Resolution Order的缩写),super(cls, inst)只能在新式类中调用。新式类中,若不覆盖定义基类同名方法,将按照MRO顺序调用基类方法;若覆盖定义基类同名方法,则需要显示调用基类同名方法。

不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!
不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!
不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!

super主要干如下事情:

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

解释如下:

  • 根据实例inst继承关系生成mro列表
  • 找到派生类cls在MRO列表中的索引,并返回此索引位置的下一个类

例1,

class Root(object):
    def __init__(self):
        print("This is Root")
class B(Root):
    def __init__(self):
        print("Enter B")
        super(B, self).__init__()
        print("Leave B")

class C(root):
    print("Enter C")
    super(C, self).__init__()
    print("Leave C")

class D(B, C):
    pass

if __name__ == '__main__':
    d = D()
    print(d.__class__.mro())

输出结果如下:

Enter B
Enter C
This is Root
Leave C
Leave B
(, , , , )

注意,若将D类中的__init__()函数写上,如例2:

class D(B, C):
    def __init__(self):
        print("Enter D")
        print("Leave D")

则,输出结果如下:

Enter D
Leave D
(, , , , )

若在D类的__init__()函数中显示调用super,如例3:

class D(B, C):
    def __init__(self):
        print("Enter D")
        super(D, self).__init__()
        print("Leave D")

则,输出结果如下:

Enter D
Enter B
Enter C
This is Root
Leave C
Leave B
Leave D
(, , , , )

参考

私有变量

注意:Python不存在私有变量,但是大多数Python代码都遵循一个约定:带有下划线的名称前缀,包括函数、方法或数据成员,应该被视为API的非公开部分。

下划线的用法总结如下:

  • 以单下划线开头的名称,为保护成员
  • 以单下划线开头的类成员,类与子类对象与其实例对象可以访问
  • 以单下划线开头的模块变量与函数,使用from module import *时,不会被获取,但使用import module可以被获取
  • 以单下划线结尾,是为了防止该名称与Python关键字冲突
  • 以双下划线开头,表示为私有成员,只允许类内部使用,子类不能访问,无法使用.引用。使用dir()可以看到其被Python转换为_classname__name形式
  • 以双下划线开头,并以双下划线结尾的变量,是一些 Python 的“魔术”对象,表示这是一个特殊成员。例如:定义类的时候,若是添加init方法,那么在创建类的实例的时候,实例会自动调用这个方法,一般用来对实例的属性进行初使化,Python不建议将自己命名的方法写为这种形式。

非公共名称以单下划线开头。但是,如果你清楚你的代码会涉及到子类, 并且有些内部属性应该在子类中隐藏起来,那么才考虑使用双下划线方案。

迭代器 (Iterators)

大多数的容器对象可以通过in方式循环。

for e in [1, 2, 3]
    print(e)
for e in (1, 2 , 3):
    print e
for key in {'one':1, 'two':2}:
    print(key)
for line in open('myfile.txt'):
    print(line, end='')

这种形式的访问足够简洁大方,for声明调用容器对象的iter函数,该函数返回一个定义了__next__方法的迭代对象。该方法一次访问一个元素。
当容器的所有元素都访问完成时,__next__()引发StopIteration异常,例如:

>>> s = 'abc'
>>> it = iter(s)
>>> it

>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)

Traceback (most recent call last):
  File "", line 1, in 
    next(it)
StopIteration
>>>

明白了如上迭代协议后,添加迭代行为到类中非常简单:

  • 定义__iter__方法,该方法返回一个带__next__()方法的对象
  • 定义__next__()方法,该方法一次返回一个数据。

例如,

class Reverse:
    """Iterator for looping over a sequence backwards"""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index  - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

生成器(Generators)

[Generators]() are a simple and powerful tool for creating iterators. They are written like regular functions but use the [yield]() statement whenever they want to return data. Each time [next()]() is called on it, the generator resumes where it left off (it remembers all the data values ans which statement was last executed). An example shows that generators can be trivially easy to createj:
生成器是创建迭代器的简单而强大的工具
编写生成器与编写常规的函数没有什么区别,只是在返回数据的时候不使用return而使用yield
每次next()被调用时,生成器从上次离开的地方恢复运行,直到再次遇到yield

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

注意:

  • 所有能使用生成器做的工作都可以通过基于类的迭代器完成。生成器比迭代器更加紧凑,是因为__iter__()方法与__next__()方法被自动创建。
  • 在生成器中,每次调用过程中的本地变量与执行状态被自动保存。 这使得其比迭代器使用实例变量self.index self.data等更加简洁。

生成器表达式(Generator Expressions)

Some simple generators can be coded succinctly as expressions using a syntax similar to list comprehensions but with parentheses instead of brackets. These expressions are designed for situations where the genrator is used right away by an enclosing function. Generator expressions are more compact but less versatile than full generator definitions and tend to be more memory friendly than equivalent list comprehensions.
一些简单的生成器可以通过类似列表解析器方式创建。但,与列表使用中括号[]不同,生成器使用小括号()
例如:

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}

>>> unique_words = set(word  for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

参考

Was this helpful?

0 / 0

发表回复 0