part6-2 Python 类和对象(类变量和实例变量区别、property 函数定义属性及装饰器方法、隐藏和封装、继承(方法重写、未绑定方法调用)、super函数使用、动态与__slots__、type()定义类、metaclass使用、多态、issubclass、isinstance、__bases__ 属性、__subclasses__()方法、枚举类)


一、成员变量

1、 类变量和实例变量
在类命名空间内定义的变量属于类变量,可以通过类来读取、修改类变量。

类变量:定义在类命名空间,程序不能直接访问、修改类变量,不管在全局范围内还是函数范围内,都必须通过对应的类名来调用类变量。当然,类的对象也可调用、修改类变量。代码示例如下:
class Address:
    detail = '成都'
    post_code = '600610'
    def info(self):
        # 不能直接访问类变量,下面的语句报错
        # print(detail)       # NameError: name 'detail' is not defined
        # 通过类来访问类变量
        print("通过类访问类变量detail:", Address.detail)
        print("通过类访问类变量post_code:", Address.post_code)
        # 通过该类的对象来访问类变量
        print("通过对象访问类变量detail:", self.detail)
        print("通过对象访问类变量post_code:", self.post_code)

# 可直接通过类来访问类变量
print(Address.detail)       # 输出:成都
print("-"*40)

addr = Address()        # 创建对象 addr
addr.info()
print(addr.detail)      # 调用类变量
print("-"*40)

# 通过类修改类变量的值
Address.detail = '重庆'
Address.post_code = '123456'
addr.info()
print("-"*40)

# 通过对象来修改类变量的值,实际上是给实例(对象)动态增加变量,对类变量没有影响
addr.detail = '北京'        # 实际为 addr 实例(对象)增加一个实例变量 detail,并没有修改类变量 detail
addr.post_code = '654321'
addr.info()

运行这段代码,输出如下所示:
成都
----------------------------------------
通过类访问类变量detail: 成都
通过类访问类变量post_code: 600610
通过对象访问类变量detail: 成都
通过对象访问类变量post_code: 600610
成都
----------------------------------------
通过类访问类变量detail: 重庆
通过类访问类变量post_code: 123456
通过对象访问类变量detail: 重庆
通过对象访问类变量post_code: 123456
----------------------------------------
通过类访问类变量detail: 重庆
通过类访问类变量post_code: 123456
通过对象访问类变量detail: 北京
通过对象访问类变量post_code: 654321
北京
在这段代码中,Address 类的 info() 方法使用了两种方式输出两个类变量,即通过类访问类变量和通过类的对象来访问类变量。由于Python 是一门动态语言,当使用类的对象访问类变量时,首先找该对象的同名实例变量,没有找到的话就访问去访问类变量,找到的话,就访问的是实例变量。在使用类修改类变量时,通过类和对象访问的类变量值同时也会修改,但是使用对象修改类变量时,实际上是为这个对象动态增加了实例变量,所以并不会影响类的变量值,影响的是实例变量的值。从输出可以证实这一点。

下面通过实例代码加深理解类变量与实例变量,代码如下:
class Human:
    # 定义两个类变量
    name = 'michael'
    age = 25
    # 定义实例方法
    def change(self, name, age):
        # 下面的赋值语句不是对类变量赋值,而是定义新的实例变量
        self.name = name
        self.age = age

# 创建 Human 类的对象
h = Human()
h.change('stark', 100)      # 调用实例方法,创建实例变量

# 访问对象 h 的 name、age 实例变量
print(h.name, h.age)        # 输出:stark 100

# 访问 Human 类的 name、age 类变量
print(Human.name, Human.age)    # 输出:michael 25

# 修改类变量的值
Human.name = 'Tom'
Human.age = 50
# 再一次访问对象 h 的 name、age实例变量,实例变量的值不受类变量值的影响
print(h.name, h.age)        # 输出:stark 100

