python类和对象 您所在的位置:网站首页 python如何在一个类中引用多个类 python类和对象

python类和对象

2023-06-05 12:02| 来源: 网络整理| 查看: 265

面向对象编程(Object-oriented Programming,简称OOP),是一种封装代码的方法。

面向对象中,常用术语包括:

类:可以理解是一个模板,通过它可以创建出无数个具体实例。比如,前面编写的 tortoise 表示的只是乌龟这个物种,通过它可以创建出无数个实例来代表各种不同特征的乌龟(这一过程又称为类的实例化)。对象:类并不能直接使用,通过类创建出的实例(又称对象)才能使用。这有点像汽车图纸和汽车的关系,图纸本身(类)并不能为人们使用,通过图纸创建出的一辆辆车(对象)才能使用。属性:类中的所有变量称为属性。例如,tortoise这个类中,bodyColor、footNum、weight、hasShell都是这个类拥有的属性。方法:类中的所有函数通常称为方法。不过,和函数所有不同的是,类方法至少要包含一个self参数(后续会做详细介绍)。例如,tortoise类中,crawl()、eat()、sleep()、protect()都是这个类所拥有的方法,类方法无法单独使用,只能和类的对象一起使用 定义类

Python中使用类的顺序是:先创建(定义)类,然后再创建类的实例对象,通过实例对象实现特定的功能。

Python中,创建一个类使用**class关键字**实现,其基本语法格式如下:

class 类名:    零个到多个类属性...    零个到多个类方法...

从上面定义来看,Python的类定义有点像函数定义,都是以**冒号(:)**作为类体的开始,以统一缩进的部分作为类体的。区别只是函数定义使用def关键字,而类定义则使用 class关键字。

class Person :    '''这是一个学习Python定义的一个Person类'''    # 下面定义了一个类属性    hair = 'black'    # 下面定义了一个say方法    def say(self, content):        print(content) __init__()类构造方法

在创建类时,我们可以手动添加一个 __init__()方法,该方法是一个特殊的类实例方法,称为构造方法(或构造函数)。构造方法用于创建对象时使用,每当创建一个类的实例对象时,Python 解释器都会自动调用它。

def __init__(self,...):    代码块

此方法的方法名中,开头和结尾各有2个下划线,且中间不能有空格。

__init__()方法可以包含多个参数,但必须包含一个名为self的参数,且必须作为第一个参数。也就是说,类的构造方法最少也要有一个self参数。

class Person :    '''这是一个学习Python定义的一个Person类'''    def __init__(self):        print("调用构造方法") zhangsan = Person() """运行结果 调用构造方法 """ class Person :    '''这是一个学习Python定义的一个Person类'''    def __init__(self,name,age):        print("这个人的名字是:",name," 年龄为:",age) #创建 zhangsan 对象,并传递参数给构造函数 zhangsan = Person("张三",20) """运行结果 这个人的名字是: 张三 年龄为: 20 """ 类对象的创建和使用

创建类对象的过程又称为类的实例化。

对已创建的类进行实例化,其语法格式如下:

类名(参数)

当创建类时,若没有显式创建__init()__构造方法或者该构造方法中只有一个self参数,则创建类对象时的参数可以省略不写

class Person :    '''这是一个学习Python定义的一个Person类'''    # 下面定义了2个类变量    name = "zhangsan"    age = "20"    def __init__(self,name,age):        #下面定义 2 个实例变量        self.name = name        self.age = age        print("这个人的名字是:",name," 年龄为:",age)    # 下面定义了一个say实例方法    def say(self, content):        print(content) # 将该Person对象赋给p变量 p = Person("张三",20) 类对象的使用

创建对象之后,接下来即可使用该对象了。Python的对象大致有以下作用:

操作对象的实例变量,包括访问、修改实例变量的值、以及给对象添加或删除实例变量)。调用对象的方法,包括调用对象的方法,已经给对象动态添加方法。 类对象访问变量或方法

