Python:类和对象

在设计之初,Python就被设计成支持面向对象的编程语言,因此Python完全能以面向对象的方式编程。而且Python的面向对象非常简单,它不像其他面向对象语言提供了大量繁杂的面向对象特征,它致力于提供简单、够用的语法功能。

正是因为如此,在Python中创建一个类和对象都和容易。Python支持面向对象的三大特征:封装、继承、多态,子类继承父类同样可以继承到父类的变量和方法。

6.1 类和对象

类是面向对象的重要内容,可以把类当成一种自定义类型,可以使用类来定义变量,也可以使用类来创建对象。

6.1.1 定义类

在面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念:对象才是一种具体存在的实体。从这个意义上看,日常所说的人,其实都是人的对象,而不是人类

Python定义类的简单语法如下:

class 类名:
    执行语句...
    零个到多个类变量
    零个到多个类方法

类名只要是一个合法的标识符即可,但这仅仅满足的是Python的语法要求;如果从程序的可读性方面来看,Python的类名必须由一个或多个有意义的单词连缀而成的,每个单词首字母大写,其他字母小写,单词与单词之间不要使用任何分隔符。

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

Python的类定义由类头(指class关键字和类名部分)和统一缩进的类体构成,在类体中最重要的两个成员就是类成员和方法。如果不为类定义任何类变量和方法,那么这个类就相当于一个空类,如果空类不需要其他可执行语句,则可使用pass语句作为占位符。

class Empty:

    pass

通常来说,空类没有太大的意义。

类中各成员之间的定义顺序没有任何影响,各成员之间可以相互调用。

Python类所包含的最重要的两个成员就是变量和方法,其中类变量属于类本身,用于定义该类自身所包含的状态数据;而实例变量则属于该类的对象,用于定义对象所包含的状态数据;方法则用于定义该类的对象的行为或功能的实现。

Python是一门动态语言,因此它的类所包含的类变量可以动态增加和删除——程序在类体中为新变量赋值就是增加类变量,程序也可在任何地方为已有的类增加变量;程序可通过del语句删除已有类的类变量。

类似的是,Python对象的实例变量也可以动态增加或删除——只要对新实例变量赋值就是增加实例变量,因此程序可以在任何地方为已有的对象增加实例对象;程序可通过del语句删除已对象的实例变量。

在类中定义的方法默认是实例方法,定义实例方法的方法与定义函数的方法基本相同,只是实例方法的第一个参数会被绑定到方法的调用者(该类的实例)——因此实例方法至少应该定义一个参数,该参数通常会被命名为self。

注意:实例方法的第一个参数并不一定要叫self,其实完全可以叫任意参数名,只是约定俗成地把该参数命名为self,这样具有最好地可读性。

在实例方法中有一个特别的方法:__init__,这个方法被称为构造方法。构造方法用于构造该类的对象,Python通过调用构造方法返回该类的对象(无须使用new)。

提示:Python中很多以双下划线开头、双下划线结尾的方法,都具有特殊的意义。

构造方法是一个类创建对象的根本途径,因此Python还提供了一个功能:如果开发者没有为该类定义任何构造方法,那么Python会自动为该类定义一个只包含一个self参数的默认构造方法。

class Person:
    '这是学习Python定义的一个Person类'
    #下面定义一个类变量
    hair='black'
    def __init__(self,name='Charlie',age=8):
        #下面为Person对象增加两个实例变量
        self.name=name
        self.age=age
    #下面定义一个say方法
    def say(self,content):
        print(content)

上面的Person类代码定义了一个构造方法,该构造方法只是方法名比较特殊:__init__,该方法的第一个参数同样是self,被绑定到构造方法初始化的对象。

与函数类似的是,Python也允许为类定义说明文档,该文档同样被放在类说明之后、类体之前,上面程序已经写出。

在定义类之后,接下来即可使用该类了。Python的类大致有如下作用:

  1. 定义变量;
  2. 创建对象;
  3. 派生子类。

6.1.2 对象的产生

创建对象的根本途径是构造方法,调用某个类的构造方法即可创建这个类的对象,Python无需使用new调用的构造方法。

#调用Person类的构造方法,返回一个Person对象
#将该Person对象赋值给p变量
p=Person()

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

  1. 操作对象的实例变量(包括访问实例变量的值、添加实例变量、删除实例变量)。
  2. 调用对象的方法。

对象访问方法或变量的语法是:对象.变量|方法(参数)。在这种方式中,对象是主调者,用于该对象的变量或方法。

#输出p的name、age实例变量
print(p.name,p.age)#Charlie 8
#访问p的name实例变量,直接为该实例变量赋值
p.name='李刚'
#调用p的say()方法,在声明say()方法时定义了两个参数
#但第一个形参self是自动绑定的,因此调用该方法的时候只需为第二个形参指定一个值
p.say('Python语言很简单,学习很容易!')#Python语言很简单,学习很容易!
#再次输出p的name、age实例变量
print(p.name,p.age)#李刚 8

大部分时候,定义一个类就是为了重复构建该类的对象,同一个类的多个对象具有相同的特征,而类则定义了多个对象的共同特征。从某个角度来看,类定义的是多个对象的特征,因此类不是一个具有存在的实例,对象才是一个具体存在的实体。完全可以这么说:你不是人这个类,我也不是人这个类,我们都只是人的对象。

6.1.3 对象的使用

由于Python是动态语言,因此程序完全可以为p对象动态增加实例变量——只要为它的新变量赋值即可;也可以动态删除实例变量——使用del语句即可删除。

#为p对象增加一个skills实例变量

p.skills=['programming','swimming']

print(p.skills)#['programming', 'swimming']

#删除p对象的name实例变量