# 下面修改实例变量的值,同样不会影响类变量的值
h.name = '实例变量name'
h.age = '实例变量age'
print(Human.name, Human.age)    # 输出:Tom 50
这段代码最开始定义了 Human 类,并在类中定义了两个类变量(name、age)和一个 change 实例方法,在实例方法中对 name、age 变量赋值。当程序第一次调用这个 change 方法时,程序就为调用这个 change 方法的对象重新定义了两个实例变量。当通过对象调用change() 方法后,就为对象创建了两个实例变量name、age,现在通过对象访问name、age 时,输出的是实例变量的值,通过类访问name、age时,输出的是类变量的值。

接着在程序中修改了类变量的值和实例变量的值,但是这种修改是互不影响的,即修改类变量的值不会影响实例变量的值,同样修改实例变量的值,也不会影响类变量的值。

2、 使用 property 函数定义属性
为类定义 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个(读写、删除、说明文档)参数。property() 函数使用简单实例如下:
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 属性
    size = property(getsize, setsize, delsize, '描述矩形大小的属性')

# 访问 size 属性的说明文档
print(Rectangle.size.__doc__)   # 描述矩形大小的属性
# 通过内置的help()函数查看 Rectangle.size 的说明文档
help(Rectangle.size)

rect = Rectangle(4, 3)      # 创建实例
# 访问 rect 对象的 size 属性,实际调用的是 getsize 方法
print(rect.size)        # 输出:(4, 3)
# 对 rect 对象的 size 属性赋值,实际调用的是 setsize 方法
rect.size = 7, 8
# 访问 rect 对象的 width、height 实例变量
print(rect.width, rect.height)      # 输出:7 8

# 删除 rect 对象的 size 属性,实际调用 delsize 方法
del rect.size
# 访问 rect 对象的 width、height 实例变量
print(rect.width, rect.height)      # 0 0

运行代码,输出如下所示:
描述矩形大小的属性
Help on property:

    描述矩形大小的属性

(4, 3)
7 8
0 0
上面代码在 Rectangle 类中 size = property(getsize, setsize, delsize, '描述矩形大小的属性') 这行代码使用 property() 函数定义了一个 size 属性,这个属性有4个参数,分别是可读、可写、可删除、说明文档。程序对 Rectangle 对象的 size 属性进行读、写、删除操作时,实际上这些操作分别被委托给 getsize()、setsize() 和 delsize() 方法来实现。

在使用 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]
    # 使用 property() 函数定义 fullname 属性,只传入两个读写属性,但不能删除
    fullname = property(getfullname, setfullname)

# 创建 User 类对象
u = User('michael', 'stev')
# 访问 fullname 属性,实际访问的是 getfullname() 实例方法
print(u.fullname)       # michael,stev

# 对 fullname 属性赋值,实际访问的是 setfullname() 实例方法
u.fullname = 'stark,woo'
# 再一次访问 fullname 属性
print(u.first, u.last)      # stark woo
这次在 User 类中使用 property() 函数定义的 fullname 属性只传入两个参数,分别作为 getter 和 setter 方法,因此该属性有读写属性,没有删除属性。

在其他一些编程语言中,类似这种 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()
# 修改 satte 属性
c.state = 'Alive'
# 访问 state 属性
print(c.state)      # 输出:alive
# 访问 is_dead 属性
print(c.is_dead)    # 输出:False
在代码中使用 @property 装饰器装饰了 state() 方法,此时 state() 方法就成了 state 属性,并为该属性设置了 getter 方法。接着在后面使用 @state.setter 装饰器装饰了 state 属性的 setter 方法。这时 state 属性就同时有了读写的属性。

第二个 @property 装饰器装饰了 is_dead() 方法,该方法变成了 is_dead 属性的 getter 方法。由于在程序中没有使用 @is_dead.setter 装饰器装饰 setter 方法,因此 is_dead 属性只是一个可读属性。

二、 隐藏和封装

面向对象三大特征:封装、继承和多态。封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类提供的方法来对内部信息的操作和访问。

