Python3简明教程之7面向对象

Python3之面向对象编程

面向对象技术简介

Python从设计之初就已经是一门面向对象的语言,

Python中创建一个类和对象是很容易的。

Python 在尽可能不增加新的语法和语义的情况下加入了类机制。

 

Python中的类提供了面向对象编程的所有基本功能:

类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,

方法中可以调用基类中的同名方法。

 

 

类(Class):

用来描述具有相同的属性和方法的对象的集合。

它定义了该集合中每个对象所共有的属性和方法。

对象是类的实例。

类变量:

类变量在整个实例化的对象中是公用的。

类变量定义在类中且在函数体之外

类变量通常不作为实例变量使用。

数据成员:

类变量或者实例变量用于处理类及其实例对象的相关的数据。

方法重写:

如果从父类继承的方法不能满足子类的需求,

可以对其进行改写,

这个过程叫方法的覆盖(override),也称为方法的重写

实例变量:

定义在方法中的变量,只作用于当前实例的类。

继承:

即一个子类(derived class)继承父类(base class)的字段和方法。

继承也允许把一个子类的对象作为一个基类对象对待。

实例化:

创建一个类的实例,类的具体对象。

方法:

类中定义的函数。

对象:

通过类定义的数据结构实例。

对象包括两个数据成员(类变量和实例变量)和方法。

对象可以包含任意数量和类型的数据。

 

类和实例

类定义的语法

类定义的语法格式如下:

 

class ClassName:

    <statement-1>

    .

    .

    .

    <statement-N>

类实例化后,可以使用其属性,

创建一个类之后,可以通过类名访问其属性。

 

 

面向对象最重要的概念就是类(Class)和实例(Instance),

必须牢记抽象的模板

实例是根据类创建出来的一个个具体的“对象”,

每个对象都拥有相同的方法,但各自的数据可能不同。

定义类

 

以Student类为例,定义类是通过class关键字:

class Student(object):

    pass

class后面紧接着是类名,即Student,

类名通常是大写开头的单词,

紧接着是(object),表示该类是从哪个类继承下来的,

object类:所有类最终都会继承这个类,它是python的根类。

 

创建类的实例

定义好了Student类,就可以根据Student类创建出Student的实例,

创建实例是通过“类名()”实现的:

>>>kart = Student()

>>>kart

<__main__.Student object at 0x0000029FCCA054A8>

>>> Student

<class '__main__.Student'>

可以看到,变量kart指向的就是一个Student的实例,

后面的0x0000029FCCA054A8是内存地址,

每个实例的地址都不一样,

而Student本身则是一个类。

 

属性

  1. 给实例变量添加属性

可以自由地给一个实例变量绑定属性,

比如,给实例kart绑定一个name属性:

>>> kart.name = 'hello,apple'

 

  1. 给类本身添加属性

由于类可以起到模板的作用,

因此,可以在创建实例的时候,

把一些我们认为必须绑定的属性强制填写进去。

通过定义一个特殊的__init__方法构造函数),

在创建实例的时候,就把name,score等属性绑上去:

 

class Student(object):

    def  __init__(self, name, score):

        self.name = name

        self.score = score

注意:特殊方法“__init__”前后分别有两个下划线!!!

注意到__init__方法的第一个参数永远是self,表示创建的实例本身,

在__init__方法内部,可以把各种属性绑定到self,

因为self就指向刚才创建的那个实例。

 

  1. 创建类的实例(带参数)

有了__init__方法,在创建实例的时候,就不能传入空的参数了,

必须传入与__init__方法匹配的参数,

但self不需要传,Python解释器自己会把实例变量传进去:

>>> kart = Student('apple', 60)

>>> kart.name

'apple'

注意,类的方法和普通函数:

在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,

并且,调用时,不用传递该参数。

除此之外,类的方法和普通函数没有什么区别,

所以,仍然可以用默认参数、可变参数、关键字参数和命名关键字参数

 

数据封装