del p.name

#再次访问p的name实例变量

print(p.name) #AttributeError: 'Person' object has no attribute 'name'

程序删除了p对象的name实例变量,当程序再次访问print(p.name)时就会导致错误。

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

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

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

如果希望动态增加的方法也能自动绑定到第一个参数,则可借助于types模块下的MethodType进行包装。

def intro_func(self,content):
    print("我是一个人,信息为:%s"%content)
#导入MethodType
from types import MethodType
#使用MethodType对intro_func进行包装,将该函数的第一个参数绑定为p
p.intro=MethodType(intro_func,p)
#第一个参数与已经绑定了,无需传入
p.intro("生活在别处")#我是一个人,信息为:生活在别处

通过MethodType包装intro_func函数之后(包装时指定了将该函数的第一个参数绑定为p),为p对象动态增加的intro()方法的第一个参数已经绑定,因此程序通过p调用intro()方法时无需传入第一个参数——就像定义类时已经定义了intro()方法一样。

6.1.4 实例方法和自动绑定self

对于类中定义的实例方法,Python会自动绑定方法的第一个参数(通常建议将该参数命名为self),第一个参数总是指向调用该方法的对象。根据第一个参数出现位置的不同,第一个参数所绑定的对象略有区别。

  1. 在构造方法中引用该构造方法正在初始化的对象。
  2. 在普通实例方法中引用调用该方法的对象

由于实例方法(包括构造方法)的第一个self参数会自动绑定,因此程序在普通实例方法、构造方法时不需要为第一个参数传值。

self参数(自动绑定的第一个参数)最大的作用就是引用当前方法的调用者,比如前面介绍的在构造方法中通过self为该对象增加实例变量。也可以在一个实例方法中访问该类的另一个实例方法或变量。假设定义了一个Dog类,这个Dog对象的run()方法需要调用它的jump()方法,此时可通过self参数作为jump()方法的调用者。

方法的第一个参数所代表的对象是不确定的,但它的类型是确定的——它所代表的只能使当前类的实例;只要当前这个方法被调用时,它所代表的对象才被确定下来——谁在调用这个方法,方法的第一个参数就代表谁。

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

上面代码的run()方法中的self代表该方法的调用者:谁在调用run()方法,那么self就代表谁。因此该方法表示:当一个Dog对象调用run()方法时,run()方法需要依赖他自己的jump()方法。

在现实世界里,对象的一个方法依赖另一个方法的情形非常常见,例如:吃饭方法依赖拿筷子方法,写程序方法依赖敲键盘方法,这种依赖都是同一个对象的两个方法之间的依赖。当Python对象的一个方法调用另一个方法时,不可以省略self。也就是说,将上面的run()方法改为如下形式不正确的。

#定义一个run()方法,run()方法需要借助jump()方法
def run(self):
    #省略self,下面代码会报错
    jump()
    print("正在执行run方法")

提示:从Python语言的设计来看,Python的类、对象有点类似一个命名空间,因此在调用类、对象的方法时,一定要加上“类.”或“对象.”的形式。如果直接调用某个方法,这种形式属于调用函数。

此外,在构造方法中,self参数(第一个参数)代表构造方法正在初始化对象。

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

在InConstrutor的构造方法中,self参数总是引用该构造方法正在初始化的对象。需要说明的是,自动绑定的self参数并不依赖具体的调用方式,不管是以方法调用还是以函数调用的方式执行它,self参数一样可以自动绑定。

class User:
    def test(self):
        print("self参数:",self)

u=User()
#以方法形式调用test()方法
u.test()#self参数: <__main__.User object at 0x000001D4D00D0B90>
#将User对象的test方法赋值给foo变量
foo=u.test
#通过foo(函数形式)调用test()方法
foo()#self参数: <__main__.User object at 0x0000023493310A70>

当self参数作为对象默认引用时,程序可以像访问普通变量一样访问这个self参数,甚至可以把self参数当作实例方法的返回值。

class ReturnSelf:
    def grow(self):
        if hasattr(self,'age'):
            self.age+=1
        else:
            self.age=1
        #return self返回调用该方法的对象
        return self
rs=ReturnSelf()
#可以连续调用同一个方法
rs.grow().grow().grow()
print('rs的属性值是:',rs.age)#rs的属性值是: 3

这种把self参数当作返回值的方法可能会造成实际意义的模糊。

注意:使用self参数作为方法的返回值可以让代码更加简洁,但可能造成实际意义的模糊。

6.2 方法

方法是类或对象的行为特征的抽象,但Python的方法其实也是函数,其定义方式、调用方式和函数都非常相似,因此Python的方法并不仅仅是单纯的方法,它与函数也有莫大的关系。

6.2.1 类也能调用实例方法

前面讲过,在Python的类体中定义的方法默认都是实例方法,前面也示范了通过对象来调用实例方法。

但要提醒的是,Python的类在很大程度上是一个命名空间——当程序在类中定义变量、定义方法时,与前面介绍的定义变量、定义函数其实并没有太大的不同。

#定义全局空间的foo函数
def foo():
    print("foo全局空间函数")
#定义全局空间的bar变量
bar=20
class Bird:
    #定义Bird空间的foo函数
    def foo():
        print("Bird空间的foo方法")
    #定Bird空间的bar变量
    bar=200
#调用全局空间的函数和变量
foo()
print(bar)
#调用Bird空间的函数和函数
Bird.foo()
print(Bird.bar)
"""
foo全局空间函数
20
Bird空间的foo方法
200
"""

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

class User:
    def walk(self):
        print(self,'正在慢慢地走')
#通过类调用实例方法
User.walk()
"""
TypeError: User.walk() missing 1 required positional argument: 'self'
"""