格式如下:

对象名.变量名 # 使用已创建好的类对象访问类中实例变量 对象名.方法名(参数) # 使用类对象调用类中方法

对象名和变量名以及方法名之间用点"."连接。

# 输出p的name、age实例变量 print(p.name, p.age) # 访问p的name实例变量,直接为该实例变量赋值 p.name = '李刚' # 调用p的say()方法,声明say()方法时定义了2个形参,但第一个形参(self)不需要传值,因此调用该方法只需为第二个形参指定一个值 p.say('Python语言很简单,学习很容易!') # 再次输出p的name、age实例变量 print(p.name, p.age) """运行结果 这个人的名字是: 张三 年龄为: 20 张三 20 Python语言很简单,学习很容易! 李刚 20 """ 给类对象动态添加变量

Python支持为已创建好的对象动态增加实例变量,方法也很简单,只要为它的新变量赋值即可。

# 为p对象增加一个skills实例变量 p.skills = ['programming', 'swimming'] print(p.skills) """ ['programming', 'swimming'] """ # 删除p对象的name实例变量 del p.name # 再次访问p的name实例变量 print(p.name) # 'Person' object has no attribute 'name' 给类对象动态添加方法

Python也允许为对象动态增加方法。比如上面程序中在定义Person类时只定义了一个say()方法,但程序完全可以为 p 对象动态增加方法。

但需要说明的是,为p对象动态增加的方法,Python不会自动将调用者自动绑定到第一个参数(即使将第一个参数命名为self也没用)。例如如下代码:

# 先定义一个函数 def info(self): print("---info函数---", self) # 使用info对p的foo方法赋值(动态绑定方法) p.foo = info # Python不会自动将调用者绑定到第一个参数, # 因此程序需要手动将调用者绑定为第一个参数 p.foo(p) # ① # 使用lambda表达式为p对象的bar方法赋值(动态绑定方法) p.bar = lambda self: print('--lambda表达式--', self) p.bar(p) # ②

上面的第 5 行和第 11 行代码分别使用函数、lambda表达式为 p 对象动态增加了方法,但对于动态增加的方法,Python不会自动将方法调用者绑定到它们的第一个参数,因此程序必须手动为第一个参数传入参数值,如上面程序中①号、②号代码所示。

self用法

同一个类可以产生多个对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法,换句话说,Python会自动绑定类方法的第一个参数指向调用该方法的对象。如此,Python解释器就能知道到底要操作哪个对象的方法了。

对于构造方法来说,self参数(第一个参数)代表该构造方法正在初始化的对象。

class Dog: def __init__(self): print(self,"在调用构造方法") # 定义一个jump()方法 def jump(self): print(self,"正在执行jump方法") # 定义一个run()方法,run()方法需要借助jump()方法 def run(self): print(self,"正在执行run方法") # 使用self参数引用调用run()方法的对象 self.jump() dog1 = Dog() dog1.run() dog2 = Dog() dog2.run()

上面代码中,jump()和run() 中的self代表该方法的调用者,即谁在调用该方法,那么 self就代表谁,因此,该程序的运行结果为:

在调用构造方法 正在执行run方法 正在执行jump方法 在调用构造方法 正在执行run方法 正在执行jump方法

当Python对象的一个方法调用另一个方法时,不可以省略self。

class InConstructor : def __init__(self) : # 在构造方法里定义一个foo变量(局部变量) foo = 0 # 使用self代表该构造方法正在初始化的对象 # 下面的代码将会把该构造方法正在初始化的对象的foo实例变量设为6 self.foo = 6 # 所有使用InConstructor创建的对象的foo实例变量将被设为6 print(InConstructor().foo) # 输出6

在InConstructor的构造方法中,self参数总是引用该构造方法正在初始化的对象。程序中将正在执行初始化的InConstructor对象的foo实例变量设为 6,这意味着该构造方法返回的所有对象的foo实例变量都等于 6。