面向对象编程的一个重要特点就是数据封装。

在上面的Student类中,每个实例就拥有各自的name和score这些数据。

可以通过普通函数来访问这些数据,比如打印一个学生的成绩。

 

但是,既然Student实例本身就拥有这些数据,

要访问这些数据,就没有必要从外面的函数去访问

可以直接在Student类的内部定义访问数据的函数,

这样,就把“数据”给封装起来了。

这些封装数据的函数是和Student类本身是关联起来的,

我们称之为类的方法

class Student(object):

    def __init__(self, name, score):

        self.name = name

        self.score = score

 

    def print_score(self): # 这个是类的方法

        print('%s: %f' % (self.name, self.score))

要定义类的一个方法,

除了第一个参数是self外,其他和普通函数一样。

要调用一个方法,只需要在实例变量上直接调用,

除了self不用传递,其他参数正常传入。

>>> kart.print_score()

apple: 60

 

从外部看Student类,创建实例需要给出name和score,

而如何打印,都是在Student类的内部定义的,

这些数据和逻辑被“封装”起来了,

调用很容易,但却不用知道内部实现的细节。

 

小结

类是创建实例的模板,

而实例则是一个一个具体的对象,

各个实例拥有的数据都互相独立,互不影响;

 

和静态语言不同,Python允许对实例变量绑定任何数据

也就是说,对于两个实例变量,

虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。

 

 

 

访问限制

属性名前加双下划线

在Class内部,可以有属性和方法,

而外部代码可以通过直接调用实例变量的方法来操作数据,

这样,就隐藏了内部的复杂逻辑

 

但是,从前面Student类的定义来看,

外部代码还是可以自由地修改一个实例的name、score属性

>>>kart = Student('apple', 60)

>>>kart.score

60

>>>kart.score = 99

>>>kart.score

99

如果要让内部属性不被外部访问

可以把属性的名称前加上两个下划线__

在Python中,实例的变量名如果以__开头,

就变成了一个私有变量(private)

只有内部可以访问,外部不能访问。

 

class Student(object):

    def __init__(self, name, score):

        self.__name = name

        self.__score = score

 

    def print_score(self):

        print('%s: %d' % (self.__name, self.__score))

改完后,对于外部代码来说,没什么变动,

但是已经无法从外部访问实例变量.__name和实例变量.__score了:

 

>>>kart = Student('apple',  60)

>>>kart.__name

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'Student' object has no attribute '__name'

这样就确保了外部代码不能随意修改对象内部的状态

这样通过访问限制的保护,代码更加健壮。

Get方法

但是如果外部代码要获取name和score怎么办?

可以给Student类增加get_name和get_score这样的方法:

class Student(object):

    ...

 

    def get_name(self):

        return self.__name

 

    def get_score(self):

        return self.__score

Set方法

如果又要允许外部代码修改score怎么办?

可以再给Student类增加set_score方法:

class Student(object):

    ...

    def set_score(self, score):

        self.__score = score

 

双下划线开头,双下划线结尾

在Python中,变量名类似__xxx__的,

也就是以双下划线开头,并且以双下划线结尾的,

特殊变量,特殊变量是可以直接访问的,不是private变量

所以,不能用__name__、__score__这样的变量名。

一个下划线开头

有些时候,会看到以一个下划线开头的实例变量名,比如_name,

这样的实例变量外部是可以访问的,

但是,按照约定俗成的规定,当看到这样的变量时,意思就是,

 “虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

 

强制给__name赋值

注意下面的这种写法:

>>>kart = Student('apple',  60)

>>>kart.get_name()

'apple'

>>>bart.__name = 'New Name' # 设置__name变量!

>>>bart.__name

'New Name'

表面上看,外部代码“成功”地设置了__name变量,

但实际上这个__name变量和class内部的__name变量不是一个变量!

内部的__name变量已经被Python解释器自动改成了_Student__name

而外部代码给bart实例新增了一个__name属性。

 

 