在使用类调用实例方法时,Python不会自动为第一个参数绑定调用者。实际上也没法自动绑定,因此实例方法地调用者是类本身,而不是对象。

如果程序依然希望使用类来调用实例方法,则必须手动传入参数值。

u=User()
#显式地为方法地第一个参数绑定参数
User.walk(u)
"""
<__main__.User object at 0x000001C2C3A10B90> 正在慢慢地走
"""

实际上,当通过User类调用walk实例方法时,Python只要求手动为第一个参数绑定参数值,并不要求必须绑定User对象,因此也可以如下调用:

#显式地为方法的第一个参数绑定fkit字符串参数
User.walk('fkit')
"""
fkit 正在慢慢地走
"""

注意:Python的类可以调用实例方法,当使用类调用实例方法时,Python不会自动为方法的第一个参数self绑定参数值;程序必须显式地为第一个参数self传入方法调用者。这种调用方式被称为“未绑定方法”。

6.2.2 类方法与静态方法

实际上,Python完全支持定义类方法,甚至支持定义静态方法。Python的方法和静态方法很相似,它们都推荐使用类来调用(其实也可使用对象来调用)。类方法和静态方法的区别在于:Python会自动绑定类方法的第一个参数,类方法的第一个参数(通常建议参数名为cls)会自动绑定到类本身;但对于静态方法则不会自动绑定。

使用@classmethod修饰的方法就是类方法;使用@staticmethod修饰的方法就是静态方法。

class Bird:
    #使用@classmethod修饰的方法为类方法
    @classmethod
    def fly(cls):
        print("fly类方法:",cls)
    #使用@staticmethod修饰的方法时静态方法
    @staticmethod
    def info(p):
        print("静态方法info:",p)
#调用类方法,Bird类自动绑定到第一个参数
Bird.fly()
#调用静态方法,不会自动绑定,因此程序必须手动绑定第一个参数
Bird.info('crazyit')
#创建Bird对象
b=Bird()
#使用对象调用fly()类方法,其实依然是使用类调用
b.fly()
#使用对象调用info()静态方法,其实依然是使用类调用
#因此程序必须为第一个参数执行绑定
b.info('fkit')
"""
fly类方法: 
静态方法info: crazyit
fly类方法: 
静态方法info: fkit
"""

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

6.2.3 @函数装饰器

前面介绍的@staticmethod和@classmethod的本质都是函数装饰器,其中staticmethod和classmethod都是Python内置的函数。

使用@符号引用已有的函数后,可用于修饰其他函数装饰被修饰的函数。那么我们能否可以自己开发自定义的函数装饰器呢?那是肯定可以的。

当程序使用“@函数”装饰另一个函数时,实际上完成如下两步:

  1. 将被修饰的函数作为参数传给@符号引用的函数。
  2. 将函数B替换(装饰)成第一步的返回值

不难看出,“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西。

def funA(fn):
    print("A")
    fn()#执行传入的fn参数
    return 'fkit'
"""
下面的装饰效果相当于funA(funB)
funB将会被替换成该语句的返回值
由于funA函数返回fkit,因此funB就是fkit
"""
@funA
def funB():
    print('B')
print(funB)#fkit
"""
A
B
fkit
"""

上面程序使用@funA修饰时funB,这意味着要完成两步操作:

  1. 将funB作为funA的参数,也就是上面程序@funA相当于执行funA(funB)。
  2. 将funB替换成funA()的参数,也就是上面funA()执行完成后返回fkit,因此funB不再是函数,而是被替换成一个字符串。

这个函数装饰器导致被修饰的函数变成字符串,那么函数装饰器有什么用?被修饰的函数总是被替换成@符号所引用的函数的返回值,因此被修饰比的函数会变成什么,完全由@符号所引用的函数的返回值确定——如果@符号所引用的函数的返回值是函数,那么被修饰的函数再替换后还是函数。

def foo(fn):
    #定义一个嵌套函数
    def bar(*args):
        print("===1===",args)
        n=args[0]
        print("===2===",n*(n-1))
        #查看传给foo函数的fn函数
        print(fn.__name__)
        fn(n*(n-1))
        print("*"*15)
        return fn(n*(n-1))
    return bar
"""
下面的装饰效果相当于foo(my_test)
my_test将会被替换成该语句的返回值
由于foo()函数返回bar函数,因此funB就是bar
"""
@foo
def my_test(a):
    print("===my_test函数===",a)