类变量和实例变量 类变量(类属性)

类变量指的是定义在类中,但在各个类方法外的变量。类变量的特点是:所有类的实例化对象都可以共享类变量的值,即类变量可以在所有实例化对象中作为公用资源。

注意,类变量推荐直接用类名访问,但也可以使用对象名访问。

class Address : detail = '广州' post_code = '510660' def info (self): # 尝试直接访问类变量 #print(detail) # 报错 # 通过类来访问类变量 print(Address.detail) # 输出 广州 print(Address.post_code) # 输出 510660 #创建 2 个类对象 addr1 = Address() addr1.info() addr2 = Address() addr2.info() # 修改Address类的类变量 Address.detail = '佛山' Address.post_code = '460110' addr1.info() addr2.info() """运算结果 广州 510660 广州 510660 佛山 460110 佛山 460110 """

在 Python 中,除了可以通过类名访问类属性之外,还可以动态地为类和对象添加类变量。例如,在上面代码的基础,添加以下代码:

Address.depict ="佛山很美" print(addr1.depict) print(addr2.depict) """ 佛山很美 佛山很美 """ 实例变量(实例属性)

实例变量指的是定义在类的方法中的属性,它的特点是:只作用于调用方法的对象。

注意,实例变量只能通过对象名访问,无法通过类名直接访问。

class Inventory: # 定义两个类变量 item = '鼠标' quantity = 2000 # 定义实例方法 def change(self, item, quantity): # 下面赋值语句不是对类变量赋值,而是定义新的实例变量 self.item = item self.quantity = quantity # 创建Inventory对象 iv = Inventory() iv.change('显示器', 500) # 访问iv的item和quantity实例变量 print(iv.item) # 显示器 print(iv.quantity) # 500 # 访问Inventory的item和quantity类变量 print(Inventory.item) # 鼠标 print(Inventory.quantity) # 2000 实例方法、静态方法和类方法详解

和类属性可细分为类属性和实例属性一样,类中的方法也可以有更细致的划分,具体可分为类方法、实例方法和静态方法。

类实例方法

通常情况下,在类中定义的方法默认都是实例方法。

class Person : #类构造方法,也属于实例方法 def __init__(self, name = 'Charlie', age=8): self.name = name self.age = age # 下面定义了一个say实例方法 def say(self, content): print(content) #创建一个类对象 person = Person() #类对象调用实例方法 person.say("类对象调用实例方法") #类名调用实例方法,需手动给 self 参数传值 Person.say(person,"类名调用实例方法") """ 类对象调用实例方法 类名调用实例方法 """ 类方法

Python类方法和实例方法相似,它最少也要包含一个参数,只不过,**类方法中通常将其命名为cls,且Python会自动将类本身绑定给cls参数(而不是类对象)。**因此,在调用类方法时,无需显式为cls参数传参。

除此之外,和实例方法最大的不同在于,类方法需要使用@classmethod进行修饰,例如:

class Bird: # classmethod修饰的方法是类方法 @classmethod def fly (cls): print('类方法fly: ', cls)

注意,如果没有@classmethod,则Python解释器会将fly()方法认定为实例方法,而不是类方法。

类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐),例如:

# 调用类方法,Bird类会自动绑定到第一个参数 Bird.fly() b = Bird() # 使用对象调用fly()类方法,其实依然还是使用类调用, # 因此第一个参数依然被自动绑定到Bird类 b.fly() """ 类方法fly: 类方法fly: """ 类静态方法

静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。

静态方法没有类似self、cls这样的特殊参数,因此Python解释器不会对它包含的参数做任何类或对象的绑定,也正是因为如此,此方法中无法调用任何类和对象的属性和方法,静态方法其实和类的关系不大。

静态方法需要使用@staticmethod修饰,例如:

class Bird: # staticmethod修饰的方法是静态方法 @staticmethod def info (p): print('静态方法info: ', p) #类名直接调用静态方法 Bird.info("类名") #类对象调用静态方法 b = Bird() b.info("类对象") """ 静态方法info: 类名 静态方法info: 类对象 """

在使用Python编程时,一般不需要使用类方法或静态方法,程序完全可以使用函数来代替类方法或静态方法。但是在特殊的场景(比如使用工厂模式)下,类方法或静态方法也是不错的选择。

类调用实例方法

使用类调用实例方法,那么该方法的第一个参数(self)怎么自动绑定呢?

class User: def walk (self): print(self, '正在慢慢地走') # 通过类调用实例方法 User.walk()

运行上面代码,程序会报出如下错误:

TypeError: walk() missing 1 required positional argument:'self'

如果程序依然希望使用类来调用实例方法,则必须手动为方法的第一个参数传入参数值。例如,将上面的最后一行代码改为如下形式:

u = User() # 显式为方法的第一个参数绑定参数值 User.walk(u)

此代码显式地为walk()方法的第一个参数绑定了参数值,这样的调用效果完全等同于执行u.walk()。

描述符

Python中,通过使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。

本质上看,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理全权委托给描述符类。

描述符类基于以下 3 个特殊方法,换句话说,这 3 个方法组成了描述符协议:

__set__(self, obj, type=None):在设置属性时将调用这一方法(后续用setter表示);__get__(self, obj, value):在读取属性时将调用这一方法(后续用getter表示);__delete__(self, obj):对属性调用del时将调用这一方法。

其中,实现了setter和 getter方法的描述符类被称为数据描述符;反之,如果只实现了getter方法,则称为非数据描述符。

实际上,在每次查找属性时,描述符协议中的方法都由类对象的特殊方法 __getattribute__()调用(注意不要和__getattr__()弄混)。也就是说,每次使用类对象.属性(或者getattr(类对象,属性值))的调用方式时,都会隐式地调用 __getattribute__(),它会按照下列顺序查找该属性:

验证该属性是否为类实例对象的数据描述符;如果不是,就查看该属性是否能在类实例对象的**__dict__**中找到;最后,查看该属性是否为类实例对象的非数据描述符。 #描述符类 class revealAccess: def __init__(self, initval = None, name = 'var'): self.val = initval self.name = name def __get__(self, obj, objtype): print("Retrieving",self.name) return self.val def __set__(self, obj, val): print("updating",self.name) self.val = val class myClass: x = revealAccess(10,'var "x"') y = 5 m = myClass() print(m.x) m.x = 20 print(m.x) print(m.y)

运行结果为:

Retrieving var "x" 10 updating var "x" Retrieving var "x" 20 5

从这个例子可以看到,如果一个类的某个属性有数据描述符,那么每次查找这个属性时,都会调用描述符的__get__()方法,并返回它的值;同样,每次在对该属性赋值时,也会调用__set__()方法。 注意,虽然上面例子中没有使用__del__()方法,但也很容易理解,当每次使用del类对象.属性(或者delattr(类对象,属性))语句时,都会调用该方法。

定义属性 property()函数

在不破坏类封装原则的基础上,为了能够有效操作类中的属性,类中应包含读(或写)类属性的多个getter(或setter)方法,这样就可以通过“类对象.方法(参数)”的方式操作属性,例如:

class Rectangle: # 定义构造方法 def __init__(self, width, height): self.width = width self.height = height # 定义setsize()函数 def setsize (self , size): self.width, self.height = size # 定义getsize()函数 def getsize (self): return self.width, self.height # 定义delsize()函数 def delsize (self): self.width, self.height = 0, 0 rect = Rectangle(3 , 4) rect.setsize((6,8)) print(rect.getsize()) # (6,8)

Python中提供了**property()函数**,可以实现在不破坏类封装原则的前提下,让开发者依旧使用“类对象.属性”的方式操作类中的属性。

property()函数的基本使用格式如下:

属性名=property(fget=None, fset=None, fdel=None, doc=None)