对一个类或对象实现良好的封装,可达到以下目的。
(1)、隐藏类的实现细节。
(2)、使用者只能通过事先预定的方法来访问数据,从而可在该方法里加入控制逻辑,限制对属性的不合理访问。
(3)、可进行数据检查 ,有利于保证对象信息的完整性。
(4)、便于修改,提高代码的可维护性。

要实现良好的封装,要考虑两个方面。
(1)、将对象的属性和实现细节隐藏起来,不允许外部直接访问。
(2)、把方法暴露出来,让方法来控制对这些属性进行安全的访问和操作。

所以,封装的两个意义是:把该隐藏的隐藏起来,把该暴露的暴露出来。Python 没有 private 修饰符,不能真正支持隐藏。但是可以将需要隐藏的成员命名为以双下划线开头的,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 = 'me'       # 引发 ValueError: 用户名长度必须在3~8之间
这里将 User 类的两个实例变量命名为 __name 和 __age,是以双下划线开头,这样实例变量会被隐藏起来,在程序中不能直接访问 __name、__age 变量,只能通过 setname()、getname()、setage()、getage() 访问器进行访问,在 setname()、setage()方法中可以对用户设置的 name、age 进行控制,当符合指定条件后才允许设置。

将引发异常的 u.name = 'me' 语句注释掉,在后面添加下面代码,程序能够正常运行。
u.name = 'stark'
u.age = 30
print(u.name, u.age)        # 正常输出:stark 30
封装的好处:将 User 对象的实现细节隐藏起来,程序只能通过暴露出来的 setname()、setage() 方法来改变 User 对象的状态,在这两个方法中可以添加需要的逻辑控制,这种控制对 User 的修改也始终是安全的。

在 User 类中还定义了一个 __hide() 方法,默认是隐藏的,如果在程序中直接通过对象调用(u.__hide()),程序会抛出:AttributeError: 'User' object has no attribute '__hide' 错误。

其实在 Python 中没有真正的隐藏机制,Python 只是将双下划线开头的方法名、变量名进行了改变,在这些双下划线开头的方法名、变量名前面添加单下划线和类名。因此在 User 类中的 __hide() 其实可通过 u._User__hide() 的方式来调用,对于实例变量也可这样修改和访问。示例如下:
# 调用隐藏的 __hide() 方法
u._User__hide()                     # 隐藏的 hide 方法示例

# 对隐藏的 __name 属性赋值,从下面的输出可知设置成功
u._User__name = 'me'
# 使用两种方式都可以访问
print(u.name, u._User__name)        # 输出:me me
小结:Python 没有提供真正的隐藏机制,在类中定义的所有成员默认都是公开的;如果要将类中的某些成员隐藏起来,可在名称前面加上双下划线开头。但即使这样做,依然可以绕过去。

三、 类的继承

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

1、继承语法
Python 子类继承父类的语法是在定义子类时,将多个父类放在子类后面的圆括号里。语法如下:
class subClass(SuperClass1, SuperClass2, ...):
    # 类定义部分
在定义类时,如果没有指定父类,则默认继承的父类是 object 类。object 类是所有类的父类,要么是直接父类,要么是间接父类。

通常,继承的类称为子类,被继承的类称为父类,也叫做基类、超类。父类和子类的关系是一般和特殊的关系。

父类比子类包含的范围要大,父类是大类,子类是小类。子类是对父类的扩展,子类是一种特殊的父类。所以从某种意义上说,使用继承描述子类和父类的关系是错误,应使用扩展更恰当,说法更准确。例如苹果类扩展了水果类。