#打印my_test()函数,将看到实际上是bar函数
print(my_test)
#下面代码看上去调用my_test()函数,实际上是调用bar()函数
my_test(10)
my_test(6,5)
"""
===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(面向切面编程)

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()函数,其实是调用修饰后返回的auth_fn函数
test(20,10)
"""
---模拟执行权限检查---
执行test函数,参数a:20,参数b:10
"""

6.2.4再论类命名空间

再次申明:Python的类就像命名空间。Python程序默认处于全局命名空间内,类体则处于类命名空间内,Python允许在全局范围内放置可执行代码——当Python执行该程序时,这些代码就会获得执行的机会;类似地,Python同样允许在类范围内放置可执行代码——当Python执行该类定义时,这些代码同样会获得执行的机会。

class Item:
    #直接在类命名空间中方舟可执行代码
    print("正在定义Item类")
    for i in range(10):
        if i%2==0:
            print("偶数")
        else:
            print("奇数")

程序直接在Item类体中放置普通的输出语句、循环语句、分支语句,都是合法的。当程序执行Item类时,Item类命名空间中的这些代码都会被执行。

从执行效果来看,这些可执行代码被放在Python类命名空间与全局空间并没有太大的区别——确实如此,这是因为并没有定义“成员”(变量或函数),这些代码执行之后就完了,不会留下什么)。

global_fn=lambda p:print('执行lambda表达式,p参数:',p)
class Category:
    cate_fn=lambda p:print('执行lambda表达式,p参数',p)
#调用全局空间内的global_fn,为参数p传入参数值
global_fn('fkit')#执行lambda表达式,p参数: fkit
c=Category()
#调用类命名空间内的cate_fn,Python自动绑定第一个参数
c.cate_fn()#执行lambda表达式,p参数 <__main__.Category object at 0x000001F857410A70>

对于类命名空间内定义的lambda表达式,则相当于在类命名空间中定义一个函数,这个函数就变成了实例方法,因此程序必须使用调用方法的方式来调用该lambda表达式,Python同样为该方法的第一个参数(相当于self参数)绑定参数值。

6.3 成员变量

在类体内定义的变量,默认属于类本身。如果把类当作类命名空间,那么该类变量就是定义在类命名空间内的变量。

6.3.1 类变量和示例变量

在类命名空间内定义的变量就属于类变量,Python可以使用类来读取、修改类变量。

class Address:
    detail='广州'
    post_code='510660'
    def info (self):
        #尝试直接访问类变量
        #print(detail)#报错
        #通过类来访问类变量
        print(Address.detail)
        print(Address.post_code)
#通过类来访问Address类的类变量
print(Address.detail)#广州
addr=Address()
addr.info()
#修改Address类的类变量
Address.detail='佛山'
Address.post_code='460110'
addr.info()
"""
广州
510660
佛山
460110
"""

对于类变量而言,它们属于在类命名空间内定义的变量,因此程序不能直接访问这些变量,程序必须使用类名来调用类变量。不管是全局范围内还是函数内访问这些类变量,都必须使用类名进行访问。

实际上,Python完全允许使用对象来访问该对象所属类的类变量(当然还是推荐使用类访问类变量)。

class Record:
    #定义两个类变量
    item='鼠标'
    date='2024-1-1'
    def info(self):
        print(self.item)
        print(self.date)
rc=Record()
print(rc.item)
print(rc.date)
rc.info()
"""
鼠标
2024-1-1
鼠标
2024-1-1
"""

实际上,程序通过对象访问类成员,其实本质还是通过类名在访问类变量。

由于通过对象访问类变量的本质还是通过类名在访问,因此如果类变量发生了改变,当程序访问这些类变量时也会读到修改之后的值。

#修改Record类的两个变量

Record.item='键盘'

Record.data='2023-3-27'

#调用info()方法

rc.info()

"""

键盘

2023-3-28

"""

从上面输出结果可以看出,通过实例访问类变量的本质依然是通过类名在访问。

需要说明的是,Python允许通过对象访问类变量,但如果程序通过对象尝试对类变量赋值,此时性质就变了——Python是动态语言,赋值语句往往意味着定义新的变量。

因此,如果程序通过对象对类变量赋值,其实不是对“类变量赋值”,而是定义新的实例变量。

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)
#访问Inventory的item和quantity类变量
print(Inventory.item)
print(Inventory.quantity)
"""
显示器
500
鼠标
2000
"""

如果程序通过修改了两个类变量的值,程序中Inventory的实例变量的值也不会受到任何影响。

Inventory.quantity='类变量的quantity'
Inventory.item='类变量的item'
#访问iv的item和quantity实例变量
print(iv.item)
print(iv.quantity)
"""
显示器
500
"""

同样程序对一个对象的实例变量进行了修改,这种修改也不会影响类变量和其他对象的实例变量。

iv.item='实例变量item'
iv.quantity='实例变量的quantity'
print(Inventory.item)
print(Inventory.quantity)
"""
鼠标
2000
"""

6.3.2 使用property()函数定义属性

如果Python类定义了getter、setter等访问器方法,则可使用property()函数将它们定义成属性(相当于实例变量)。

property()函数的语法格式如下:

property(fget=None,fset=None,fdel=None,doc=None)

从上面的语法格式可以看出,在使用property()函数时,可传入4个参数,分别代表getter方法、setter方法、del方法和doc,其中doc是一个文档字符串,用于说明该属性。当然,开发者调用property也可传入0个(既不能读也不能写的属性)、1个(只能读属性)、2个(读写属性)、3个(读写属性,也能删除)和4个(读写属性,也可删除,包含文档说明)参数。

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
    #使用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)
#对rect的size属性赋值
rect.size=9,7
#访问rect的width、height实例变量
print(rect.width)
print(rect.height)
#删除rect的size属性
del rect.size
#访问rect的width、height实例变量
print(rect.width)
print(rect.height)
"""
用于描述矩形的大小的属性
Help on property:

    用于描述矩形的大小的属性

(4, 3)
9
7
0
0
"""

在使用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]
    #使用property()函数定义fullname属性,只传入两个参数
    #该属性是一个读写属性,但不能删除
    fullname=property(getfullname,setfullname)
u=User('悟空','孙')
#访问fullname属性
print(u.fullname)
#对fullname属性赋值
u.fullname='八戒,朱'
print(u.first)
print(u.last)
"""
悟空,孙
八戒
朱
"""

提示:某些编程语言中,类似于这种property合成的属性被称为计算属性。这种属性并不真正存储任何状态,它的值其实是通过某种算法计算得到的。当程序对该属性赋值时,被赋得值也会被存储到其它实例变量中。

还可以使用@property装饰器来修饰方法,使之成为属性。

class Cell:
    #使用@property修饰方法,相当于为该属性设置getter方法
    @property
    def state(self):
        return self._state
    #为state属性设置setter方法
    @state.setter
    def state(self,value):
        if 'alive' in value.lower():
            self._state='alive'
        else:
            self._state='dead'
    #为is_dead属性设置getter方法
    #只有getter方法得属性时只读属性
    @property
    def is_dead(self):
        return not self._state.lower()=='alive'
c=Cell()
#修改state属性
c.state='Alive'
#访问state属性
print(c.state)
#访问is_dead属性
print(c.is_dead)
"""
alive
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 隐藏和封装

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