其中,fget参数用于指定获取该属性值的类方法;fset参数用于指定设置该属性值的方法;fdel参数用于指定删除该属性值的方法;最后的doc是一个文档字符串,用于提供说明此函数的作用。

开发者调用property()函数时,可以传入 0 个(既不能读,也不能写的属性)、1 个(只读属性)、2 个(读写属性)、3 个(读写属性,也可删除)和 4 个(读写属性,也可删除,包含文档说明)参数。

例如,对前面的Rectangle类做适当的修改,使用property()函数定义一个size属性:

class Rectangle: # 定义构造方法 def __init__(self, width, height): self.width = width self.height = height # 定义setsize()函数 def setsize (self , size): self.width, self.height = size # 定义getsize()函数 def getsize (self): return self.width, self.height # 定义getsize()函数 def delsize (self): self.width, self.height = 0, 0 # 使用property定义属性 size = property(getsize, setsize, delsize, '用于描述矩形大小的属性') # 访问size属性的说明文档 print(Rectangle.size.__doc__) # 通过内置的help()函数查看Rectangle.size的说明文档 help(Rectangle.size) rect = Rectangle(4, 3) # 访问rect的size属性 print(rect.size) # (4, 3) # 对rect的size属性赋值 rect.size = 9, 7 # 访问rect的width、height实例变量 print(rect.width) # 9 print(rect.height) # 7 # 删除rect的size属性 del rect.size # 访问rect的width、height实例变量 print(rect.width) # 0 print(rect.height) # 0 """运行结果 用于描述矩形大小的属性 Help on property: 用于描述矩形大小的属性 (4, 3) 9 7 0 0 """

程序中,使用property()函数定义了一个size属性,在定义该属性时一共传入了 4 个参数,这意味着该属性可读、可写、可删除,也有说明文档。所以,该程序尝试对Rectangle对象的size属性进行读、写、删除操作,其实这种读、写、删除操作分别被委托给getsize()、setsize()和delsize()方法来实现。

@property装饰器 get属性

Python还提供了@property装饰器。通过@property装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对“()”小括号。

@property的语法格式如下:

@property def 方法名(self) 代码块

例如,定义一个矩形类,并定义用 @property 修饰的方法操作类中的 area 私有属性,代码如下:

class Rect: def __init__(self,area): self.__area = area @property def area(self): return self.__area rect = Rect(30) #直接通过方法名来访问 area 方法 print("矩形的面积是:",rect.area) # 运行结果为:矩形的面积为: 30

上面程序中,使用@property修饰了area()方法,这样就使得该方法变成了area属性的getter方法。需要注意的是,如果类中只包含该方法,那么area属性将是一个只读属性。也就是说,在使用Rect类时,无法对area属性重新赋值,即运行如下代码会报错:

rect.area = 90 print("修改后的面积:",rect.area) """运行结果 Traceback (most recent call last): File "C:\Users\mengma\Desktop\1.py", line 10, in rect.area = 90 AttributeError: can't set attribute """ set属性

要想实现修改area属性的值,还需要为area属性添加setter方法,就需要用到setter装饰器,它的语法格式如下:

@方法名.setter def 方法名(self, value): 代码块

例如,为Rect类中的area方法添加setter方法,代码如下:

@area.setter def area(self, value): self.__area = value rect.area = 90 print("修改后的面积:",rect.area) # 运行结果为: 修改后的面积: 90 del属性

还可以使用deleter装饰器来删除指定属性,其语法格式为:

@方法名.deleter def 方法名(self): 代码块

例如,在Rect类中,给area()方法添加deleter方法,实现代码如下:

@area.deleter def area(self): self.__area = 0 del rect.area print("删除后的area值为:",rect.area) # 运行结果为: 删除后的area值为: 0 封装机制