下面用示例理解子类继承父类,代码如下:
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 = 6.3
# 调用 Apple 对象的 info() 方法
a.info()        # 输出:我是一个水果!重6.3克
# 调用 Apple 对象的 taste() 方法
a.taste()       # 输出:不同食物的口感不同
这里定义的 Apple 类是一个空类,这个类继承了 Fruit 类和 Food 类。Apple 类的对象访问的 info() 和 taste() 方法是父类提供的,这说明子类继承父类后,子类也具有父类的方法,这就是继承的作用,子类扩展(继承)了父类。

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,在两个父类中有同名的方法 info() 。由于在 Mouse 类中继承的顺序是先继承 Item 类,后继承 Product 类,所以 Item 类中的方法优先级会更好。所以执行 m.info() 语句时,由于子类中没有 info() 方法,就到第一个继承的父类 Item 中寻找,所以实际调用的是 Item 类中的 info() 方法。

3、重写父类的方法
子类以父类为基础,子类可以额外增加新的方法,子类还可以重写父类的方法来满足子类的需求。子类包含与父类同名的方法就被称为方法重写(Override),也被称为方法覆盖。示例如下:
class Product:
    def info(self):
        print("Product 中的方法:", "这是一个产品")

class Mouse(Product):
    def info(self):
        print("这是一个鼠标。")

m = Mouse()
m.info()        # 输出:这是一个鼠标。
这里 Mouse 类中包含有与父类 Product 同名的方法 info(),在调用这个方法时,会优先调用子类的 info() 方法。也就是说父类的 info() 方法被子类重写或覆盖了。

4、 使用未绑定方法调用被重写的方法
前面说过,Python 的类有类空间,类中的方法是类空间内的函数,类空间的实例变量、实例方法都可通过类名调用。区别是:使用类名调用实例方法时,不会为实例方法的第一个参数 self 自动绑定参数值,需要程序显式绑定第一个参数 self。这种机制叫作未绑定方法。

通过使用未绑定方法即可在子类中再次调用父类中被重写的方法。示例如下:
class Product:
    def info(self):
        print("Product 中的方法:", "这是一个产品")

class Mouse(Product):
    def info(self):
        print("这是一个鼠标。")
    def foo(self):
        # 直接调用 info 方法,调用子类重写之后的 info() 方法,self 参数自动传递
        self.info()
        # 使用类名调用实例方法(未绑定方法),调用父类被重写的方法,self 参数手动传递
        Product.info(self)

m = Mouse()
m.foo()

输出如下所示:
这是一个鼠标。
Product 中的方法: 这是一个产品
上面代码中 Mouse 类继承了 Product 类,并且在 Mouse 类中重写了父类的 info() 方法。在 Mouse 类中定义的 foo() 方法中,self.info()  语句直接通过 self 调用子类的 info() 方法;Product.info(self) 语句通过未绑定方法显式调用 Product 类中的 info()  实例方法,并显式为第一个参数 self 绑定参数值,这样就实现了调用父类中被重写的方法。

5、 使用 super 函数调用父类的构造方法
子类会继承父类的构造方法,当子类有多个直接父类,则排在前面的父类的构造方法会被优先使用。示例如下:
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))

class Manager(Employee, Customer):
    pass

m = Manager(10000)
m.work()        # 普通员工在写代码,薪水是: 10000
m.info()        # AttributeError: 'Manager' object has no attribute 'favorite'
这里定义的 Manager 类继承了 Employee 和 Customer 两个父类,在子类中没有定义构造方法。在初始化子类对象时,会根据继承的顺序优先使用继承排在前面的父类的构造方法,所以这里的 m = Manager(10000) 代码初始化 m 对象时,调用的是 Employee 类的构造方法。这行 m.info() 调用第二个父类的 info() 时,由于构造方法没有执行,实例变量也没有被初始化,所以出现 AttributeError 错误。

子类可以重写父类的构造方法,这样在子类中可以同时初始化多个父类的实例变量。如果子类重写了父类的构造方法,那么子类的构造方法必须调用父类的构造方法。子类构造方法中有两种方式调用父类的构造方法:
(1)、使用未绑定法。由于构造方法也是实例方法,所以可通过这种方式来调用。
(2)、使用 super() 函数调用父类的构造方法。

在命令行下,使用 help(super) 可查看 super() 函数的帮助信息,帮助信息如下:
Help on class super in module builtins:

