疯狂Python讲义-李刚编著-第6章类和对象
第6章 类和对象
6.1.类和对象
6.1.1.定义类
类中各成员之间的定义顺序没有任何影响,各成员之间可以相互调用。
Python 类所包含的最重要的两个成员就是变量和方法,其中类变量属于类本身,用于定义该类本身所包含的状态数据;而实例变量则属于该类的对象,用于定义对象所包含的状态数据;方法则用于该类的对象的行为或功能实现。
类似的是,Python 对象的实例变量也可以动态增加或删除——只要对新实例变量赋值就是增加实例变量,因此程序可以在任何地方为已有的对象增加实例变量;程序可通过 del 语句删除已有对象的实例变量。
在类中定义的方法默认是实例方法,定义实例方法的方法与定义函数的方法基本相同,只是实例方法的第一个参数会被绑定到方法的调用者(该类的实例)——因此实例方法至少应该定义一个参数,该参数通常会被命名为 self。
注意:实例方法的第一个参数并不一定要叫 self,其实完全可以叫任意参数名,只是约定俗成地把该参数命名为 self,这样具有最好的可读性。
在实例方法中有一个特别的方法:__init__,这个方法被称为构造方法。构造方法用于构造类的对象,Python 通过调用构造方法返回该类的对象(无须使用 new)。
构造方法是一个类创建对象的根本途径,因此 Python 还提供了一个功能:如果开发者没有为该类定义任何构造方法,那么 Python 会自动为该类定义一个只包含一个 self 参数的默认的构造方法。
class Person:
hair = 'black'
def __init__(self, name = 'Charlie', age = 8):
self.name = name
self.age = age
def say(self, content):
print(content)
在定义类之后,接下来即可使用该类了。Python 的类大致有如下作用:
1.创建对象
2.派生子类
6.1.2.对象的产生和使用
创建对象的根本途径是构造方法,调用某个类的构造方法即可创建这个类的对象,Python 无须使用 new 调用构造方法。
p = Person()
在创建对象之后,接下来即可使用该对象了。Python 的对象大致有如下作用:
1.操作对象的实例变量(包括访问实例变量的值、添加实例变量、删除实例变量)
2.操作对象的方法(包括调用方法,添加方法,删除方法)
对象访问方法或变量的语法是:对象.变量|方法(参数)。在这种方式中,对象是主调者,用于访问该对象的变量或方法。
print(p.name, p.age) # Charlie 8
p.name = '李刚'
p.say('Python 语言很简单,学习很容易!')
print(p.name, p.age) # 李刚 8
6.1.3.对象的动态性
由于 Python 是动态语言,因此程序完全可以为 p 对象动态增加实例变量——只要为它的新变量赋值即可;也可以动态删除实力变量——使用 del 语句即可删除。
p.skills = ['programming', 'swimming'] print(p.skills) del p.name print(p.name) # AttributeError
Python 是动态语言,当然也允许为对象动态增加方法。比如上面程序中在定义 Person 类时只定义了一个 say() 方法,但程序完全可以为 p 对象动态增加方法。
当需要说明的是,为 p 对象动态增加的方法,Python 不会自动将调用自动绑定到第一个参数(即使将第一个参数命名为 self 也没用)。
def info(self):
print("———info 函数———", self)
#动态增加方法
p.foo = info
#Python 不会自动将调用者绑定到第一个参数
#因此程序需要手动将调用者绑定到第一个参数
p.foo(p)
#使用 lambda 表达式为 p 对象的 bar 方法赋值(动态增加方法)
p.bar = lambda self:print('———lambda 表达式———', self)
p.bar(p)
如果希望动态增加的方法也能自动绑定到第一个参数,则可借助于 types 模块下的 MethodType 进行包装。
def intro_func(self, content):
print("我是一个人,信息为:%s" % content)
from types import MethodType
p.intro = MethodType(intro_func, p)
p.intro("生活在别处")
6.1.4.实例方法和自动绑定 self
对于在类体中定义的实例方法,Python 会自动绑定方法的第一个参数(通常建议将参数命名为 self),第一个参数总是指向调用该方法的对象。根据第一个参数出现位置的不同,第一个参数所绑定的对象略有区别。
1.在构造方法中引用该构造方法正在初始化的对象
2.在普通实例方法中引用调用该方法的对象
由于实例方法(包括构造方法)的第一个 self 参数会自动绑定,因此程序在调用普通实例方法、构造方法时不需要为第一个参数传值。
self 参数(自动绑定的第一个参数)最大的作用就是引用当前方法的调用者,比如前面介绍的在构造方法中通过 self 为该对象增加实例对象。也可以在一个实例方法中访问该类的另一个实例方法或变量。假设定义了一个 Dog 类,这个 Dog 对象的 run() 方法需要调用它的 jump() 方法,此时就可通过 self 参数作为 jump() 方法的调用者。
方法的第一个参数所代表的对象是不确定的,但它的类型是确定的——它所代表的只能是当前类的实例;只有当这个方法被调用时,它所代表的对象才被确定下来——谁在调用这个方法,方法的第一个参数就代表谁。
class Dog:
def jump(self):
print("正在执行 jump 方法")
def run(self):
self.jump()
print("正在执行 run 方法")
当 Python 对象的一个方法调用另一个方法时,不可以省略 self。
提示:从 Python 语言的设计来看,Python 的类、对象有点类似于一个命令空间,因此在调用类、对象的方法时,一定要加上"类" 或 "对象." 的形式。如果直接调用某个方法,这种形式属于调用函数。
此外,在构造方法中,self 参数(第一个参数)代表该构造方法正在初始化的对象。
class InConstructor:
def __init__(self):
foo = 0
self.foo = 6
print(InConstructor().foo) # 输出 6
需要说明的是,自动绑定的 self 参数并不依赖具体的调用方式,不管是以方法调用还是以函数调用的方式执行它,self 参数一样可以自动绑定。
class User:
def test(self):
print('self参数', self)
u = User()
u.test() # <__main__.User object at 0x00000000021F8240>
foo = u.test
foo() # <__main__.User object at 0x00000000021F8240>
当 self 参数作为对象的默认引用时,程序可以像访问普通变量一样来访问这个 self 参数,甚至可以把 self 参数当成实例方法的返回值。
class ReturnSelf:
def grow(self):
if hasattr(self, 'age'):
self.age += 1
else:
self.age = 1
return self
rs = ReturnSelf()
rs.grow().grow().grow()
print("rs 的 age 属性值是:", rs.age) # 输出3
如果在某个方法中把 self 参数作为返回值,则可以多次连续调用方法(只要该方法也返回 self),从而使得代码更加简洁。但是这种把 self 参数作为返回值的方法可能造成实际意义的模糊。
6.2.方法
方法是类或对象的行为特征的抽象,但 Python 的方法其实就是函数,其定义方式、调用方法和函数都非常相似,因此 Python 的方法并不仅仅是单纯的方法,它与函数也有莫大的关系。
6.2.1.类也能调用实例方法
Python 的类很大程度上是一个命名空间
def foo():
print("全局空间的 foo 方法")
bar = 20
class Bird:
def foo():
print("Bird 空间的 foo 方法")
bar = 200
foo()
print(bar)
Bird.foo()
print(Bird.bar)
如果使用类调用实例方法,那么该方法的第一个参数(self)怎么自动绑定呢?
class User:
def walk(self):
print(self, '正在慢慢地走')
User.walk() # 报错
调用 walk() 方法缺少传入的 self 参数,所以导致程序出错。这说明在使用类调用实例方法时,Python 不会自动为第一个参数绑定调用者。实际上也没法自动绑定,因此实例方法的调用者是类本身,而不是对象。
如果程序依然希望使用类来调用实例方法,则必须手动为方法的第一个参数传入参数值。
u = User() User.walk(u)
实际上,当通过 User 类调用 walk() 实例方法时,Python 只要求手动为第一个参数绑定参数值,并不要求必须绑定 User 对象,因此也可使用如下代码进行调用
User.walk('fkit')
如果按上面方式进行绑定,那么 'fkit' 字符串就会被传给 walk() 方法的第一个参数 self。
注意:Python 的类可以调用实例方法,但使用类调用实例方法时,Python 不会自动为方法的第一个参数 self 绑定参数值;程序必须显式地为第一个参数 self 传入方法调用者。这种调用方式被称为"未绑定方法"。
6.2.2.类方法与静态方法
实际上,Python 完全支持定义类方法,甚至支持静态方法。Python 的类方法和静态方法很相似,它们都推荐使用类来调用(其实也可使用对象来调用)。类方法和静态方法的区别在于:Python 会自动绑定类方法的第一个参数,类方法的第一个参数(通常建议参数名为 cls)会自动绑定到类本身;但对于静态方法则不会自动绑定。
使用 @classmethod 修饰的方法就是类方法;使用 @staticmethod 修饰的方法就是静态方法。
class Bird:
@classmethod
def fly(cls):
print('类方法 fly:', cls)
@staticmethod
def info(p):
print('静态方法 info:', p)
Bird.fly()
Bird.info('crazyit')
b = Bird()
b.fly()
b.info('fkit')
使用 @classmethod 修饰的方法是类方法,该类方法定义了一个 cls 参数,该参数会被自动绑定到 Bird 类本身,不管程序是使用类还是对象调用该方法,Python 始终都会将类方法的第一个参数绑定到类本身。
@staticmethod 定义了一个静态方法,程序同样既可使用类调用静态方法也可使用对象调用静态方法,不管用哪种方式调用,Python 都不会为静态方法执行自动绑定。
在使用 Python 编程时,一般不需要使用类方法或静态方法,程序完全可以使用函数来代替类方法或静态方法。但是在特殊的场景(比如使用工厂模式)下,类方法或静态方法也是不错的选择。
6.2.3.@函数装饰器
使用@符号引用已有的函数(比如@staticmethod、@classmethod)后,可用于修饰其他函数装饰被修饰的函数。那么我们能否可以开发自定义的函数装饰器呢?答案是肯定的。
当程序使用"@函数"(比如函数 A)装饰另一个函数(比如函数 B)时,实际上完成如下两步:
1.将被修饰的函数(函数 B)作为参数传给@符号引用的函数(函数 A)
2.将函数 B 替换(装饰)成第1步的返回值
从上面介绍不难看出,被"@函数"装饰的函数不再是原来的函数,而是被替换成一个新的东西
def funA(fn):
print('A')
fn()
return 'fkit'
@funA
def funB():
print('B')
print(funB) # 输出 fkit
上面程序使用 @funA 装饰 funB,这意味着程序要完成两步操作
1.将 funB 作为 funA() 的参数,也就是相当于执行 funA(funB)
2.将 funB 替换成第 1 步执行的结果,funA 执行完成后返回 fkit,因此 funB 就不再是函数,而是被替换成要给字符串
#上面代码的输出结果 A B fkit
被修饰的函数总是被替换成@符号所引用的函数的返回值,因此被装饰的函数会变成什么,完全由于@符号所引用的函数的返回值决定——如果@符号所引用的函数的返回值是函数,那么被修饰的函数在替换之后还是函数。
def foo(fn):
def bar(*args):
print("===1===", args)
n = args[0]
print("===2===", n * (n - 1))
print(fn.__name__)
fn(n * (n - 1))
print("*" * 15)
return fn(n * (n - 1))
return bar
@foo
def my_test(a):
print("==my_test函数==", a)
print(my_test) # <function foo.<locals>.bar at 0x00000000021FABF8>
my_test(10)
my_test(6, 5)
上面程序定义了一个装饰器函数 foo,该函数执行完成后并不是返回普通值,而是返回 bar 函数(这是关键),这意味着被@foo修饰的函数最终都会被替换成 bar 函数。
上面程序使用@foo 修饰 my_test() 函数,因此程序同样会执行 foo(my_test),并将 my_test 替换成 foo() 函数的返回值:bar 函数。
#输出内容 <function foo.<locals>.bar at 0x02E45858> ===1=== (10,) ===2=== 90 my_test ==my_test函数== 90 *************** ==my_test函数== 90 ===1=== (6, 5) ===2=== 30 my_test ==my_test函数== 30 *************** ==my_test函数== 30
通过@符号来修饰函数是 Python 的一个非常有用的功能,它既可以在被修饰函数的前面添加一些额外的处理逻辑(比如权限检查),也可以在被修饰函数的后面添加一些额外的处理逻辑(比如记录日志),还可以在目标方法抛出异常时进行一些修复工作......这种改变不需要修改被修饰函数的代码,只要增加一个修饰符即可。
提示:上面的介绍的这种在被修饰函数之前、之后、抛出异常后增加某种处理逻辑的方式,就是其他编程语言中的 AOP(Aspect Orient Programming,面向切面编程)。
下面例子示范了如何通过函数装饰器为函数添加权限检查的功能
def auth(fn):
def auth_fn(*args):
print("----模拟执行权限检查----")
fn(*args)
return auth_fn
@auth
def test(a, b):
print("执行 test 函数,参数 a:%s,参数 b:%s" % (a, b))
test(20, 15)
#输出内容如下
----模拟执行权限检查----
执行 test 函数,参数 a:20,参数 b:15
6.2.4.再论类命名空间
6.3.成员变量
在类体内定义的变量,默认属于类本身。如果把类当成类命名空间,那么该类变量其实就是定义在类命名空间内的变量。
6.3.1.类变量和实例变量
在类命名空间内定义的变量就属于类变量,Python 可以使用类来读取、修改类变量。
class Address:
detail = '广州'
post_code = '510660'
def info(self):
print(detail) # 报错
print(Address.detail)
print(Address.post_code)
print(Address.detail)
addr = Address()
addr.info()
Address.detail = '佛山'
Address.post_code = '460110'
addr.info()
对于类变量而言,它们就是属于在类命名空间内定义的变量,因此程序不能直接访问这些变量,程序必须使用类名来调用类变量。不管是在全局范围内还是函数内访问这些类变量,都必须使用类名进行访问。
#上面代码输出 广州 广州 510660 佛山 460110
实际上,Python 完全允许使用对象来访问该对象所属类的类变量(当然还是推荐使用类访问类变量)
class Record:
item = '鼠标'
date = '2016-06-16'
def info(self):
print('info 方法中:', self.item)
print('info 方法中:', self.date)
rc = Record()
print(rc.item)
print(rc.date)
rc.info()
实际上,程序通过对象访问类变量,其本质还是通过类名在访问类变量。
#上面代码输出 鼠标 2016-06-16 info 方法中:鼠标 info 方法中:2016-06-16
由于通过对象访问类变量的本质还是通过类名在访问,因此如果类发生了改变,当程序访问这些类变量时也会读到修改之后的值。
Record.item = '键盘' Record.date = '2016-08-18' rc.info() #输出内容 info 方法中:键盘 info 方法中:2016-08-18
从上面的输出结果可以看出,通过实例访问类变量的本质依然是通过类名在访问。
需要说明的是,Python 允许通过对象访问类变量,但如果程序通过对象尝试对类变量赋值,此时性质就变了——Python 是动态语言,赋值语句往往意味着定义新变量。
因此,如果程序通过对象对类变量赋值,其实不是对"类变量赋值",而是定义新的实例变量。
class Inventory:
item = '鼠标'
quantity = 2000
def change(self, item, quantity):
self.item = item
self.quantity = quantity
iv = Inventory()
iv.change('显示器', 500)
print(iv.item) # 显示器
print(iv.quantity) # 500
print(Inventory.item) # 鼠标
print(Inventory.quantity) # 2000
如果程序通过类修改了两个类变量的值,程序中 Inventory 的实例变量的值也不会受到任何影响。
Inventory.item = '类变量 item' Inventory.quantity = '类变量 quantity' print(iv.item) print(iv.quantity) #输出 显示器 500
同样,如果程序对一个对象的实例变量进行了修改,这种修改也不会影响类变量和其他对象的实例变量。
iv.item = '实例变量 item' iv.quantity = '实例变量 quantity' print(Inventory.item) print(Inventory.quantity) #输出 类变量 item 类变量 quantity
6.3.2.使用 property 函数定义属性
如果 Python 类定义了 getter、setter 等访问器方法,则可使用 property() 函数将它们定义成属性(相当于实例变量)。
property() 函数的语法格式
property(fget=None, fset=None, fdel=None, doc=None)
实例
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def setsize(self, size):
self.width, self.height = size
def getsize(self):
return self.width, self.height
def delsize(self):
self.width, self.height = 0, 0
size = property(getsize, setsize, delsize, '用于描述矩形大小的属性')
print(Rectangle.size.__doc__)
help(Rectangle.size)
rect = Rectangle(4, 3)
print(rect.size) # (4, 3)
rect.size = 9, 7
print(rect.width) # 9
print(rect.height) # 7
del rect.size
print(rect.width) # 0
print(rect.height) # 0
#输出
用于描述矩形大小的属性
Help on property:
用于描述矩形大小的属性
(4, 3)
9
7
0
0
在使用 property() 函数定义属性时,也可根据需要只传入少量的参数。使用 property() 函数定义了一个读写属性,该属性不能删除。
class User:
def __init__(self, first, last):
self.first = first
self.last = last
def getfullname(self):
return self.first + ',' + self.last
def setfullname(self, fullname):
first_last = fullname.rsplit(',')
self.first = first_last[0]
self.last = first_last[1]
fullname = property(getfullname, setfullname)
u = User('悟空', '孙')
print(u.fullname)
u.fullname = '八戒,猪'
print(u.first)
print(u.last)
#输出
悟空, 孙
八戒
猪
提示:在某些编程语言中,类似于这种 property 合成的属性被称为计算属性。这种属性并不真正存储任何状态,它的值其实是通过某种算法计算得到的。当程序对该属性赋值时,被赋的值也会被存储到其他实例变量中。
还可使用 @property 装饰器来修饰方法,使之成为属性。
class Cell:
@property
def state(self):
return self._state
@state.setter
def state(self, value):
if 'alive' in value.lower():
self._state = 'alive'
else:
self._state = 'dead'
@property
def is_dead(self):
return not self._state.lower() == 'alive'
c = Cell()
c.state = 'Alive'
print(c.state) # alive
print(c.is_dead) # False
上面程序中 @property 修饰了 state() 方法,这样就使得该方法变成了 state 属性的 getter 方法。如果只有该方法,那么 state 属性只是一个只读属性。
当程序使用 @property 修饰了 state 属性之后,又多出一个 @state.setter 装饰器,该装饰器用于修饰 state 属性的 setter 方法,这样 state 属性就有了 getter 和 setter 方法,state 属性就变成了读写属性。
@property 修饰了 is_dead 方法,该方法就会变成 is_dead 属性的 getter 方法。此为同样会多出一个 @is_dead.setter 装饰器,但程序并未使用该装饰器修饰 setter 方法,因此 is_dead 属性只是一个只读属性。
6.4.隐藏和封装
封装(Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
对一个类或对象实现良好的封装,可以达到以下目的
1.隐藏类的实现细节
2.让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。
3.可进行数据检查,从而有利于保证对象信息的完整性。
4.便于修改,提高代码的可维护性
为了实现良好的封装,需要从两个方面来考虑
1.将对象的属性和实现细节隐藏起来,不允许外部直接访问
2.把方法暴露出来,让方法来控制对这些属性进行安全的访问和操作
Python 并没有提供类似于其他语言的 private 等修饰符,因此 Python 并不能真正支持隐藏。
为了隐藏类中的成员,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.__name
age = property(getage, setage)
u = User()
u.name = 'fk' # 引发 ValueError 错误
上面程序将 User 的两个实例变量分别命名为 __name 和 __age,这两个实例变量就会被隐藏起来,这样程序就无法访问__name、__age变量,只能通过 setname()、getname()、getage()、setage() 这些访问器方法进行访问,而 setname()、setage() 会对用户设置的 name、age 进行控制,只有符合条件的 name、age 才允许设置。
代码修改
u.name = 'fkit' u.age = 25 print(u.name) # fkit print(u.age) # 25
从该程序可以看出封装的好处,程序可以将 User 对象的实现细节隐藏起来,程序只能通过暴露出来的 setname()、setage() 方法来改变 User 对象的状态,而这两个方法可以添加自己的逻辑控制,这种控制对 User 的修改始终是安全的。
上面的程序还定义了一个 __hide() 方法,这个方法默认是隐藏的。如果程序尝试执行如下代码:
u.__hide()
将会提示如下错误:
AttributeError: 'User' object has no attribute '__hide'
最后需要说明的是,Python 其实没有真正的隐藏机制,双下划线只是 Python 的一个小技巧:Python 会 "偷偷" 地改变以双下划线开头的方法名,会在这些方法名前添加单下划线和类名。因此上面的__hide() 方法其实可以按如下方式调用(通常并不推荐这么干)。
u._User__hide() # 输出 "示范隐藏的 hide 方法"
类似的是,程序也可通过为隐藏的实例变量添加下划线和类名的方式来访问或修改对象的变量。
u._User__name = 'fk' print(u.name) # 输出 fk
总结:Python 并没有提供真正的隐藏机制,所以 Python 类定义的所有成员默认都是公开的;如果程序希望将 Python 类中的某些成员隐藏起来,那么只要让该成员的名字以双下划线开头即可。即使通过这种机制实现了隐藏,其实也依然可以绕过去。
6.5.类的继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Python 的继承是多继承机制,即一个子类可以同时有多个父类。
6.5.1.继承的语法
Python 子类继承父类的语法是在定义子类时,将多个父类放在子类之后的圆括号里。语法格式如下:
class SubClass(SuperClass1, SuperClass2, ...):
# 类定义部分
从上面的语法格式来看,定义子类的语法非常简单,只需在原来的类定义后增加圆括号,并在圆括号中添加多个父类,即可表明该子类继承了这些父类。
如果在定义一个 Python 类时并未显示指定这个类的直接父类,则这个类默认继承 object 类。因此,object 类是所有类的父类,要么是其直接父类,要么是其间接父类。
实现继承的类被称为子类,被继承的类被称为父类,也被称为基类、超类。父类和子类的关系,是一般和特殊的关系。
由于子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。
从实际意义上看,子类是对父类的扩展,子类是一种特殊的父类。从这个意义上看,使用继承来描述子类和父类的关系是错误的,用扩展更恰当。
从子类的角度看,子类扩展(extend)了父类;但从父类的角度来看,父类派生(derive)出子类。也就是说,扩展和派生描述的是同一个动作,只是观察角度不同而已。
class Fruit:
def info(self):
print('我是一个水果!重%g克' % self.weight)
class Food:
def taste(self):
print('不同食物的口感不同')
class Apple(Fruit, Food):
pass
a = Apple()
a.weight = 5.6
a.info()
a.taste()
6.5.2.关于多继承
大部分面向对象的编程语言(除了 c++)都只支持单继承,而不支持多继承,这是由于多继承不仅增加了编程的复杂度,而且很容易导致一些莫名地错误。
Python 虽然在语法上明确支持多继承,但通常推荐:如果不是很有必要,则尽量不要使用多继承,而是使用单继承,这样可以保证编程思路更清晰,而且可以避免很多麻烦。
当一个子类有多个直接父类时,该子类会继承得到所有父类的方法。现在的问题是:如果多个父类中包含了同名的方法,此时会发生什么呢?此时排在前面的父类中的方法会 "遮蔽" 排在后面的父类中的同名方法。
class Item:
def info(self):
print('Item 中方法:', '这是一个商品')
class Product:
def info(self):
print('Product 中方法:', '这是一个工业产品')
class Mouse(Item, Product):
pass
m = Mouse()
m.info() # 输出 "Item 中方法:这是一个商品"
6.5.3.重写父类的方法
子类扩展了父类,子类是一种特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的方法。但有一种情况例外:子类需要重写父类的方法。
class Bird:
def fly(self):
print('我在天空里自由自在地飞翔')
class Ostrich(Bird):
def fly(self):
print('只能在地上奔跑...')
os = Ostrich()
os.fly() # 输出 "只能在地上奔跑..."
这种子类包含与父类同名的方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
6.5.4.使用未绑定方法调用被重写的方法
如果在子类中调用重写之后的方法,Python 总是会执行子类的方法,不会执行父类中被重写的方法。如果需要在子类中调用父类中被重写的实例方法,那该怎么办呢?
读者别忘了,Python 类相当于类空间,因此 Python 类中的方法本质上相当于类空间内的函数。所以,即使是实例方法,Python 也允许通过类名调用。区别在于:在通过类名调用实例方法时,Python 不会为实例方法的第一个参数 self 自动绑定参数值,而是需要程序显示绑定第一个参数 self。这种机制被称为未绑定方法。
通过使用未绑定方法即可在子类中再次调用父类中被重写的方法。
class BaseClass:
def foo(self):
print('父类中定义的 foo 方法')
class SubClass:
def foo(self):
print('子类重写父类中的 foo 方法')
def bar(self):
print('执行 bar 方法')
self.foo()
BaseClass.foo(self)
sc = SubClass()
sc.bar()
6.5.5.使用 super 函数调用父类的构造方法
Python 的子类也会继承得到父类的构造方法,如果子类有多个直接父类,那么排在前面的父类的构造方法会被优先使用。
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))
class Manager(Employee, Customer):
pass
m = Manager(25000)
m.work() # 输出 "普通员工正在写代码,工资是:25000"
m.info() # 引发错误
引发的错误的原因是,由于程序在使用 Employee 类的构造方法创建 Manager 对象时,程序并未初始化 Customer 对象所需的两个实例变量:favorite 和 address,因此程序引发错误。
如果改成如下:
class Manager(Customer, Employee):
pass
m = Manager('IT 产品', '广州')
m.info() # 输出 "我是一个顾客,我的爱好是: IT 产品,地址是 广州"
m.work() # 引发错误
引发的错误的原因是,上面的代码为 Manager 的构造方法传入两个参数,这明显是调用从 Customer 类继承得到的两个构造方法,此时程序将可以初始化 Customer 类中的 favorite 和 address 实例变量,但它又不能初始化 Employee 类中的 salary 实例变量。因此代码报错。
为了让 Manager 能同时初始化两个父类中的实例变量,Manager 应该定义自己的构造方法——就是重写父类的构造方法。Python 要求:如果子类重写了父类的构造方法,那么子类的构造方法必须调用父类的构造方法。
子类的构造方法调用父类的构造方法有两种方式
1.使用未绑定方法,这种方式很容易理解。因为构造方法也是实例方法,当然可以通过这种方式来调用。
2.使用 super() 函数调用父类的构造方法
在交互式解释器中输入 help(super) 查看 super() 函数的帮助信息,可以看到如下输出信息。