封装(Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

封装机制保证了类内部数据结构的完整性,因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。

为了实现良好的封装,需要从以下两个方面来考虑:

将对象的属性和实现细节隐藏起来,不允许外部直接访问。把方法暴露出来,让方法来控制对这些属性进行安全的访问和操作。

因此,实际上封装有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。

python中,只要将 Python 类的成员命名为以双下画线开头的,Python 就会把它们隐藏起来。

class User : def __hide(self): print('示范隐藏的hide方法') def getname(self): return self.__name def setname(self, name): if len(name) < 3 or len(name) > 8: raise ValueError('用户名长度必须在3~8之间') self.__name = name name = property(getname, setname) def setage(self, age): if age < 18 or age > 70: raise ValueError('用户名年龄必须在18在70之间') self.__age = age def getage(self): return self.__age age = property(getage, setage) # 创建User对象 u = User() # 对name属性赋值,实际上调用setname()方法 u.name = 'fk' # 引发 ValueError: 用户名长度必须在3~8之间 u.name = 'fkit' u.age = 25 print(u.name) # fkit print(u.age) # 25 # 尝试调用隐藏的__hide()方法 u.__hide() # AttributeError:'User' object has no attribute 'hide'

上面程序将User的两个实例变量分别命名为**__name和__age,这两个实例变量就会被隐藏起来,这样程序就无法直接访问__name、__age**变量,只能通过setname()、getname()、setage()、getage()这些访问器方法进行访问,而setname()、setage()会对用户设置的 name、age进行控制,只有符合条件的name、age才允许设置。

Python其实没有真正的隐藏机制,双下画线只是Python的一个小技巧,Python 会“偷偷”地改变以双下画线开头的方法名,会在这些方法名前添加单下画线和类名。因此上面的__hide()方法其实可以按如下方式调用(通常并不推荐这么干):

# 调用隐藏的__hide()方法 u._User__hide() # 示范隐藏的hide方法 继承机制

继承是面向对象的三大特征之一,也是实现代码复用的重要手段。继承经常用于创建和现有类功能类似的新类,又或是新类只需要在现有类基础上添加一些成员(属性和方法),但又不想直接将现有类代码复制给新类。

Python 中,实现继承的类称为子类,被继承的类称为父类(也可称为基类、超类)。子类继承父类的语法是:在定义子类时,将多个父类放在子类之后的圆括号里。语法格式如下:

class 类名(父类1, 父类2, ...): #类定义部分

注意,Python 的继承是多继承机制,即一个子类可以同时拥有多个直接父类。

class Fruit: def info(self): print("我是一个水果!重%g克" % self.weight) class Food: def taste(self): print("不同食物的口感不同") # 定义Apple类,继承了Fruit和Food类 class Apple(Fruit, Food): pass # 创建Apple对象 a = Apple() a.weight = 5.6 # 调用Apple对象的info()方法 a.info() # 调用Apple对象的taste()方法 a.taste() """运行结果 我是一个水果!重5.6克 不同食物的口感不同 """ 子类如何找到父类的属性和方法

方法解析顺序(Method Resolution Order),简称**MRO**。对于只支持单继承的编程语言来说,MRO很简单,就是从当前类开始,逐个搜索它的父类;而对于Python,它支持多继承,MRO相对会复杂一些。

Python发展至今,经历了以下 3 种MRO算法,分别是:

从左往右,采用深度优先搜索(DFS)的算法,称为旧式类的MRO;自Python 2.2版本开始,新式类在采用深度优先搜索算法的基础上,对其做了优化;自Python 2.3版本,对新式类采用了C3算法。由于Python 3.x仅支持新式类,所以该版本只使用C3算法。 旧式类MRO算法 class A: def method(self): print("CommonA") class B(A): pass class C(A): def method(self): print("CommonC") class D(B, C): pass print(D().method())

此程序中的 4 个类是一个“菱形”继承的关系,当使用 D 类对象访问method()方法时,根据深度优先算法,搜索顺序为 D->B->A->C->A。

因此,使用旧式类的MRO算法最先搜索得到的是基类 A 中的method()方法,即在Python 2.x版本中,此程序的运行结果为:

CommonA

但是,这个结果显然不是想要的,我们希望搜索到的是 C 类中的method()方法。

新式类MRO算法

Python 2.2版本推出了新的计算新式类MRO的方法,它仍然采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。

仍以上面程序为例,通过深度优先遍历,其搜索顺序为 D->B->A->C->A,由于此顺序中有 2 个 A,因此仅保留后一个,简化后得到最终的搜索顺序为 D->B->C->A。

这种MRO方式已经能够解决“菱形”继承的问题,但是可能会违反单调性原则。所谓单调性原则,是指在类存在多继承时,子类不能改变基类的MRO搜索顺序,否则会导致程序发生异常。

class X(object): pass class Y(object): pass class A(X,Y): pass class B(Y,X): pass class C(A, B): pass

通过进行深度遍历,得到搜索顺序为 C->A->X->object->Y->object->B->Y->object->X->object,再进行简化(相同取后者),得到 C->A->B->Y->X->object。

下面来分析这样的搜索顺序是否合理,我们来看下各个类中的MRO:

对于A,其搜索顺序为A->X->Y->object;对于B,其搜索顺序为B->Y->X->object;对于C,其搜索顺序为C->A->B->X->Y->object。

可以看到,B和C中,X、Y的搜索顺序是相反的,也就是说,当B被继承时,它本身的搜索顺序发生了改变,这违反了单调性原则。

MRO C3

在Python 2.3及后续版本中,运行程序一,得到如下结果:

CommonC 运行程序二,会产生如下异常: Traceback (most recent call last): File "C:\Users\mengma\Desktop\demo.py", line 9, in class C(A, B): TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y

以程序一为主,C3把各个类的MRO记为如下等式:

类A:L[A] = merge(A , object)类B:L[B] = [B] + merge(L[A] , [A])类C:L[C] = [C] + merge(L[A] , [A])类D:L[D] = [D] + merge(L[A] , L[B] , [A] , [B])

注意,以类 A 等式为例,其中merge包含的A称为L[A]的头,剩余元素(这里仅有一个object)称为尾。

这里的关键在于merge,它的运算方式如下:

检查第一个列表的头元素(如L[A]的头),记作H。若H未出现在merge中其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤 1;否则,取出下一个列表的头部记作 H,继续该步骤。

重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,Python会抛出异常。

由此,可以计算出类B的MRO,其计算过程为:

L[B] = [B] + merge(L[A],[A]) = [B] + merge([A,object],[A]) = [B,A] + merge([object]) = [B,A,object] 父类方法重写

子类扩展了父类,子类是一种特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的方法。但在一些场景中,子类需要重写父类的方法。

class Bird: # Bird类的fly()方法 def fly(self): print("我在天空里自由自在地飞翔...") class Ostrich(Bird): # 重写Bird类的fly()方法 def fly(self): print("我只能在地上奔跑...") # 创建Ostrich对象 os = Ostrich() # 执行Ostrich对象的fly()方法,将输出"我只能在地上奔跑..." os.fly()

这种子类包含与父类同名的方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。

使用未绑定方法调用被重写的方法

如果在子类中调用重写之后的方法,Python总是会执行子类重写的方法,不会执行父类中被重写的方法。如果需要在子类中调用父类中被重写的实例方法,则可以通过类名调用。区别在于:在通过类名调用实例方法时,Python 不会为实例方法的第一个参数 self 自动绑定参数值,而是需要程序显式绑定第一个参数 self。这种机制被称为未绑定方法。

class BaseClass: def foo (self): print('父类中定义的foo方法') class SubClass(BaseClass): # 重写父类的foo方法 def foo (self): print('子类重写父类中的foo方法') def bar (self): print('执行bar方法') # 直接执行foo方法,将会调用子类重写之后的foo()方法 self.foo() # 使用类名调用实例方法(未绑定方法)调用父类被重写的方法 BaseClass.foo(self) sc = SubClass() sc.bar() super()函数:调用父类的构造方法

Python的子类也会继承得到父类的构造方法,但如果子类有多个直接父类,那么会优先选择排在最前面的父类的构造方法。例如如下代码:

class Employee : def __init__ (self, salary): self.salary = salary def work (self): print('普通员工正在写代码,工资是:', self.salary) class Customer: def __init__ (self, favorite, address): self.favorite = favorite self.address = address def info (self): print('我是一个顾客,我的爱好是: %s,地址是%s' % (self.favorite, self.address)) # Manager继承了Employee、Customer class Manager (Employee, Customer): pass m = Manager(25000) m.work() #① #m.info() #②

上面程序中第 13 行代码定义了Manager类,该类继承了Employee和Customer两个父类。接下来程序中的Manager类将会优先使用Employee类的构造方法(因为它排在前面),所以程序使用Manager(25000)来创建Manager对象。该构造方法只会初始化 salary实例变量,因此执行上面程序中 ① 号代码是没有任何问题的。

但是当执行到 ② 号代码时就会引发错误,这是由于程序在使用Employee类的构造方法创建Manager对象时,程序并未初始化Customer对象所需的两个实例变量:favorite和address,因此程序引发错误。

为了让 Manager 能同时初始化两个父类中的实例变量,Manager 应该定义自己的构造方法,即重写父类的构造方法。Python 要求,如果子类重写了父类的构造方法,那么子类的构造方法必须调用父类的构造方法。

子类的构造方法调用父类的构造方法有两种方式:

使用未绑定方法,这种方式很容易理解。因为构造方法也是实例方法,当然可以通过这种方式来调用。使用super()函数调用父类的构造方法。

注意,当子类继承多个父类是,super()函数只能用来调用第一个父类的构造方法,而其它父类的构造方法只能使用未绑定的方式调用。

# Manager继承了Employee、Customer class Manager(Employee, Customer): # 重写父类的构造方法 def __init__(self, salary, favorite, address): print('--Manager的构造方法--') # 通过super()函数调用父类的构造方法 super().__init__(salary) # 与上一行代码的效果相同 #super(Manager, self).__init__(salary) # 使用未绑定方法调用父类的构造方法 Customer.__init__(self, favorite, address) # 创建Manager对象 m = Manager(25000, 'IT产品', '广州') m.work() #① m.info() #② """运行结果 --Manager的构造方法-- 普通员工正在写代码,工资是:2500。 我是一个顾客,我的爱好是:IT产品,地址是广州 """

Python 中,由于基类不会在**__init__()**中被隐式地调用,需要程序员显式调用它们。这种情况下,当程序中包含多重继承的类层次结构时,使用super是非常危险的,往往会在类的初始化过程中出现问题。

多态

多态也是一个非常重要的特性,Python是弱类型语言,即在使用变量时,无需为其指定具体的数据类型,这就可能出现,同一个变量会赋值不同的类对象,例如:

class Bird: def move(self, field): print('鸟在%s' % field) class Dog: def move(self, field): print('狗在%s' % field) a = Bird() a.move("飞") a = Dog() a.move("跑") """运行结果 鸟在飞 狗在跑 """

可以看到,a 可以被先后赋值为 Bird 类和 Dog 类的对象。而在此基础上,发生多态还要满足以下 2 个前提条件:

继承:多态一定是发生在子类和父类之间;重写:子类重写了父类的方法。 class Animal:    def move(self,field):        print("动物在%s" % field) class Bird(Animal):    def move(self, field):        print('鸟在%s' % field) class Dog(Animal):    def move(self, field):        print('狗在%s' % field) a = Animal() a.move("叫") a = Bird() a.move("飞") a = Dog() a.move("跑") """运行结果 动物在叫 鸟在飞 狗在跑 """


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有