继承和多态

 

继承的概念

在OOP程序设计中,

当我们定义一个class的时候,可以从某个现有的class继承,

新的class称为子类(Subclass)

而被继承的class称为基类、父类或超类(Base class、Super class)。

 

比如,我们已经编写了一个名为Animal的class,

有一个run()方法可以直接打印:

class Animal(object):

    def run(self):

        print('Animal is running...')

 

当我们需要编写Dog和Cat类时,

就可以直接从Animal类继承:

class Dog(Animal):

    pass

 

class Cat(Animal):

    pass

对于Dog来说,Animal就是它的父类,

对于Animal来说,Dog就是它的子类。

 

继承有什么好处?

最大的好处是子类获得了父类的全部功能:

由于Animial实现了run()方法,

因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:

dog = Dog()

dog.run()

 

cat = Cat()

cat.run()

运行结果如下:

Animal is running...

Animal is running...

 

可以对子类增加一些方法,

比如Dog类:

class Dog(Animal):

    def run(self):

        print('Dog is running...')

 

    def eat(self):

        print('Eating meat...')

 

继承的第二个好处需要我们对代码做一点改进。

你看到了,无论是Dog还是Cat,

它们run()的时候,显示的都是Animal is running...,

符合逻辑的做法是分别显示Dog is running...和Cat is running...,

因此,对Dog和Cat类改进如下:

 

class Dog(Animal):

    def run(self): #覆盖父类的同名方法

        print('Dog is running...')

 

class Cat(Animal):

    def run(self): #覆盖父类的同名方法

        print('Cat is running...')

再次运行,结果如下:

Dog is running...

Cat is running...

 

当子类和父类都存在相同的run()方法时,

子类的run()覆盖了父类的run(),

在代码运行的时候,总是会调用子类的run()。

这样,我们就获得了继承的另一个好处:多态

多态

要理解什么是多态,首先要对数据类型再作一点说明。

当定义一个class的时候,实际上就定义了一种数据类型

定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

 

a = list() # a是list类型

b = Animal() # b是Animal类型

c = Dog() # c是Dog类型

判断一个变量是否是某个类型可以用isinstance()判断:

>>> isinstance(a, list)

True

>>> isinstance(b, Animal)

True

>>> isinstance(c, Dog)

True

看来a、b、c确实对应着list、Animal、Dog这3种类型。

>>> isinstance(c, Animal)

True

看来c不仅仅是Dog,c还是Animal!

因为Dog是从Animal继承下来的,当创建了一个Dog的实例c时,

认为c的数据类型是Dog没错,但c同时也是Animal也没错,

Dog本来就是Animal的一种

 

在继承关系中,

如果一个实例的数据类型是某个子类,

那它的数据类型也可以被看做是父类。

但是,反过来就不行:

>>> b = Animal()

>>> isinstance(b, Dog)

False

Dog可以看成Animal,但Animal不可以看成Dog。

 

 

要理解多态的好处,还需要再编写一个函数,

这个函数接受一个Animal类型的变量:

 

def run_once(animal):

    animal.run()

当我们传入Animal的实例时,run_ once ()就打印出:

>>> run_once (Animal())

Animal is running...

 

当我们传入Dog的实例时,run_ once ()就打印出:

>>> run_once (Dog())

Dog is running...

当我们传入Cat的实例时,run_ once ()就打印出:

 

>>> run_once (Cat())

Cat is running...

 

再定义一个Tiger类型,也从Animal派生:

 