封装是面向对象编程语言对客观世界的模拟,在客观世界中,对象的状态信息 都被隐藏在对象内部,外界无法直接操作和修改,对一个类或对象实现良好的封装,可以达到以下目的:

  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.__age
    age=property(getage,setage)
#创建User对象
u=User()
#对name属性赋值,实际上调用setname()方法
u.name='fk'#引发valueError错误:用户名长度必须在3~8之间

上面程序尝试将User对象的name设为fk,这个字符串的长度为“2”不符合实际要求,因此会报错。

将最后一行代码注释掉,改为如下的程序:

u.name='fkit'
u.age=25
print(u.name)
print(u.age)
"""
fkit
25
"""

从该程序可以看出封装的好处,程序可以将User对象的实现细节隐藏起来,程序只能通过暴露出来的setname()、setage()方法来改变User对象的状态,而这两种方法可以添加自己的逻辑控制,这种控制对User的修改始终是安全的。

上面程序还定义了一个__hide()方法,这个方法默认是隐藏的。如果程序尝试执行如下代码:

#尝试调用隐藏的__hide()方法
u.__hide()
"""
AttributeError: 'User' object has no attribute '__hide'
"""

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

#调用隐藏的__hide()方法
u._User__hide()
"""
示范隐藏hide方法
"""

通过上面的调用可以看出,Python并没有实现真正的隐藏。

类似的是,程序也可以通过为隐藏的示例变量添加画线和类名的方式来访问或修改对象的实例变量。

#对隐藏的__name属性赋值
u._User__name='fk'
#访问User对象的name属性(实际上访问的是__name实例变量)
print(u.name)#fk

总结:Python并没有提供真正的隐藏机制,所以Python类定义的所有成员变量默认都是公开的;如果希望将Python类中的某些成员隐藏起来,那么只要让该成员的名字以双下划线开头即可,即使通过这种机制实现了隐藏,其实也依然可以绕过去。

6.5 类的继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Python的继承是多继承机制,即一个子类可以同时有多个父类。

6.5.1 继承的语法

Python子类继承父类的语法是在定义子类时,将多个父类放在子类之后的圆括号里。语法格式如下:

class SubClass(SuperClass1,SuperClass2,...):
    #类定义部分

从上面的语法格式来看,定义子类的语法非常简单,只需在原来的类定义后增加圆括号,并在圆括号中添加多个父类,即可表明该子类继承了这些父类。

如果子啊定义一个Python类时并未显式指定这个类的直接父类,则这个类默认继承object类,。因此,object类是所有类的父类,要么是其直接父类,要么是间接父类。

实现继承的类被称为子类,被继承的类被称为父类,也被称为基类、超类。父类和子类的关系,是一般和特殊的关系。例如水果与苹果之间的关系,苹果继承了水果,苹果是水果的子类,则苹果是一种特殊的水果。

由于子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。

从实际意义上看,子类是对父类的扩展,子类是一种特殊的父类。从这个意义上看,使用继承来描述子类和父类的关系是错误的,用扩展更恰当。因此,这样的说法更准确:Apple类扩展了Fruit类。

从子类的角度来看,子类扩展了父类;但从父类的角度来看,父类派生出子类。也就是说,扩展和派生所描述的是同一个动作,只是观察的角度不同而已。

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克
不同事物的口感不同
"""

在主程序部分,主程序创建了Apple对象之后,可以访问该Apple对象的info()和taste()方法,这表明Apple对象也具有了info()和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中方法: 这是一个商品
"""

上面程序让Mouse继承了Item类和Product类,由于Item排在前面,因此Item中定义的方法优先级更好,Python会有优先到Item父类中搜寻方法,一旦Item父类中搜寻到目标方法,Python就不会继续向下搜寻了。

上面程序中Item和Product两个父类中都柏寒了info()方法,当Mouse子类对象调用info()方法时——子类中没有定义info()方法,因此Python会从父类中寻找info()方法,此时优先使用第一个父类Item中的info()方法。

如果将上面程序改写成如下形式:

class Mouse(Product,Item):

    pass

m=Mouse()

m.info()

"""

Product中方法: 这是一个工业产品

"""

6.5.3 重写父类的方法

子类扩展了父类,子类是一种特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的方法。但有一种情况例外:子类需要重写父类的方法。例如鸟类都包含了飞翔方法,其中鸵鸟类是一种特殊的鸟类,因此鸵鸟应该是鸟的子类,它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合鸵鸟,为此,鸵鸟需要重写鸟类的方法。

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

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

6.5.4 使用非绑定方法调用被重写的方法

如果在子类中调用之后的方法,Python总是会执行子类重写的方法,不会执行父类中被重写的方法。如果需要在子类中调用父类中被重写的实列方法,那该怎么办呢?

Python类相当于类空间,因此Python类中的方法本质上相当于类空间的函数。所以,即使是实例方法,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()
"""
执行bar方法
子类重写父类中的foo方法
父类中定义的foo方法
"""

6.5.5 使用super函数调用父类的构造方法

Python的子类也会得到父类的构造方法,如果子类有多个父类,那么排在前面的父类的构造方法也会被优先使用。

class Emplyee:
    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 Manger(Emplyee,Customer):
    pass
m=Manger(25000)
m.work()#普通员工正在写代码,工资是: 25000
m.info()#引发错误AttributeError: 'Manger' object has no attribute 'favorite'