从上面介绍可以看出,super 其实是一个类,因此调用 super() 的本质就是调用 super 类的构造方法来创建 super 对象。
从上面的帮助信息可以看到,使用 super() 构造方法最常用的做法就是不传入任何参数(这种做法与 super(type, obj) 的效果相同),然后通过 super 对象的方法既可调用父类的实例方法,也可调用父类的类方法。在调用父类的实例方法时,程序会完成第一个参数 self 的自动绑定。在调用类方法时,程序会完成第一个参数 cls 的自动绑定。
class Manager(Employee, Customer):
# 重写父类的构造方法
def __init__(self, salary, favorite, address):
print('--Manager 的构造方法--')
# 通过 super() 函数调用父类的构造方法
super().__init__(salary)
# 效果同上
# super(Manager, self).__init__(salary)
# 使用未绑定方法调用父类的构造方法
#Employee.__init__(self, salary)
Customer.__init__(self, favorite, address)
m = Manager(25000, 'IT产品', '广州')
m.work()
m.info()
代码分别示范了两种方式调用父类的构造方法。通过这种方式,Manager 类重写了父类的构造方法,并在构造方法中显式调用了父类的两个构造方法执行初始化,这样两个父类中的实例变量都能被初始化。
# 输出内容 --Manager 的构造方法-- 普通员工正在写代码,工资是: 25000 我是一个顾客,我的爱好是: IT产品,地址是 广州
6.6.Python 的动态性
Python 是动态语言,动态语言的典型特征就是:类、对象的属性、方法都可以动态增加和修改。
6.6.1.动态属性与 __slots__
前面介绍了为对象动态添加方法,但是所添加的方法只是对当前对象有效,如果希望为所有实例都添加方法,则可通过为类添加方法来实现。
class Cat:
def __init__(self, name):
self.name = name
def walk_func(self):
print('%s 慢慢地走过一片草地' % self.name)
d1 = Cat('Garfield')
d2 = Cat('Kitty')
#d1.walk() # AttributeError
# 为 Cat 动态添加 walk()方法,该方法的第一个参数会自动绑定
Cat.walk = walk_func
d1.walk()
d2.walk()
上面程序定义了一个 Cat 类,该 Cat 类只定义了一个构造方法,并未提供任何方法。因此调用 d1.walk() 方法会出现异常:Cat 类并没有 walk() 方法。
程序中为 Cat 动态添加了 walk() 方法,为类动态添加方法时不需要使用 MethodType 进行包装,该函数的第一个参数会自动绑定。为 Cat 动态添加 walk() 方法之后,Cat 类的连个实例 d1、d2 都具有了 walk() 方法。
Python 的这种动态性固然有其优势,但也给程序带来了一定的隐患:程序定义好的类,完全有可能在后面被其他程序修改,这就带了一些不确定性。如果程序要限制为某个类动态添加属性和方法,则可通过__slots__属性来指定。
__slots__属性的值是一个元组,该元组的所有元素列出了该类的实例允许动态添加的所有属性名和方法名(对于 Python 而言,方法相当于属性值为函数的属性)。
class Dog:
__slots__ = ('walk', 'age', 'name')
def __init__(self, name):
self.name = name
def test(self):
print('预先定义的 test 方法')
d = Dog('Snoopy')
from types import MethodType
# 只允许为实例动态添加 walk、age、name 这三个属性或方法
d.walk = MethodType(lambda self:print('%s 正在慢慢地走' % self.name), d)
d.age = 5
d.walk()
d.foo = 30 # AttributeError
如果程序尝试为 Dog 对象添加其他额外属性,程序就会引发 AttributeError 错误。
需要说明的是:__slots__属性并不限制通过类来动态添加属性和方法,下面是合法的
Dog.bar = lambda self:print('abc')
此外,__slots__属性指定的限制只对当前类的实例起作用,对该类派生出来的子类是不起作用的。
class GunDog(Dog):
def __init__(self, name):
super().__init__(name)
pass
gd = GunDog('Puppy')
# 完全可以为 GunDog 实例动态添加属性
gd.speed = 99
print(gd.speed)
如果要限制子类的实例动态添加属性和方法,则需要在子类中也定义__slots__属性,这样,子类的实例允许动态添加属性和方法就是子类的__slots__元组加上父类的__slots__元组的和。
6.6.2.使用 type() 函数定义类
type() 可以查看变量的类型
class Role:
pass
r = Role()
print(type(r)) # <class '__main__.Role'>
print(type(Role)) # <class 'type'>
从 Python 解释器的角度来看,当程序使用 class 定义 Role 类时,也可理解为定义了一个特殊的对象(tpye 类的对象),并将该对象赋值给 Role 变量。因此,程序使用 class 定义的所有类都是 type 类的实例。
实际上 Python 完全允许使用 type() 函数(相当于 type 类的构造器函数)来创建 type 对象,又由于 type 类的实例就是类,因此 Python 可以使用 type() 函数来动态创建类。
def fn(self):
print('fn 函数')
Dog = type('Dog', (object,), dict(walk=fn, age=6))
d = Dog()
print(type(d)) # <class '__main__.Dog'>
print(type(Dog)) # <class 'type'>
d.walk() # fn 函数
print(Dog.age) # 6
使用 type() 定义了一个 Dog 类。
在使用 type() 定义类时可指定三个参数
1.参数一:创建的类名
2.参数二:该类继承的父类集合。由于 Python 支持多继承,因此此处使用元组指定它的多个父类。即使实际只有一个父类,也需要使用元组语法(必须要多一个逗号)。
3.参数三:该字典对象为该类绑定的类变量和方法。其中字典的 key 就是类变量或方法名,如果字典的 value 是普通值,那就代表类变量;如果字典的 value 是函数,则代表方法。
使用 type() 函数定义的类与直接使用 class 定义的类并没有任何区别。事实上,Python 解释器在执行使用 class 定义的类时,其实依然是使用 type() 函数来创建类的。因此,无论通过哪种方式定义类,程序最终都是创建一个 type 的实例。
6.6.3.使用 metaclass
如果希望创建一批类全部具有某种特征,则可通过 metaclass 来实现。使用 metaclass 可以在创建类时动态修改类定义。
为了使用 metaclass 动态修改类定义,程序需要先定义 metaclass,metaclass 应该继承 type 类,并重写__new__() 方法。
class ItemMetaClass(type):
# cls 代表被动态修改的类
# name 代表被动态修改的类名
# bases 代表被动态修改的类的所有父类
# attrs 代表被动态修改的类的所有属性,方法组成的字典
def __new__(cls, name, bases, attrs):
attrs['cal_price'] = lambda self:self.price * self.discount
return type.__new__(cls, name, bases, attrs)
mataclass 类的 __new__ 方法的作用是:当程序使用 class 定义新类时,如果指定了 metaclass,那么 metaclass 的 __new__ 方法就会被自动执行。
class Book(metaclass=ItemMetaClass):
__slots__ = ('name', 'price', '_discount')
def __init__(self, name, price):
self.name = name
self.price = price
@property
def discount(self):
return self._discount
@discount.setter
def discount(self, discount):
self._discount = discount
class CellPhone(metaclass=ItemMetaClass):
__slots__ = ('price', '_discount')
def __init__(self, price):
self.price = price
@property
def discount(self):
return self._discount
@discount.setter
def discount(self, discount):
self._discount = discount
上面程序定义了 Book 和 CellPhone 两个类,在定义这两个类时都指定了 metaclass 信息,因此当 Python 解释器在创建这两个类时,ItemMetaClass 的 __new__ 方法就会被调用,用于修改这两个类。
ItemMetaClass 类的 __new__ 方法会为目标类动态添加 cal_price 方法,因此,虽然在定义 Book、CellPhone 类时没有定义 cal_price() 方法,但这两个类依然有 cal_price() 方法。
b = Book('疯狂 Python 讲义', 89)
b.discount = 0.76
print(b.cal_price()) # 67.64
cp = CellPhone(2399)
cp.discount = 0.85
print(cp.cal_price()) # 2039.1499999999999
从上面的运行结果来看,通过使用 metaclass 可以动态修改程序中的一批类,对它们集中进行某种修改。这个功能在开发一些基础框架时非常有用,程序可以通过使用 metaclass 为某一批需要具有通用功能的类添加方法。
6.7.多态
对于弱类型的语言来说,变量并没有声明类型,因此同一个变量完全可以在不同的时间引用不同的对象。当同一个变量在调用同一个方法时,完全可能呈现出多种行为(具体呈现出哪种行为由该变量所引用的对象决定),这就是所谓的多态(Polymorphism)。
6.7.1.多态性
class Bird:
def move(self,field):
print('鸟在%s上自由地飞翔' % field)
class Dog:
def move(self,field):
print('狗在%s里飞快地奔跑' % field)
x = Bird()
x.move('天空')
x = Dog()
x.move('草地')
#输出
鸟在天空上自由地飞翔
狗在草地里飞快地奔跑
结果可以看出,同一个变量 x 在执行同一个 move() 方法时,由于 x 指向的对象不同,因此它呈现出不同的行为特征,这就是多态。
看到这里,是否感到失望?多态不就是创建对象、调用方法吗?
实际上,多态是一种非常灵活的编程机制。
class Canvas:
def draw_pic(self, shape):
print('--开始绘画--')
shape.draw(self)
从上面的代码看出,Canvas 的 draw_pic() 方法需要传入一个 shape 参数,该方法就是调用 shape 参数的 draw() 方法将自己绘制到画布上。
从上面的程序来看,Canvas 的 draw_pic() 传入的参数对象只要带一个 draw() 方法就行,至于该方法具有何种行为(到底执行怎样的绘制行为),这与 draw_pic() 方法是完全分离的,这就为编程增加了很大的灵活性。
class Rectangle:
def draw(self, canvas):
print('在%s上绘制矩形' % canvas)
class Triangle:
def draw(self, canvas):
print('%s上绘制三角形' % canvas)
class Circle:
def draw(self, canvas):
print('%s上绘制圆形' % canvas)
c = Canvas()
c.draw_pic(Rectangle())
c.draw_pic(Triangle())
c.draw_pic(Circle())
#输出内容
--开始绘画--
在<__main__.Canvas object at 0x02EE2490>上绘制矩形
--开始绘画--
<__main__.Canvas object at 0x02EE2490>上绘制三角形
--开始绘画--
<__main__.Canvas object at 0x02EE2490>上绘制圆形
从上面这个例子可以体会到 Python 多态的优势。当程序涉及 Canvas 类的 draw_pic() 方法时,该方法所需的参数是非常灵活的,程序为该方法传入的参数对象只要具有指定方法就行,至于该方法呈现怎样的行为特征,则完全取决于对象本身,这大大提高了 draw_pic() 方法的灵活性。
6.7.2.检查类型
Python 提供了如下两个函数来检查类型
1.issubclass(cls, class_or_tuple):检查 cls 是否为后一个类或元组包含的多个类中任意类的子类
2.isinstance(obj, class_or_tuple):检查 obj 是否为后一个类或元组包含的多个类中任意类的对象
通过使用上面的两个函数,程序可以方便地先执行检查,然后才调用方法,这样可以保证程序不会出现意外情况。
hello = "hello"
print('"Hello"是否是 str 类的实例:', isinstance(hello, str)) # True
print('"Hello"是否 object 类的实例:', isinstance(hello, object)) # True
print('str 是否是 object 类的子类:', issubclass(str, object)) # True
print('"Hello"是否是 tuple 类的实例:', isinstance(hello, tuple)) # False
print('str 是否是 tuple 类的实例:', issubclass(str, tuple)) # False
my_list = [2, 4]
print('[2, 4]是否是 list 类的实例:', isinstance(my_list, list)) # True
print('[2, 4]是否是 object 类及其子类的实例:', isinstance(my_list, object)) # True
print('list 是否是 object 类的子类:', issubclass(list, object)) # True
print('[2, 4]是否是 tuple 类的实例:', isinstance([2, 4], tuple)) # False
print('list 是否是 tuple 类的子类:', issubclass(list, tuple)) # False
此外,Python 为所有类都提供了一个 __bases__ 属性,通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。
Python 还为所有类都提供了一个 __subclasses__()方法,通过该方法可以查看该类的所有直接子类,该方法返回该类的所有子类组成的列表。
6.8.枚举类
在某些情况下,一个类的对象是有限且固定的,例如季节类,它只有4个对象。这种实例有限且固定的类,在 Python 中被称为枚举类。
6.8.1.枚举入门
程序有两种方式来定义枚举型
1.直接使用 Enum 列出多个枚举值来创建枚举类。
2.通过继承 Enum 基类来派生枚举类
import enum
Season = enum.Enum(Season, ('SPRING', 'SUMMER', 'FALL', 'WINTER'))
第一个参数是枚举类的类名;第二个参数是一个元组,用于列出所有枚举值。
定义后程序可以直接通过枚举值进行访问,这些枚举值都是该枚举的成员,每个成员都有 name、value 两个属性,其中 name 属性值为该枚举值的变量名,value 代表该枚举值的序号(序号通常从 1 开始)。
print(Season.SPRING) # Season.SPRING print(Season.SPRING.name) # SPRING print(Season.SPRING.id) # 1 # 根据枚举变量名访问枚举对象 print(Season['SUMMER']) # Season.SUMMER print(Season(3)) # Season.FALL
此外,Python 还为枚举提供了一个 __members__ 属性,该属性返回一个 dict 字典,字典包含了该枚举的所有枚举实例。程序可通过遍历__members__属性来访问枚举的所有实例。
for name, member in Season.__members__.items():
print(name, '=>', member, ',', member.value)
如果要定义更复杂的枚举,则可通过继承 Enum 来派生枚举类,在这种方式下程序就可以为枚举额外定义方法了。
import enum
class Orientiation(enum.Enum):
EAST = '东'
SOUTH = '南'
WEST = '西'
NORTH = '北'
def info(self):
print('这是一个代表方向[%s]的枚举' % self.value)
print(Orientiation.SOUTH)
print(Orientiation.SOUTH.value)
print(Orientiation['WEST'])
print(Orientiation('南'))
Orientiation.EAST.info()
for name, member in Orientiation.__members__.items():
print(name, '=>', member, ',', member.value)
#输出内容
Orientiation.SOUTH
南
Orientiation.WEST
Orientiation.SOUTH
这是一个代表方向[东]的枚举
EAST => Orientiation.EAST , 东
SOUTH => Orientiation.SOUTH , 南
WEST => Orientiation.WEST , 西
NORTH => Orientiation.NORTH , 北
6.8.2.枚举的构造器
枚举也是类,因此枚举也可以定义构造器。为枚举定义构造器之后,在定义枚举实例时必须为构造器参数设置值。
import enum
class Gender(enum.Enum):
MALE = '男', '阳刚之力'
FEMALE = '女', '柔顺之美'
def __init__(self, cn_name, desc):
self._cn_name = cn_name
self._desc = desc
@property
def desc(self):
return self._desc
@property
def cn_name(self):
return self._cn_name
print('FEMALE 的 name:', Gender.FEMALE.name)
print('FEMALE 的 value:', Gender.FEMALE.value)
print('FEMALE 的 cn_name:', Gender.FEMALE.cn_name)
print('FEMALE 的 desc:', Gender.FEMALE.desc)
# 输出
FEMALE 的 name: FEMALE
FEMALE 的 value: ('女', '柔顺之美')
FEMALE 的 cn_name: 女
FEMALE 的 desc: 柔顺之美
MALE 枚举指定的 value 是 '男' 和 '阳刚之力' 这两个字符串,其实它们会被自动封装成元组后传给 MALE 的 value 属性;而且此处传入的 "男" 和 "阳刚之力" 这两个参数值正好分别传给 cn_name 和 desc 两个参数。简单地说,枚举的构造器需要几个参数,此处就必须指定几个值。
6.9.本章小结
posted on 2020-01-29 11:42 herisson_pan 阅读(12) 评论(0) 收藏 举报
浙公网安备 33010602011771号