class super(object)
 |  super() -> same as super(__class__, <first argument>)
 |  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, /)
 |      Return an attribute of instance, which is of type owner.
 |
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
······
从帮助信息可看出 super 也是一个类,调用 super() 的本质是调用 super 类的构造方法来创建 super 对象。使用 super() 构造方法可不传入任何参数,这种做法与 super(type, obj)的效果相同,然后通过 super 对象的方法既可调用父类的实例方法,也可调用父类的类方法。在调用父类的实例方法时,程序会完成第一个参数 self 的自动绑定(super().meth(arg))。在调用类方法时,程序会完成第一个参数 cls 的自动绑定(super().cmeth(arg))。super() 函数的用法实例如下:
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))

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)

m = Manager(10000, 'software', 'CD')
m.work()
m.info()

运行程序,输出如下:
Manager 类的构造方法
普通员工在写代码,薪水是: 10000
我是顾客,爱好是:software,地址是:CD
上面代码中的 Manager 类中,重写了父类的构造方法,并在构造方法内通过两种方式调用父类的构造方法,这样两个父类的实例变量都能被初始化。要注意的是,在子类的构造方法中,不能同时用两个 super() 函数去调用两个父类的构造方法。

四、 Python 的动态性
动态语言的典型特征:类、对象的属性、方法都可以动态增加和修改。前面已经用过为对象动态添加属性和方法,现在进一步理解动态特征。

1、 动态属性与 __slots__
在为对象动态添加方法时,只对当前对象有效。如果要为所有实例都添加方法,可通过为类添加方法来实现。示例如下:
class Person:
    def __init__(self, name):
        self.name = name

def walk_func(self):
    print("%s 正在学走路" % self.name)

p1 = Person('michael')
p2 = Person('stark')

# 为 Person 类动态添加 walk() 方法,该方法的第一个参数自动绑定
Person.walk = walk_func
# p1、p2 对象调用 walk 方法
p1.walk()       # 输出:michael 正在学走路
p2.walk()       # 输出:stark 正在学走路
这里在定义 Person 类时只定义了一个构造方法,没有定义其它方法。Person.walk = walk_func 这一行代码为 Person 类动态添加一个 walk() 方法,为类动态添加方法时不需要使用 MethodType 进行包装,该函数的第一个参数会自动绑定。这样 Person 类的对象都具有 walk() 方法。

为类动态添加方法存在一定的隐患,当程序定义好的类,完全可能在后面被其他程序修改,这会带来一些不确定性。所以需要限制程序为某个类动态添加属性和方法,可通过 __slots__ 属性来指定。

__slots__ 属性的值是一个元组,该元组的所有元素列出了该类的实例允许动态添加的所有属性名和方法(在 Python 中,方法相当于属性值为函数的属性)。示例如下:
class Dog:
    __slots__ = ('walk', 'age', 'name')
    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.city = 'CD'   # 报错:AttributeError
上面程序中的 Dog 类中的第一行代码 __slots__ = ('walk', 'age', 'name') 限制了只能为该类实例动态添加 walk、age、name 这三个属性或方法,因此这行代码 d.city = 'CD' 动态添加的属性不在 __slots__ 元组的规定内,所以程序引发 AttributeError 错误。

注意1:__slots__ 属性限制的是实例动态添加属性或方法,并不限制通过类来动态添加属性或方法。因此下面代码是合法的:
Dog.city = 'CD'
print(d.city)       # 输出:CD
注意2:__slots__ 属性指定的限制只对当前类的实例起作用,对该类派生出来的子类不起作用。如果要限制子类的实例动态添加属性和方法,则需要在子类中也定义 __slots__ 属性,这样,子类的实例允许动态添加的属性和方法就是子类的 __slots__ 元组加上父类的 __slots__ 元组的和。示例如下:
class GunDog(Dog):
    __slots__ = ('city',)
    def __init__(self, name):
        super().__init__(name)