class Tiger(Animal):

    def run(self):

        print(Tiger is running...')

当我们调用run_once()时,传入Tiger的实例:

 

>>> run_once(Tiger ())

Tiger is running...

新增一个Animal的子类,不必对run_once()做任何修改,

实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,

原因就在于多态

 

多态的好处就是,

当需要传入Dog、Cat、Tiger……时,

只需要接收Animal类型就可以了,

因为Dog、Cat、Tortoise……都是Animal类型,

然后,按照Animal类型进行操作即可。

由于Animal类型有run()方法,

因此,传入的任意类型,只要是Animal类或者子类,

就会自动调用实际子类的run()方法

 

对于一个变量,只需要知道它是Animal类型,

无需确切地知道它的子类型,就可以放心地调用run()方法,

而具体调用的run()方法是作用在Animal、Dog、Cat还是Tiger对象上,

由运行时该对象的确切类型决定

这就是多态真正的威力:

调用方只管调用,不管细节,

而当新增一种Animal的子类时,

只要确保run()方法编写正确,不用管原来的代码是如何调用的。

这就是著名的“开闭”原则【对对扩展开放,对修改封闭】:

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_once()等函数。

 

Python继承关系树

继承还可以一级一级地继承下来,

就好比从爷爷到爸爸、再到儿子这样的关系。

而任何类,最终都可以追溯到根类object,

这些继承关系看上去就像一颗倒着的树。

比如如下的继承树:

       

 

 

静态语言 vs 动态语言

对于静态语言(例如Java)来说,

如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,

否则,将无法调用run()方法。

 

对于Python这样的动态语言来说,

则不一定需要传入Animal类型。

只需要保证传入的对象有一个run()方法就可以了

 

class Timer(object):

    def run(self):

        print('Start...')

这就是动态语言的“鸭子类型”,

它并不要求严格的继承体系,

一个对象只要“看起来像鸭子,走起路来像鸭子”,

那它就可以被看做是鸭子。

 

Python的“file-like object“就是一种鸭子类型。

对真正的文件对象,它有一个read()方法,返回其内容。

但是,许多对象,只要有read()方法,都被视为“file-like object“。

许多函数接收的参数就是“file-like object“,

不一定要传入真正的文件对象,

完全可以传入任何实现了read()方法的对象。

 

 

 

获取对象信息

 

当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?

 

使用type()

首先,判断对象类型,使用type()函数:

基本类型都可以用type()判断:

>>>type(123)

<class 'int'>

>>>type('xxx')

<class 'str'>

>>>type(None)

<type(None) 'NoneType'>

 

如果一个变量指向函数或者类,也可以用type()判断:

>>> type(abs)

<class 'builtin_function_or_method'>

>>> type(a)

<class '__main__.Animal'>

但是type()函数返回的是什么类型呢?它返回对应的Class类型。

 

如果要判断一个对象是否是函数怎么办?

可以使用types模块中定义的常量:

>>> import types

>>> def fn():

...     pass

...

>>> type(fn)==types.FunctionType

True

>>> type(abs)==types.BuiltinFunctionType

True

>>> type(lambda x: x)==types.LambdaType

True

 

使用isinstance()

对于class的继承关系来说,使用type()就很不方便。

我们要判断class的类型,可以使用isinstance()函数。

优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。

 

我们回顾上次的例子,如果继承关系是:

object -> Animal -> Dog -> Husky

那么,isinstance()就可以告诉我们,一个对象是否是某种类型。

先创建3种类型的对象:

>>> a = Animal()

>>> d = Dog()

>>> h = Husky()

然后,判断:

>>> isinstance(h, Husky)

True

没有问题,因为h变量指向的就是Husky对象。

 

再判断:

>>> isinstance(h, Dog)

True

h虽然自身是Husky类型,但由于Husky是从Dog继承下来的,

所以,h也还是Dog类型。

 

换句话说,isinstance()判断的是一个对象是否是该类型本身

或者位于该类型的父继承链上

 

 

实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性

给实例绑定属性的方法是通过实例变量,或者通过self变量

class Student(object):

    def __init__(self, name):

        self.name = name

s = Student('Karry')

s.score = 60

但是,如果Student类本身需要绑定一个属性呢?

可以直接在class中定义属性,这种属性是类属性,归Student类所有:

class Student(object):

    name = 'Student'

当我们定义了一个类属性后,这个类属性归类所有,

但类的所有实例都可以访问到

来测试一下:

>>> class Student(object):

...     name = 'Student'

...

>>> s = Student() # 创建实例s

# 打印name属性,实例并没有name属性,会继续查找class的name属性

>>> print(s.name)  àStudent

# 打印类的name属性

>>> print(Student.name) àStudent

>>> s.name = 'Kitty'  # 给实例绑定name属性

# 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性

>>> print(s.name)  àKitty

 

编程时,千万不要对实例属性类属性使用相同的名字

因为相同名称的实例属性将屏蔽掉类属性,

但是当删除实例属性后,再使用相同的名称,访问到的将是类属性。

 

小结:

  1. 实例属性属于各个实例所有,互不干扰;
  2. 类属性属于类所有,所有实例共享一个属性;
  3. 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。

 

使用__slots__

给类的实例动态绑定属性

正常情况下:

当定义了一个class,创建了一个class的实例后,

就可以给该实例绑定任何属性和方法,

这就是动态语言的灵活性。

 

先定义class:

class Student(object):

pass

 

然后,尝试给实例绑定一个属性:

>>> s = Student()

>>> s.name = 'apple'  # 动态给实例绑定一个属性

>>> print(s.name)

 

给类的实例动态绑定方法

可以尝试给实例绑定一个方法:

>>> def set_age(self, age): # 定义一个函数作为实例方法

...     self.age = age

...

>>> from types import MethodType

>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法

>>> s.set_age(25) # 调用实例方法

>>> s.age # 测试结果

25

 

给一个实例绑定的方法,对另一个实例是不起作用的:

>>> s2 = Student() # 创建新的实例

>>> s2.set_age(25) # 尝试调用方法

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'Student' object has no attribute 'set_age'

 

为了给所有实例都绑定方法,可以给class绑定方法:

>>> def set_score(self, score):

...     self.score = score

...

>>> Student.set_score = set_score

 

给class绑定方法后,所有实例均可调用:

>>> s.set_score(100)

>>> s.score

100

>>> s2.set_score(99)

>>> s2.score

99

 

通常情况下,上面的set_score方法可以直接定义在class中,

但动态绑定允许我们在程序运行的过程中动态给class加上功能,

这在静态语言中很难实现。

 

使用__slots__

但是,如果我们想要限制实例的属性怎么办?

比如,只允许对Student实例添加name和age属性。

 

为了达到限制的目的,

Python允许在定义class的时候,定义一个特殊的__slots__变量,

来限制该class实例能添加的属性:

class Student(object):

__slots__ = ('name', 'age')  # 用tuple定义允许绑定的属性名称

 

然后,我们试试:

>>> s = Student() # 创建新的实例

>>> s.name = 'Michael' # 绑定属性'name'

>>> s.age = 25 # 绑定属性'age'

>>> s.score = 99 # 绑定属性'score'

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'Student' object has no attribute 'score'

 

由于'score'没有被放到__slots__中,

所以不能绑定score属性,

试图绑定score将得到AttributeError的错误。

 

使用__slots__要注意,

__slots__定义的属性仅对当前类实例起作用,

对继承的子类是不起作用的:

除非在子类中也定义__slots__,

这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。

 

__slots__原理分析

Python是一门动态语言:

可以在运行过程中,修改实例的属性和增删方法。

一般,任何类的实例都包含一个字典__dict__,

Python通过这个字典可以将任意属性绑定到实例上。

 

有时候我们只想使用固定的属性,而不想任意绑定属性,

这时候就可以定义一个属性名称集合,只有在这个集合里的名称才可以绑定。

__slots__就是完成这个功能的。

 

使用@property

检查参数的合法性

在绑定属性时,如果我们直接把属性暴露出去,

虽然写起来很简单,但是,没办法检查参数

导致可以随便修改属性值。比如:

s = Student()

s.score = 9999

这显然不合逻辑。

为了限制score的范围,可以通过一个set_score()方法来设置成绩,

再通过一个get_score()来获取成绩,这样,在set_score()方法里,

就可以检查参数:

class Student(object):

    def get_score(self):

         return self._score

 

    def set_score(self, value):

        if not isinstance(value, int):

            raise ValueError('score must be an integer!')

        if value < 0 or value > 100:

            raise ValueError('score must between 0 ~ 100!')

        self._score = value

现在,对任意的Student实例进行操作,

就不能随心所欲地设置score了:

>>> s = Student()

>>> s.set_score(60) # ok!

>>> s.get_score()

60

>>> s.set_score(9999)

Traceback (most recent call last):

  ...

ValueError: score must between 0 ~ 100!

但是,上面的调用方法又略显复杂,

没有直接用属性这么直接简单。

@property装饰器

有没有既能检查参数

又可以用类似属性这样简单的方式来访问类的变量呢?

对于追求完美的Python程序员来说,这是必须要做到的!

 

还记得装饰器(decorator)可以给函数动态加上功能吗?

对于类的方法,装饰器一样起作用。

Python内置的@property装饰器就是负责把一个方法变成属性调用的:

class Student(object):

    @property

    def score(self):

        return self._score

 

    @score.setter

    def score(self, value):

        if not isinstance(value, int):

            raise ValueError('score must be an integer!')

        if value < 0 or value > 100:

            raise ValueError('score must between 0 ~ 100!')

        self._score = value

@property实现原理

@property的实现比较复杂,我们先考察如何使用。

把一个getter方法变成属性,只需要加上@property就可以了,

此时,@property本身又创建了另一个装饰器@score.setter,

负责把一个setter方法变成属性赋值,

于是,我们就拥有一个可控的属性操作:

>>> s = Student()

>>> s.score = 60 # OK,实际转化为s.set_score(60)

>>> s.score # OK,实际转化为s.get_score()

60

>>> s.score = 9999

Traceback (most recent call last):

  ...

ValueError: score must between 0 ~ 100!

注意到这个神奇的@property,

我们在对实例属性操作的时候,

就知道该属性很可能不是直接暴露的,

而是通过getter和setter方法来实现的。

 

只读属性

还可以定义只读属性只定义getter方法,不定义setter方法。

class Student(object):

    @property

    def age(self):

        return 2015 - self._birth

上面的age就是一个只读属性,因为没有定义setter方法。

 

@property小结

@property广泛应用在类的定义中,

可以让调用者写出简短的代码,同时保证对参数进行必要的检查,

这样,程序运行时就减少了出错的可能性

 

多重继承

 

多重继承的概念

先来了解下多重继承的概念,

所谓多重继承,是指python的类可以有两个以上父类,

也即有类A,类B,类C,

C同时继承类A与类B,

此时C中可以使用A与B中的属性与方法。

 

那么问题来了,如果A与B中具有相同名字的方法,

这个时候python怎么调用的会是哪个方法呢?

 

python中使用多继承,

会涉及到查找顺序(MRO)重复调用(钻石继承,也叫菱形继承问题)等

 

MRO

MRO即method resolution order,

用于判断子类调用的属性来自于哪个父类。

在Python2.3之前,MRO是基于深度优先算法的,

自2.3开始使用C3算法,定义类时需要继承object

这样的类称为新式类,否则为旧式类

从图中可以看出,

旧式类查找属性时是深度优先搜索,

新式类则是广度优先搜索

MixIn

在设计类的继承关系时,

通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。

但是,如果需要“混入”额外的功能,

通过多重继承就可以实现,

比如,让Ostrich除了继承自Bird外,再同时继承Runnable。

这种设计通常称之为MixIn

通过多重继承,一个子类就可以同时获得多个父类的所有功能。

 

多继承案例

python和C++一样,支持多继承。

概念虽然容易,但是困难的工作是如果子类调用一个自身没有定义的属性,

它是按照何种顺序去到父类寻找呢,

尤其是众多父类中有多个都包含该同名属性

 

class P1 #(object):

   def foo(self):           

       print ('p1-foo' )

 

class P2 #(object):

   def foo(self):

       print ('p2-foo' ) 

   def bar(self):

       print( 'p2-bar' ) 

 

class C1 (P1,P2):

   pass  

 

class C2 (P1,P2):

   def bar(self):

       print ('C2-bar' ) 

 

class D(C1,C2):

   pass

  

对经典类和新式类来说,属性的查找顺序是不同的。

现在分别看一下经典类和新式类两种不同的表现

 

1、经典类:深度优先

d=D()

d.foo() # 输出 p1-foo

d.bar() # 输出 p2-bar

实例d调用foo()时,搜索顺序是 D => C1 => P1

 

实例d调用bar()时,搜索顺序是 D => C1 => P1 => P2

 

换句话说,经典类的搜索方式是按照“从左至右,深度优先”的方式去查找属性。

d先查找自身是否有foo方法,没有则查找最近的父类C1里是否有该方法,

如果没有则继续向上查找,直到在P1中找到该方法,查找结束。

 

2、新式类:广度优先

使用新式类要去掉第一段代码中的注释

 

d=D()

d.foo() # 输出 p1-foo

d.bar() # 输出 c2-bar

实例d调用foo()时,搜索顺序是 D => C1 => C2 => P1

实例d调用bar()时,搜索顺序是 D => C1 => C2

 

可以看出,新式类的搜索方式是采用“广度优先”的方式去查找属性。

 

 

 

 

定制类

看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,

这些在Python中是有特殊用途的。

 

除此之外,

Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类

 

__str__

先定义一个Student类,打印一个实例:

>>> class Student(object):

...     def __init__(self, name):

...         self.name = name

...

>>> print(Student('apple'))

<__main__.Student object at 0x109afb190>

打印出一堆<__main__.Student object at 0x109afb190>,不好看。

 

怎么才能打印得好看呢?

只需要定义好__str__()方法,返回一个好看的字符串就可以了:

 

>>> class Student(object):

...     def __init__(self, name):

...         self.name = name

...     def __str__(self):

...         return 'Student object (name: %s)' % self.name

...

>>> print(Student('banana'))

Student object (name: banana)

这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。

 

__repr__

如果直接敲变量不用print,打印出来的实例还是不好看:

>>> s = Student('apple')

>>> s

<__main__.Student object at 0x109afb310>

这是因为直接显示变量调用的不是__str__(),而是__repr__()

两者的区别是__str__()返回用户看到的字符串,

而__repr__()返回程序开发者看到的字符串,

也就是说,__repr__()是为调试服务的

 

解决办法是再定义一个__repr__()。

但是通常__str__()和__repr__()代码都是一样的,

所以,有个偷懒的写法:

class Student(object):

    def __init__(self, name):

        self.name = name

    def __str__(self):

        return 'Student object (name=%s)' % self.name

    __repr__ = __str__

 

使用枚举类

当需要定义常量时,一个办法是用大写变量通过整数来定义

例如月份:

JAN = 1

FEB = 2

MAR = 3

...

NOV = 11

DEC = 12

好处是简单,缺点是类型是int,并且仍然是变量。

 

更好的方法是为这样的枚举类型定义一个class类型

然后,每个常量都是class的一个唯一实例。

Python提供了Enum类来实现这个功能:

 

from enum import Enum

Month = Enum('Month',

('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',

'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')

)

这样我们就获得了Month类型的枚举类

可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

for name, member in Month.__members__.items():

    print(name, '=>', member, ',', member.value)

value属性则是自动赋给成员的int常量,默认从1开始计数。

 

动态创建类

python解释器创建类型

动态语言和静态语言最大的不同:

函数和类的定义,不是编译时定义的,而是运行时动态创建的。

 

比方要定义一个Hello的class,就写一个hello.py模块:

class Hello(object):

    def hello(self, name='world'):

        print('Hello, %s.' % name)

当Python解释器载入hello模块时,

就会依次执行该模块的所有语句,

执行结果就是动态创建出一个Hello的class对象

 

测试如下:

>>> from com.freencre.oo.hello import Hello

>>> h = Hello()

>>> h.hello()

Hello, world.

>>> print(type(Hello))

<class 'type'>

>>> print(type(h))

<class 'hello.Hello'>

type()函数可以查看一个类型或变量的类型,

Hello是一个class,它的类型就是type,

而h是一个实例,它的类型就是class Hello。

type()函数

使用type()函数动态创建类

class的定义是运行时动态创建的

而创建class的方法就是使用type()函数

 

type()函数既可以返回一个对象的类型

又可以创建出新的类型

 

比如,可以通过type()函数创建出Hello类,

而无需通过class Hello(object)...的定义:

 

>>> def fn(self, name='world'): # 先定义函数

...     print('Hello, %s.' % name)

...

>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class

>>> h = Hello()

>>> h.hello()

Hello, world.

>>> print(type(Hello))

<class 'type'>

>>> print(type(h))

<class '__main__.Hello'>

 

type()函数分析

要创建一个class对象,type()函数依次传入3个参数:

Hello = type('Hello', (object,), dict(hello=fn))

  1. class的名称;
  2. 继承的父类集合,

注意Python支持多重继承,

如果只有一个父类,别忘了tuple的单元素写法;

  1. class的方法名称与函数绑定,

这里把函数fn绑定到方法名hello上。

 

通过type()函数创建的类和直接写class是完全一样的,

因为Python解释器遇到class定义时,

仅仅是扫描一下class定义的语法,

然后调用type()函数创建出class。

 

正常情况下,都用class Xxx...来定义类,

但是,type()函数也允许动态创建出类来,

动态语言本身支持运行期动态创建类,

这和静态语言有非常大的不同。

 

metaclass

元类简介

除了使用type()动态创建类以外,

要控制类的创建行为,还可以使用metaclass

 

metaclass,直译为元类,简单的解释就是:

当定义了类以后,就可以根据这个类创建出实例,

所以:先定义类,然后创建实例

 

但是如果想动态创建出类呢?

那就必须根据metaclass创建出类,

所以:先定义metaclass,然后创建类

 

连接起来就是:

先定义metaclass,就可以创建类,最后创建实例

 

所以,metaclass允许创建类或者修改类。

换句话说,可以把类看成是metaclass创建出来的“实例”

 

到底什么是元类?

通俗的就是说,元类就是创建类的类

也就是说metaclass的实例化结果是类,

而class实例化的结果是instance。

 

也可以这么理解的:

metaclass是类似创建类的模板,

所有的类都是通过它来创建的(调用__init__)

元类分析

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。

正常情况下,不会碰到需要使用metaclass的情况,

 

先看一个简单的例子,定义TestMetaclass,

按照默认习惯,metaclass的类名总是以Metaclass结尾,

以便清楚地表示这是一个metaclass

 

  1. print('1. Metaclass declaration')

# metaclass是创建类,所以必须从”type”类型派生:

class TestMetaclass(type):

    def __init__(cls, name, bases, attrd):

        super(TestMetaclass, cls).__init__(name, bases, attrd)

        print('3. Create class %r' % (name))

 

  1. print('2. Class Foo declaration')

class Foo(object):

    __metaclass__ = TestMetaclass

 

    def __init__(self):

        print('*. Init class %r' % (self.__class__.__name__))

 

  1. print('4. Class Foo f1 instantiation')

f1 = Foo()

 

print('5. Class Foo f2 instantiation')

f2 = Foo()

 

print('END')

 

当我们写下__metaclass__ = ListMetaclass语句时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。

 

__new__()方法接收到的参数依次是:

当前准备创建的类的对象;

类的名字;

类继承的父类集合;

类的方法集合。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

福优学苑@音视频+流媒体

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值