如果将程序中改为如下形式:

class Manger(Customer,Emplyee):

上面Manger类将优先使用Customer类的构造方法,因此程序必须使用如下代码来创建Manger对象:

m=Manger('IT产品','广州')

上面程序中为Manger的构造方法传入两个参数,这明显是调用从Customer类继承得到的两个构造方法,此时程序将可以初始化Customer类中的favorite和address实例变量,但它又不能初始化Employee类中的salary实例变量。因此,此时程序中的info()可以正常运行,但work()方法会报错。

为了让Manger能特殊初始化两个父类中的实例变量,Manger应该定义自己的构造方法——就是重写父类的构造方法。Python要求:如果子类重写了父类的构造方法,那么子类的构造方法必须调用父类的构造方法。子类的构造方法调用父类的构造方法有两种:

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

在交互解释器中输入help(super)查看super()函数的帮助帮助信息,可以看到如下的输出信息:

help(super)
Help on class super in module builtins:

class super(object)
 |  super() -> same as super(__class__, )
 |  super(type) -> unbound super object
 |  super(type, obj) -> bound super object; requires isinstance(obj, type)
 |  super(type, type2) -> bound super object; requires issubclass(type2, type)
 |  Typical use to call a cooperative superclass method:
 |  class C(B):
 |      def meth(self, arg):
 |          super().meth(arg)
 |  This works for class methods too:
 |  class C(B):
 |      @classmethod
 |      def cmeth(cls, arg):
 |          super().cmeth(arg)
 |
 |  Methods defined here:
 |
 |  __get__(self, instance, owner=None, /)
 |      Return an attribute of instance, which is of type owner.
 |
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |
 |  __init__(self, /, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  __repr__(self, /)
 |      Return repr(self).
 |
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __self__
 |      the instance invoking super(); may be None
 |
 |  __self_class__
 |      the type of the instance invoking super(); may be None
 |
 |  __thisclass__
 |      the class invoking super()

从上面的介绍可以看出,super其实是一个类,因此调用super()的本质就是调用super类的构造方法来创建super对象。

从上面的帮助信息可以看到,使用super()构造方法最常用的做法就是不传入任何参数(这种做法super(type,obj)的效果相同),然后通过super对象的方法既可调用父类的实例方法,也可调用父类的类方法。在调用父类的实例方法时,程序会完成第一个参数self的自动绑定,如上帮助信息中的super().meth(arg)。在调用类方法时,程序会完成第一个参数cls的自动绑定,如上面帮助信息中super().cmeth(arg)信息所示。

掌握了super()函数的方法之后,把程序改写成如下形式:

#Manager继承了Employee、Customer
class Manger(Customer,Emplyee):
    #重写父类的构造方法
    def __init__(self,salary,favorite,address):
        print("--Manger的构造方法--")
        #通过super()函数调用父类的构造方法
        super().__init__(salary)
        #与上一行代码效果相同
        #super(Manger,self).__init__(salary)
        #使用未绑定方法调用父类的构造方法
        Customer.__init__(self,favorite,address)
m=Manger(25000,'IT产品','广州')
m.work()
m.info()

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()#报错
#为Cat动态添加一个walk()方法,该方法的第一个参数自动绑定
Cat.walk=walk_func
#d1、d2调用walk()方法
d1.walk()
d2.walk()
"""
Garfield慢慢走过一片草地
Kitty慢慢走过一片草地
"""

Python的这种动态性固然有其有时,但也给程序带来了一定的隐患:程序定义好的类,完全有可能在后面被其他程序修改,这就带来了一些不确定性。如果程序要限制为某个类动态添加属性和方法,则可以提供__slots__属性来指定。

__slots__属性的值就是一个元组,该元组的所有元素列出了该类的实例允许添加的所有属性名和方法名(对于Python来说,方法相当于属性值为函数的属性)。

class Dog:
    __slots__ = ['name', 'age', 'walk']
    def __init__(self,name):
        self.name = name
    def test():
        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()#Snoopy正在慢慢地走
d.foo=30#AttributeError: 'Dog' object has no attribute 'foo'

如果程序尝试为Dog对象添加其他额外属性,程序会引发AttributeError错误。

需要说明地是,__slots__属性并不限制通过类来动态添加属性或方法。

#__slots__属性并不限制通过类来动态添加方法
Dog.bar=lambda self:print("abc")
d.bar()#abc

此外,__slots__属性指定地限制只是对当前类的实例起作用,对该类派生出来的子类是不起作用的。

class GunDog(Dog):
    def __init__(self,name):
        super().__init__(name)
    pass
gd=GunDog('Puppy')
#完全可以为GunDog实例动态添加属性
gd.speed=99
print(gd.speed)
print(gd.name)

上面程序说明__slots__属性指定的限制只对当前类起作用。

如果要限制子类的实例动态添加属性和方法,则需要在子类中也定义__slots__属性,这样,子类的实例允许动态添加属性和方法就是子类的__slots__元组加上父类的__slots__元组的和。

6.6.2 使用type()函数定义类

前面已经提到使用type()函数可以查看变量的类型,但如果想使用type()直接查看某个类的类型呢?

class Role:
    pass
r=Role()
#查看变量r的类型
print(type(r))#
#查看Role类本身的类型
print(type(Role))#

从上面的输出结果可以看出,Role类本身的类型是type。这句话有点拗口,怎么理解Role类的类型是type?

从Python解释器的角度来看,当程序使用class定义Role类时,也可理解为定义了一个特殊的对象(type类的对象),并将该对象赋值给Role变量。因此,程序使用class定义的所有都是type类的实例。

实际上Python完全允许使用type()函数(相当于type类的构造器函数)来创建type对象,又由于type类的实例就是类,因此Python可以使用type()函数来动态创建类。

def fn(self):
    print("fn函数")
#使用type()定义Dog类
Dog=type('Dog',(object,),dict(walk=fn,age=6))
#创建Dog对象
d=Dog()
#分别查看d、Dog的类型
print(type(d))#
print(type(Dog))#
d.walk()#fn函数
print(Dog.age)#6

上面程序中使用type()定义了一个Dog类。在使用type()定义类时可指定三个参数:

  1. 参数一:创建的类名;
  2. 参数二:该类继承的父类集合。由于Python支持多继承,因此此处使用元组指定它的多个父类。即使实际只有一个父类,也需要使用元组语法(必须多一个逗号)。
  3. 参数三:该字典对象为该类绑定的类变量和方法。其中字典的key就是类变量或方法名,如果字典的value是普通值,那就代表类变量;如果字典的value是函数,则代表方法。

由此可见,上面程序定义了一个Dog类,该类继承了一个object类,还为该类定义了一个walk()方法和一个age类变量。

从上面的输出结果可以看出,看出type()函数定义的类直接使用class定义的类并没有任何区别。事实上,Python解释器在执行使用class定义的类时,其实依然是使用type()函数来创建类的。因此,无论通过哪种方式定义类,程序最终都是创建一个type的实例。

6.6.3 使用metaclass

使用希望创建某一批类全部具有某种特性,则可通过metaclass来实现。使用metaclass可以在创建类时动态修改类定义。

为了使用metaclass动态修改类定义,程序需要先定义metaclass,metaclass应该继承type类,并重写__new__()方法。

#定义ItemMetaClass,继承type
class ItemMetaClass(type):
    #cls代表被动态修改类
    #name代表被东涛修改的类名
    #bases代表被动态修改的类的所有父类
    #attr代表被动态修改的类的所有属性、方法组成的字典
    def __new__(cls, name, bases,attrs):
        #为该类动态添加一个cal_price方法
        attrs['cal_price']=lambda self:self.price*self.discount
        return type.__new__(cls, name, bases, attrs)

上面程序定义一个ItemMetaClass类,该类继承了type类,并重写了__new__方法,在重写该类方法时为目标动态添加了一个cal_price方法。

metaclass类的__new__方法的作用是:当程序使用class定义新类时,如果指定了metaclass,那么metaclass的__new__方法就会被自动执行。

#定义Book类
class Book(metaclass=ItemMetaClass):
    __slots__=('mname','price','_dicount')
    def __init__(self,name,price):
        self.mname=name
        self.price=price
    @property
    def discount(self):
        return self._discount
    @discount.setter
    def discount(self,discount):
        self._dicount=discount
#定义CellPhone类
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
#创建Book对象的cal_price()方法
print(b.cal_price())
cp=CellPhone(2399)
cp.discount=0.85
#创建CellPhone对象的cal_price()方法
print(cp.cal_price())
"""
67.64
2039.1499999999999
"""

6.7 多态

对于弱类型的语言来说,变量并没有声明类型,因此同一个变量完全可以在不同的时间引用不同的对象。当同一个变量在调用同一个方法时,完全可能呈现出多种行为(具体呈现哪种行为由该变量所引用的对象来决定),这就是类的多态。

6.7.1 多态性

class Bird:
    def move(self,field):
        print("鸟在%s上自由地飞翔"%field)
class Dog:
    def move(self,field):
        print("狗在%s李飞快地奔跑"%field)
#x变量被赋值为Bird对象
x=Bird()
#调用x变量的move()方法
x.move("天空")
#x变量被赋值为Dog对象
x=Dog()
#调用x变量的move()方法
x.move("草地")
"""
鸟在天空上自由地飞翔
狗在草地李飞快地奔跑
"""

从上面的运行结果来看,同一个变量x在执行同一个move()方法时,由于x指向的对象不同,因此它呈现出不同的行为特征,这就是多态。

看到这里,可能会有疑问:这个多态有什么用?不就是创建对象、调用方法吗?

实际上多态是一种非常灵活的编程机制。假如要定义一个Canvas(画布)类,这个画布类定义一个draw_pic()方法,该方法负责绘制各种图形。

class Canvas:
    def draw_pic(self,shape):
        print("---开始绘图---")
        shape.draw(self)

从上面程序可以看出,Canvas的draw_pic()方法需要传入一个shape参数,该方法就是调用shape参数的drwa()方法将自己绘制到画布上。

从上面程序来看,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()
#传入Rectangle参数,绘制矩形
c.draw_pic(Rectangle())
#传入Triangle参数,绘制三角形
c.draw_pic(Triangle())
#传入Circle参数,绘制圆形
c.draw_pic(Circle())

"""
---开始绘图---
在<__main__.Canvas object at 0x000001FAF97B1250>上绘制矩形
---开始绘图---
在<__main__.Canvas object at 0x000001FAF97B1250>上绘制三角形
---开始绘图---
在<__main__.Canvas object at 0x000001FAF97B1250>上绘制圆形
"""

从上面的例子可以体会到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'
#'Hello'是str类的实例,输出True
print("'Hello'是否是str类的实例:",isinstance(hello,str))
#'Hello'是object类的子类的实例输出True
print("'Hello'是否是object类的实例:",isinstance(hello,object))
#str是object类的子类,输出True
print("'str'是否为object的子类:",isinstance(str,object))
#'Hello'不是tuple类及其子类的实例,输出False
print('"Hello"是否为tuple及其子类的实例:',isinstance(hello,tuple))
#str不是tuple类的子类,输出False
print("str是否为tuple的子类:",issubclass(str,tuple))
#定义一个列表
my_list=[2,4]
#[2,4]是list类的实例,输出True
print(isinstance(my_list,list))
#[2,4]是object类及其子类的实例,输出True
print(isinstance(my_list,object))
#list是object的子类,输出True
print(issubclass(list,object))
#[2,4]不是tuple类及其子类的实例,输出False
print(isinstance([2,4],tuple))
#list不是tuple类的子类,输出False
print(issubclass(list,tuple))
"""
'Hello'是否是str类的实例: True
'Hello'是否是object类的实例: True
'str'是否为object的子类: True
"Hello"是否为tuple及其子类的实例: False
str是否为tuple的子类: False
True
True
True
False
False
"""

两个函数区别只是,issubclass()的第一个参数是类名,而isinstance()的第一个参数是变量,这也与两个函数的意义对应:issubclass用于判断是否为子类,而isinstance()用于判断是否为该类或子类的实例。

issubclass()和isinstance()两个函数的第二个参数都可以使用元组。

data=(20,'fkit')
print("data是否为列表或元组的实例:",isinstance(data,(list,tuple)))#True
#str不是list或tuple的子类,输出False
print('str是否为list和tuple的子类',isinstance(str,(list,tuple)))
#str是list和object的子类,输出True
print("str是否为list或object的子类:",issubclass(str,(list,object)))
"""
data是否为列表或元组的实例: True
str是否为list和tuple的子类 False
str是否为list或object的子类: True
"""

此外,Python为所有类都提供了一个__bases__属性,通过该属性可以查看该类的所有父类,该属性返回所有直接父类组成的元组。

class A:
    pass
class B:
    pass
class C(A,B):
    pass
print("类A的所有父类:",A.__bases__)
print("类B的所有父类:",B.__bases__)
print("类C的所有父类:",C.__bases__)
"""
类A的所有父类: (,)
类B的所有父类: (,)
类C的所有父类: (, )
"""

从上面运行结果来看,如果在定义类时没有显式制定它的父类,则这些类默认的父类是object类。

Python还为所有类都提供了一个__subclass__()方法,通过该方法可以查看该类的所有直接子类,该方法返回该类的所有子类组成的列表。

print("类A的所有子类:",A.__subclasses__())
print("类B的所有子类:",B.__subclasses__())
"""
类A的所有子类: []
类B的所有子类: []
"""

6.8 枚举类

在某些情况下,一个类的对象是有限且固定的,比如季节类,它只有四个对象;再比如行星类,目前只要八个对象。这种实例有限且固定的类,在Python中被称为枚举类。

6.8.1 枚举入门

程序有两种方式来定义枚举类:

  1. 直接使用Enum列出多个枚举值来创建枚举类。
  2. 通过继承Enum基类来派生枚举类。
import enum

#定义Season枚举类

Season = enum.Enum('Season',('Spring','Summer','Autumn','Winter'))

上面程序使用Enum()函数(就是Enum的构造方法)来创建枚举类,该构造方法的第一个参数是枚举类的类名;第二个参数是一个元组,用于列出所有枚举值。

在定义了上面的Season枚举类之后,程序可直接通过枚举值进行访问,这些枚举值都是该枚举的成员,每个成员都有name、value两个属性,其中name属性值为该枚举值的变量,value代表该枚举值的序号(序号通常从1开始)。

#直接访问制定枚举
print(Season.SPRING)
#访问枚举成员的变量名
print(Season.SPRING.name)
#访问枚举成员的值
print(Season.SPRING.value)
"""
Season.SPRING
SPRING
1
"""

此外,Python还为枚举提供了一个__members__属性,该属性返回一个dict字典,字典包含了该枚举的所有枚举类型。程序可通过遍历__members__属性来访问枚举的所有实例。

#遍历Season枚举的所有成员
for name,member in Season.__members__.items():
    print(name,"=>",member,",",member.value)
"""
SPRING => Season.SPRING , 1
Summer => Season.Summer , 2
Autumn => Season.Autumn , 3
Winter => Season.Winter , 4
"""

如果要定义更复杂的枚举,则可通过继承Enum来派生枚举类,在这种方式下程序就可以为枚举额外定义方法了。

import enum
class Orientation(enum.Enum):
    #为序列值指定value值
    EAST='东'
    SOUTH='南'
    WEST='西'
    NORTH='北'
    def info(self):
        print('这是一个代表方向[%s]的枚举'%self.value)
print(Orientation.SOUTH)
print(Orientation.SOUTH.value)
#通过枚举变量名访问枚举
print(Orientation['WEST'])
#通过枚举值来访问枚举
print(Orientation('南'))
#调用枚举的info()方法
Orientation.EAST.info()
#遍历Orientation枚举的所有成员
for name,member in Orientation.__members__.items():
    print(name,'=>',member,',',member.value)
"""
Orientation.SOUTH
南
Orientation.WEST
Orientation.SOUTH
这是一个代表方向[东]的枚举
EAST => Orientation.EAST , 东
SOUTH => Orientation.SOUTH , 南
WEST => Orientation.WEST , 西
NORTH => Orientation.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
#访问FEMALE的name
print("FEMALE的name:",Gender.FEMALE.name)
#访问FEMALE的value
print("FEMALE的value:",Gender.FEMALE.value)
#访问自定义的cn_name属性
print("FEMALE的cn_name:",Gender.FEMALE.cn_name)
#访问自定义的desc属性
print("FEMALE的desc:",Gender.FEMALE.desc)
"""
FEMALE的name: FEMALE
FEMALE的value: ('女', '阴柔之美')
FEMALE的cn_name: 女
FEMALE的desc: 阴柔之美
"""

你可能感兴趣的:(python,python,面向对象)