gd = GunDog('Puppy')
# city 属性在子类的 __slots__ 属性的限制范围内
gd.city = 'CD'
print(gd.city)      # CD
# age 属性在父类的 __slots__ 属性的限制范围内
gd.age = 5
print(gd.age)       # 5
# speed 属性不在父类和子类的 __slots__ 属性的限制范围内,程序报错
gd.speed = 50
print(gd.speed)     # AttributeError

2、 使用 type() 函数定义类
type() 函数可以查看变量的类型,也可查看类的类型。例如使用 type(Dog) 得到的输出是 <class 'type'> 。由此可知,类本身的类型是 type。

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

Python 允许使用 type() 函数(相当于 type 类的构造器函数)来创建 type 对象,又由于 type 类的实例(对象)就是类,因此 Python 可以使用 type() 函数动态创建类。示例如下:
def foo(self):
    print("foo 函数")

# 使用 type() 函数定义 Dog 类
Dog = type('Dog', (object,), dict(walk=foo, age=10))

# 创建 Dog 对象
d = Dog()
# 分别查看d、Dog 的类型
print(type(d))
print(type(Dog))
d.walk()
print(Dog.age)

运行程序,输出如下:
<class '__main__.Dog'>
<class 'type'>
foo 函数
10
上面程序中,使用 type() 定义了一个 Dog 类。在使用 type() 定义类时可指定三个参数。
(1)、参数一:创建的类名。
(2)、参数二:该类继承的父类集合。由于 Python 支持多继承,这里要使用元组指定它的多个父类,哪怕只有一个父类,也要使用元组语法。
(3)、参数三:字典对象为该类绑定了类变量和方法。字典的 key 就是类变量名或方法名,如果字典的 value 是普通值,就代表类变量;如果字典的 value 是函数,则代表方法。

所以,这行代码 Dog = type('Dog', (object,), dict(walk=foo, age=10)) 定义了一个 Dog 类,该类继承了 object 类,并且为该类定义了一个 walk() 方法和一个 age 类变量。

从输出结果可知,使用 type() 函数定义类与直接使用 class 定义的类是一样的。事实上,Python 解释器在执行使用 class 定义的类时,其实依然是使用 type() 函数来创建类的。所以,不管使用哪种方式定义类,程序最终都是创建一个 type 的实例。

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

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

下面代码定义一个 metaclass 类:
# 定义 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__ 方法会自动执行。示例,下面使用 metaclass 定义了两个类,代码如下:
# 定义 Book 类
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

# 定义 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() 方法。下面代码测试 Book、CellPhone 两个类的 cal_price() 方法。
b = Book('Python 基础', 10)
b.discount = 0.55
# 调用 Book 对象的 cal_price() 方法
print(b.cal_price())        # 输出:5.5

cp = CellPhone(1000)
cp.discount = 0.65
# 调用 CellPhone 对象的 cal_price() 方法
print(cp.cal_price())       # 输出:650.0
通过上面示例可知,使用 metaclass 可动态修改程序中的一批类,对它们集中进行某种修改。这个功能在开一些基础性框架时非常有用,程序可通过使用 metaclass 为某一批需要具有通用功能的类添加方法。


五、 多态

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

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 变量开始被赋值为 Bird 对象,当 x 变量执行 move() 方法时,输出是鸟的飞翔行为;接下来 x 变量被赋值为 Dog 对象,因此当 x 变量执行 move() 方法时,输出的是狗的奔跑行为。

从输出结果可知,同一个变量 x 在执行同一个 move() 方法时,由于 x 指向的对象不同,因此它呈现出不同的行为特征,这就是多态。上面这段代码看不出多态的用途。实际上,多态是一种非常灵活的编程机制。假如定义一个画布类(Canvas),这个画布类定义一个 draw_pic() 方法,该方法负责绘制各种图形。Canvas 类的代码如下所示:
class Canvas:
    def draw_pic(self, shape):
        print("---开始绘图---")
        shape.draw(self)

Canvas 类中的 draw_pic() 方法需要传入一个 shape 参数,该方法就是调用 shape 参数的 draw() 方法将自己绘制到画布上。这个 draw() 方法有何种行为或者执行怎样的绘制行为,这与 draw_pic() 方法是完全分离的,这为编程增加了很大的灵活性。下面代码定义了三个图形类,并为它们都提供 draw() 方法,这样它们就能以不同的行为绘制在画布上,这是多态的实际应用。代码如下:
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 0x0000020A988E0C18>上绘制矩形
---开始绘图---<__main__.Canvas object at 0x0000020A988E0C18>上绘制三角形
---开始绘图---<__main__.Canvas object at 0x0000020A988E0C18>上绘制圆形
上面示例可以看到 Python 多态的优势。在代码中涉及到 Canvas 类的 draw_pic() 方法时,该方法所需要的参数非常灵活,程序为该方法传入的参数对象只要具有指定方法就行,至于该方法呈现怎样的行为特征,完全取决于对象本身,这大大提高了 draw_pic() 方法的灵活性。

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 = [1, 2, 3]
print('[1, 2, 3]是否是 list 类的实例:', isinstance(my_list, list))         # 输出:True
print('[1, 2, 3]是否是 object 类及其子类的实例:', isinstance(my_list, object)) # 输出:True
print('list是否是 object 类的子类:', issubclass(list, object))             # 输出:True
print('[1, 2, 3]是否是 tuple 类的实例:', isinstance(my_list, tuple))       # 输出:False
print('list 是否是 tuple 类的子类:', issubclass(list, tuple))              # 输出:False

issubclass()和isinstance()两个函数语法基本相同,issubclass()函数的第一个参数是类名,isinstance()的第一个参数是变量。issubclass 用于判断是否为子类,isinstance用于判断是否为该类或子类的实例。这两个函数的第二个参数都可以使用元组。示例如下:
data = ('michael', 25)
print('data是否为列表或元组的实例:', isinstance(data, (list, tuple)))      # 输出:True
print('str是否为list或tuple的子类:', issubclass(str, (list, tuple)))       # 输出:False
print('str是否为list、tuple、object的子类:', issubclass(str, (list, tuple, object)))   # 输出:True

Python 为所有类提供了一个 __bases__ 属性,通过该属性可查看该类的所有直接父类,并返回所有直接父类组成的元组。另外,还提供了一个 __subclasses__()方法,通过该方法可查看该类的所有直接子类,并返回该类的所有子类组成的列表。示例如下:
class A:
    pass
class B:
    pass
class C(A, B):
    def hello(self):
        print("hello")

print("类A的所有父类:", A.__bases__)      # 注意输出是元组
print("类B的所有父类:", B.__bases__)
print("类C的所有父类:", C.__bases__)
print("类A的所有子类:", A.__subclasses__())   # 注意输出是列表
print("类B的所有子类:", B.__subclasses__())
for s in B.__subclasses__():
    s().hello()

运行代码,输出如下所示:
类A的所有父类: (<class 'object'>,)
类B的所有父类: (<class 'object'>,)
类C的所有父类: (<class '__main__.A'>, <class '__main__.B'>)
类A的所有子类: [<class '__main__.C'>]
类B的所有子类: [<class '__main__.C'>]
hello
从输出可知,在定义类时没有显式指定父类,则这些类默认的父类是 object 类。通过 __subclasses__() 方法获得的直接子类列表,也可用于调用子类的方法。

六、 枚举类

Python 中某些类的对象(实例)是有限且固定的,这种实例有限且固定的类,称为枚举类。

1、 枚举基础
两种方式定义枚举类:
(1)、直接使用 Enum 列出多个枚举值来创建枚举类。
(2)、通过继承 Enum 基类来派生枚举类。

下面代码直接使用 Enum 列出多个枚举值来创建枚举类:
import enum
# 定义Season 枚举类,注意变量名和类名要一致
Season = enum.Enum('Season', ('SPRING', 'SUMMER', 'FALL', 'WINTER'))
Enum()函数(Enum的构造方法)可用来创建枚举类,该构造方法的第一个参数是枚举类的类名;第二个参数是一个元组,用于列出所有枚举值。这里定义的是 Season 枚举类,有4个成员(或枚举值),每个成员有 name、value 两个属性,其中 name 属性值为枚举值的变量名,value 是枚举值的序号(序号从1开始)。下面代码测试枚举成员用法:
# 直接访问指定枚举
print(Season.SPRING)            # 输出:Season.SPRING
# 访问枚举成员的变量名
print(Season.SPRING.name)       # 输出:SPRING
# 访问枚举成员的值
print(Season.SPRING.value)      # 输出:1

还可通过枚举变量名或枚举值访问指定枚举对象,示例如下:
# 根据枚举变量名访问枚举对象
print(Season['SUMMER'])         # 输出:Season.SUMMER
# 根据枚举值访问枚举对象
print(Season(3))                # 输出:Season.FALL

枚举还有一个 __members__ 属性,该属性返回一个 dict 字典,字典包含了该枚举的所有枚举实例。可遍历 __members__ 属性来访问枚举的所有实例。示例如下:
# 遍历 Season 枚举的所有成员
for name, member in Season.__members__.items():
    print(name, '=>', member, ',', member.value)

输出如下所示:
SPRING => Season.SPRING , 1
SUMMER => Season.SUMMER , 2
FALL => Season.FALL , 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 , 北
上面代码中定义的 Orientation 枚举类是继承了 Enum,这种可以为枚举类定义额外的方法,如 info() 方法,还可为枚举指定字符串(str)类型的值(默认是1,2,3,...)。在访问枚举时也可通过值来访问,如代码中的 Orientation('南')。此外,枚举类中的方法由枚举类中的成员调用,self 参数就指向调用的成员。

2、 枚举构造器
枚举也是类,也可以有构造器。为枚举定义构造器后,创建实例时必须为构造器参数设置值。示例如下:
import enum
class Gender(enum.Enum):
    # 枚举成员
    MALE = '', '阳刚之力'
    FEMAL = '', '温柔之美'
    # 定义构造函数
    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.FEMAL.name)
# 访问 FEMALE 的 value
print('FEMALE 的 value: ', Gender.FEMAL.value)
# 访问自定义的 cn_name 属性
print('FEMALEE 的 cn_name: ', Gender.FEMAL.cn_name)
# 访问自定义的 desc 属性
print('FEMAIL 的 desc:', Gender.FEMAL.desc)

运行程序,输出如下所示:
FEMALE 的 name:  FEMAL
FEMALE 的 value:  ('', '温柔之美')
FEMALEE 的 cn_name:  女
FEMAIL 的 desc: 温柔之美
上面代码中定义的 Gender 枚举类有两个成员 MALE、FEMALE,并且这个枚举类定义了一个构造器,这个构造器需要传入两个参数 cn_name 和 desc。枚举类中的方法和属性都由枚举类中的成员调用,构造方法也不例外,因此构造方法需要传入的两个参数由成员的值提供,这里枚举类中成员的值是元组类型。所以,枚举的构造器需要几个参数,在定义枚举成员时就必须要指定几个值。

七、小结

Python面向对象的基础知识,以及定义类,为类定义变量、方法,创建类的对象(实例)。Python 的方法特征,Python 的类相当于类命名空间,在类中定义的方法位于类命名空间内,因此使用类调用方法非常灵活,类不仅可以调用类方法、静态方法,也可以使用未绑定的方式调用实例方法。

Python 面向对象的三大特征:封装、继承和多态,Python 通过双下划线方式隐藏类中的成员。Python 的多继承机制以及多继承导致的问题和注意点。Python 的多态大大提高Python 编程的灵活性。


posted @ 2019-10-15 16:19  远方那一抹云  阅读(427)  评论(0编辑  收藏  举报