• Python学习笔记:类和对象


    第六章 类和对象

    在设计之初,Python就被设计成支持面向对象的编程语言,因此python完全能以面向对象的方式编程。
    在python中创建一个类和对象都很容易,python支持面向对象的三大特征:封装、继承和多态,子类继承父类同样可以继承到父类的变量和方法。

    类和对象

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

    定义类

    在面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,对象才是具体存在的实体。
    python定义类的简单语法如下:

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

    从程序的可读性来看,python的类名必须是由一个或多个有意义的单词连缀而成,每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。
    python的类定义有点像函数定义,都是以冒号(:)作为类体的开始,以统一缩进的部分作为类体的。区别只是 函数定义使用def关键字,而类定义则使用class关键字。
    python的类定义由类头(指class关键字和类名部分)和统一缩进的类体组成,在类体中最主要的两个成员就是类变量和方法。如果不为类定义任何变量和方法,那么这个类就相当于一个空类,如果空类不需要其他可执行语句,则可使用pass语句作为占位符。示例:

    class Empty
        pass
    

    Python类所包含的最重要的两个成员就是变量和方法,其中类变量属于类本身,用于定义该类本身所包含的状态数据;而实例变量则属于该类的对象,用于定义对象所包含的状态数据;方法则用于定义该类的对象的行为或功能实现。
    在类中定义的方法默认是实例方法,定义实例方法的方法与定义函数的方法基本相同,只是实例方法的第一个参数会被绑定到方法的调用者(该类的实例) --- 因此实例方法至少应该定义一个参数,该参数通常会被命名为self。
    注意:
    实例方法的第一个参数并不一定要叫self,其实完全可以叫任意参数名,只是约定俗称地把该参数命名为self,这样具有更好的可读性。
    在实例方法中有一个特别的方法:init,这个方法被称为构造方法。构造方法用于构造该类的对象,python通过调用构造方法返回该类的对象(无须使用new)
    构造方法是一个类创建对象的根本途径,如果开发者没有为该类定义任何构造方法,那么python会自动为该类定义一个只包含一个self参数的默认的构造方法。

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

    上面的Person类定义了一个构造方法,该构造方法只是方法名比较特殊:init,该方法的第一个参数同样是self,被绑定到构造方法初始化的对象。
    Python也允许为类定义说明文档,该文档同样被放在类声明之后、类体之前,如上面所示。
    python的类大致有如下作用:

    • 定义变量
    • 创建对象
    • 派生子类

    对象的产生和使用

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

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

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

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

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

    class Person :
        '这是一个学习Python定义的一个Person类'
        # 下面定义了一个类变量
        hair = 'black'
        def __init__(self, name = 'Charlie', age=8):
            # 下面为Person对象增加2个实例变量
            self.name = name
            self.age = age
        # 下面定义了一个say方法
        def say(self, content):
            print(content)
    help(Person)
    # 调用Person类的构造方法,返回一个Person对象
    # 将该Person对象赋给p变量
    p = Person()
    # 输出p的name、age实例变量
    print(p.name, p.age)  # Charlie 8
    # 访问p的name实例变量,直接为该实例变量赋值
    p.name = '李刚'
    # 调用p的say()方法,声明say()方法时定义了2个形参
    # 但第一个形参(self)是自动绑定的,因此调用该方法只需为第二个形参指定一个值
    p.say('Python语言很简单,学习很容易!')
    # 再次输出p的name、age实例变量
    print(p.name, p.age)  # 李刚 8
    

    输出结果:

    Help on class Person in module __main__:
    
    class Person(builtins.object)
     |  Person(name='Charlie', age=8)
     |  
     |  这是一个学习Python定义的一个Person类
     |  
     |  Methods defined here:
     |  
     |  __init__(self, name='Charlie', age=8)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |  
     |  say(self, content)
     |      # 下面定义了一个say方法
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)
     |  
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |  
     |  hair = 'black'
    
    Charlie 8
    Python语言很简单,学习很容易!
    李刚 8
    

    对象的产生和使用

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

    # 为p对象增加一个skills实例变量
    p.skills = ['programming', 'swimming']
    print(p.skills)
    # 删除p对象的name实例变量
    del p.name
    # 再次访问p的name实例变量,报错
    print(p.name)  # AttributeError
    
    输出结果:
    ['programming', 'swimming']
    Traceback (most recent call last):
    
      File "C:Userszz.spyder-py3	emp.py", line 32, in <module>
        print(p.name)  # AttributeError
    
    AttributeError: 'Person' object has no attribute 'name'
    

    python是动态语言,当然也允许为对象动态增加方法,但需要说明的是,为p对象动态增加的方法,python不会自动将调用者自动绑定到第一个参数(即使第一个参数命名为self也没用),示例:

    class Person :
        '这是一个学习Python定义的一个Person类'
        # 下面定义了一个类变量
        hair = 'black'
        def __init__(self, name = 'Charlie', age=8):
            # 下面为Person对象增加2个实例变量
            self.name = name
            self.age = age
        # 下面定义了一个say方法
        def say(self, content):
            print(content)
    
    # 调用Person类的构造方法,返回一个Person对象
    # 将该Person对象赋给p变量
    p = Person()
    
    # 先定义一个函数
    def info(self):
        print("---info函数---", self)
    # 使用info对p的foo方法赋值(动态绑定方法)
    p.foo = info
    # Python不会自动将调用者绑定到第一个参数,
    # 因此程序需要手动将调用者绑定为第一个参数
    p.foo(p)  # ①
    

    输出结果:

    ---info函数--- <__main__.Person object at 0x000001FFE9821888>
    

    若使用lambda表达式,该如何写呢?

    # 使用lambda表达式为p对象的bar方法赋值(动态绑定方法)
    p.bar = lambda self: print('--lambda表达式--', self)
    p.bar(p) # ②
    

    如果不手动将调用者绑定为第一个参数,看看会发生什么?改为如下:

    # 先定义一个函数
    def info(self):
        print("---info函数---", self)
    # 使用info对p的foo方法赋值(动态绑定方法)
    p.foo = info
    p.foo()  # ①
    

    输出结果:

    Traceback (most recent call last):
    
      File "C:Userszz.spyder-py3	emp.py", line 24, in <module>
        p.foo()  # ①
    
    TypeError: info() missing 1 required positional argument: 'self'
    

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

    class Person :
        '这是一个学习Python定义的一个Person类'
        # 下面定义了一个类变量
        hair = 'black'
        def __init__(self, name = 'Charlie', age=8):
            # 下面为Person对象增加2个实例变量
            self.name = name
            self.age = age
        # 下面定义了一个say方法
        def say(self, content):
            print(content)
    
    # 调用Person类的构造方法,返回一个Person对象
    # 将该Person对象赋给p变量
    p = Person()
    
    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()方法一样。

    实例方法和自动绑定self

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

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

    由于实例方法(包括构造方法)的第一个self参数会自动绑定,因此程序在调用普通实例方法、构造方法时不需要为第一个参数传值。
    self参数(自动绑定的第一个参数)最大的作用就是引用当前方法的调用者。
    方法的第一参数所代表的对象是不确定的,但它的类型是确定的 --- 它所代表的的只能是当前类的实例;只有当这个方法被调用时,它所代表的对象才被确定下来 --- 谁在调用这个方法,方法的第一个参数就代表谁。

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

    这个Dog对象的run方法需要调用它的jump方法,需要通过self参数作为jump方法的调用者,如果没有声明使用self参数作为jump()方法的调用者,那么会报错,示例:

    class Dog:
        # 定义一个jump()方法
        def jump(self):
            print("正在执行jump方法")
        # 定义一个run()方法,run()方法需要借助jump()方法
        def run(self):
            # 使用self参数引用调用run()方法的对象
            jump()
            print("正在执行run方法")
    
    d = Dog()
    d.run()
    
    
    输出结果:
    Traceback (most recent call last):
    
      File "C:Userszz.spyder-py3	emp.py", line 12, in <module>
        d.run()
    
      File "C:Userszz.spyder-py3	emp.py", line 8, in run
        jump()
    
    NameError: name 'jump' is not defined
    

    当python对象的一个方法调用另一个方法时,不可以省略self。
    提示:
    从python的语言设计来看,Python的类、对象有点类似于一个命名空间,因此在调用类、对象的方法时,一定要加上“类.”或“对象.”的形式。如果直接调用某个方法,这种形式属于调用函数。
    在构造方法中,self参数(第一个参数)代表该构造方法正在初始化的对象。示例:

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

    需要说明的是,自动绑定的self参数并不依赖具体调用方式,不管是以方法调用还是以函数调用的方式执行它,self参数一样可以自动绑定,示例:

    class User:
        def test(self):
            print('self参数: ', self)
            
    u = User()
    # 以方法形式调用test()方法
    u.test() #  <__main__.User object at 0x000001FFE99AD188>
    # 将User对象的test方法赋值给foo变量
    foo = u.test
    # 通过foo变量(函数形式)调用test()方法。
    foo() # <__main__.User object at 0x000001FFE99AD188>
    
    
    输出结果:
    self参数:  <__main__.User object at 0x000001FFE99AD188>
    self参数:  <__main__.User object at 0x000001FFE99AD188>
    

    当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的age属性值是:", rs.age)
    
    
    输出结果:
    rs的age属性值是: 3
    

    方法

    方法是类或对象的行为特征的抽象,但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
    

    使用类调用实例方法时,python不会自动为第一个参数绑定调用者。实际上也没法自动绑定,因此实例方法的调用者是类本身,而不是对象。
    如果程序依然希望使用类来调用实例方法,则必须手动为方法的第一个参数传入参数值。示例:

    class User:
        def walk (self):
            print(self, '正在慢慢地走')
    # 通过类调用实例方法
    #User.walk()
    u = User()
    # 显式为方法的第一个参数绑定参数值
    User.walk(u)
    
    
    输出结果:
    <__main__.User object at 0x000001FFE9805CC8> 正在慢慢地走
    

    这样的调用效果完全等同于执行u.walk()
    实际上,当通过user类调用walk实例方法时,python只要求手动为第一个参数绑定参数值,并不要求必须绑定User对象,因此也可使用如下代码进行调用;

    class User:
        def walk (self):
            print(self, '正在慢慢地走')
    # 通过类调用实例方法
    #User.walk()
    u = User()
    
    # 显式为方法的第一个参数绑定fkit字符串参数值
    User.walk('fkit')
    
    输出结果:
    fkit 正在慢慢地走
    

    如果按照上面进行绑定,那么“fkit”字符串将会被传给walk()方法的第一个参数self。
    注意:
    Python的类可以调用实例方法,但使用类调用实力方法时,Python不会自动为方法的第一个参数self绑定参数值;程序必须显示地为第一个参数self传入方法调用者。这种调用方式被称为“未绑定方法”

    类方法与静态方法

    python完全支持定义类方法、静态方法。它们很相似,它们都体推荐使用类来调用(其实也可使用对象来调用)。类方法和静态方法的区别在于:类方法的第一个参数(通常建议参数名为cls)会自动绑定到类本身,不管是使用类还是对象调用该方法,类方法的第一个参数始终绑定到类本身;但对于静态方法则不会自动绑定,不管用哪种方式调用,python都不会为静态方法执行自动绑定。


    使用@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()类方法,其实依然还是使用类调用,
    # 因此第一个参数依然被自动绑定到Bird类
    b.fly()  #②
    # 使用对象调用info()静态方法,其实依然还是使用类调用,
    # 因此程序必须为第一个参数执行绑定
    b.info('fkit')
    
    
    输出结果:
    类方法fly:  <class '__main__.Bird'>
    静态方法info:  crazyit
    类方法fly:  <class '__main__.Bird'>
    静态方法info:  fkit
    

    在使用python编程时,一般不需要使用类方法或静态方法,程序完全可以使用函数来代替类方法或静态方法。

    @函数装饰器

    前面介绍的@classmethod、@staticmethod的本质就是函数装饰器。其中classmethod和staticmethod都是python内置的函数。
    我们也可以开发自定义的函数装饰器
    当程序使用“@函数”(比如函数A)装饰另一个函数(比如函数B)时,实际上完成如下两步:

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

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

    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(funB)
    2. 将funB替换成第1步执行的结果,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) # <function foo.<locals>.bar at 0x00000000021FABF8>
    # 下面代码看上去是调用my_test(),其实是调用bar()函数
    my_test(10)
    my_test(6, 5)
    
    输出结果:
    <function foo.<locals>.bar at 0x000001FFE9835318>
    ===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的一个非常实用的功能,它既可以在被修饰的函数前面加一些额外的处理逻辑(比如权限检查),也可以在被修饰的函数的后面添加一些额外的处理逻辑 (比如记录日志),还可以在目标方法抛出异常时进行一些修复操作....这种改变不需要修改被修饰函数的代码,只需要增加一个修饰即可。

    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, 15)
    
    输出结果:
    ----模拟执行权限检查----
    执行test函数,参数a: 20, 参数b: 15
    

    再论类命名空间

    再次重申:Python的类就像命名空间。python程序默认处于全局命名空间内,类体则处于类命名空间内,Python允许在全局范围内放置可执行代码 --- 当python执行该程序时,这些代码就获得执行的机会;类似地,Python允许在类范围内放置可执行代码 --- 当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')  # ①
    c = Category()
    # 调用类命名空间内的cate_fn,Python自动绑定第一个参数
    c.cate_fn()  # ②
    
    
    输出结果:
    执行lambda表达式,p参数:  fkit
    执行lambda表达式,p参数:  <__main__.Category object at 0x000001FFE99B0688>
    

    分别在全局命名空间、类命名空间内定义了两个lambda表达式,在全局命名空间内定义的lambda表达式就相当于一个普通函数,因此程序使用调用函数的方式来调用该lambda表达式,并显示地为第一个参数绑定参数值。
    对于在类命名空间内定义的lambda表达式,则相当于在该类命名空间中定义了一个函数,这个函数就变成了实例方法,因此程序必须使用调用方法的方式来调用该lambda表达式,python同样会为该方法的第一个参数(相当于self参数)绑定参数值。

    成员变量

    类变量和实例变量

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

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

    对于类变量而言,它们就是属于在类命名空间内定义的变量,因此程序不能直接访问这些变量,程序必须使用类名来调用类变量。不管是在全局范围内还是函数内访问这些类变量,都必须使用类名进行访问。
    实际上,完全允许使用对象来访问该对象所属类的类变量(当然还是推荐使用类访问类变量)

    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) # '2016-06-16'
    rc.info()
        
    # 修改Record类的两个类变量
    Record.item = '键盘'
    Record.date = '2016-08-18'
    # 调用info()方法
    rc.info()
    
    输出结果:
    鼠标
    2016-06-16
    info方法中:  鼠标
    info方法中:  2016-06-16
    info方法中:  键盘
    info方法中:  2016-08-18
    

    实际上,程序通过对象访问类变量,其本质还是通过类名在访问类变量。
    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) # 500
    # 访问Inventory的item和quantity类变量
    print(Inventory.item) # 鼠标
    print(Inventory.quantity) # 2000
    
    输出结果:
    显示器
    500
    鼠标
    2000
    

    如果程序通过类修改了两个类变量的值 ,程序中Inventory的实例变量也不会受到任何影响。同样,如果程序对对一个对象的实例变量进行了修改,这种修改也不会影响类变量和其他对象的实例变量。

    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
    print("---------------------")
    # 访问Inventory的item和quantity类变量
    print(Inventory.item) # 鼠标
    print(Inventory.quantity) # 2000
    
    print("---------------------")
    Inventory.item = '类变量item'
    Inventory.quantity = '类变量quantity'
    # 访问iv的item和quantity实例变量
    print(iv.item)
    print(iv.quantity)
    print("---------------------")
    iv.item = '实例变量item'
    iv.quantity = '实例变量quantity'
    print(Inventory.item)
    print(Inventory.quantity)
    
    
    输出结果:
    显示器
    500
    ---------------------
    鼠标
    2000
    ---------------------
    显示器
    500
    ---------------------
    类变量item
    类变量quantity
    

    使用property函数定义属性

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

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

    在使用property()函数时,可传入四个参数,分别代表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
         # 定义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
    print("----------------------")
    print(dir(Rectangle))
    
    
    输出结果:
    用于描述矩形大小的属性
    Help on property:
    
        用于描述矩形大小的属性
    
    (4, 3)
    9
    7
    0
    0
    ----------------------
    ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'delsize', 'getsize', 'setsize', 'size']
    

    该程序尝试对Rectangle对象的size属性进行读、写、删除操作,其中这种读、写、删除操作分别被委托给getsize()、setsize()和delsize()方法来实现。
    在使用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属性,只传入2个参数
        # 该属性是一个读写属性,但不能删除
        fullname = property(getfullname, setfullname)
    u = User('悟空', '孙')
    # 访问fullname属性
    print(u.fullname)
    # 对fullname属性赋值
    u.fullname = '八戒,朱'
    print(u.first)
    print(u.last)
    print("----------------")
    # 不能删除fullname属性,报错
    del u.fullname
    
    
    输出结果:
    悟空,孙
    八戒
    朱
    ----------------
    Traceback (most recent call last):
    
      File "C:Userszz.spyder-py3	emp.py", line 23, in <module>
        del u.fullname
    
    AttributeError: can't delete attribute
    

    提示:
    在某些编程语言中,类似于这种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
    

    隐藏和封装

    封装是面向对象的三大特征之一,它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
    为了实现良好的封装,需要从两个方面来考虑:

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

    实际上封装有两个方面的含义:把该隐藏的隐藏起来;把该暴露的暴露出来。
    为了隐藏类中的成员,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
    
    
    输出结果:
    fkit
    25
    

    如果程序尝试执行如下代码:

    u.__hide()
    

    将会提示错误:

    AttributeError: 'User' object has no attribute '__hide'
    

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

    # 调用隐藏的__hide()方法
    u._User__hide()
    

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

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

    总结:python并没有提供真正的隐藏机制。

    类的继承

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

    继承的语法

    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("不同食物的口感不同")
    
    # 定义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克
    不同食物的口感不同
    

    子类继承了父类,将可以继承得到父类定义的方法,这样子类就可复用父类的方法了。

    关于多继承

    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中方法: 这是一个商品
    

    由于item排在前面,因此item中定义的方法优先级更高,python会优先到Item父类中搜村方法,一旦在Item父类中搜村到目标方法,Python就不会继续向下搜寻了。

    重写父类的方法

    鸵鸟不能飞,需要重写父类方法

    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 类相当于类空间,因此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方法
    

    使用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):
    class Manager (Customer, Employee):
        pass
    #m = Manager(25000)
    m = Manager('IT产品', '广州')
    #m.work()  #①
    m.info()  #②
    
    
    输出结果:
    我是一个顾客,我的爱好是: IT产品,地址是广州
    

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

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

    使用super()构造方法最常用的做法就是不传入任何参数,然后通过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))
    # 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的构造方法--
    普通员工正在写代码,工资是: 25000
    我是一个顾客,我的爱好是: IT产品,地址是广州
    

    Python的多态性

    python是多态语言,多态语言的典型特征就是:类、对象的属性、方法都可以动态增加和修改。

    动态属性与_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、d2调用walk()方法
    d1.walk()
    d2.walk()
    
    输出结果:
    Garfield慢慢地走过一片草地
    Kitty慢慢地走过一片草地
    

    为类动态添加方法时不需要使用MethodType进行包装(为实例对象添加动态方法时,则需要),该函数的第一个参数会自动绑定。
    python的这种动态性固然有其优势,但也给程序带来了一定的隐患:程序定义好的类,完全有可能在后面被其他程序修改,这就带来了一些不确定性。如果程序要限制为某个类动态添加属性和方法,则可通过__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这3个属性或方法
    d.walk = MethodType(lambda self: print('%s正在慢慢地走' % self.name), d)
    d.age = 5
    d.walk()
    print("------------")
    d.foo = 30 # AttributeError
    
    输出结果:
    Snoopy正在慢慢地走
    ------------
    Traceback (most recent call last):
    
      File "C:Userszhanghu.spyder-py3	emp.py", line 14, in <module>
        d.foo = 30 # AttributeError
    
    AttributeError: 'Dog' object has no attribute 'foo'
    

    需要说明的是,__slots__属性并不限制通过类来动态添加属性和方法,示例:

    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这3个属性或方法
    d.walk = MethodType(lambda self: print('%s正在慢慢地走' % self.name), d)
    d.age = 5
    d.walk()
    print("------------")
    # __slots__属性并不限制通过类来动态添加方法
    Dog.bar = lambda self: print('abc')
    d.bar()
    
    
    输出结果:
    Snoopy正在慢慢地走
    ------------
    abc
    

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

    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这3个属性或方法
    d.walk = MethodType(lambda self: print('%s正在慢慢地走' % self.name), d)
    d.age = 5
    d.walk()
    print("------------")
    # __slots__属性并不限制通过类来动态添加方法
    Dog.bar = lambda self: print('abc')
    d.bar()
    
    print("------------")
    class GunDog(Dog):
        def __init__(self, name):
            super().__init__(name)
        pass
    gd = GunDog('Puppy')
    # 完全可以为Gundog实例动态添加属性
    gd.speed = 99
    print(gd.speed)
    
    
    输出结果:
    Snoopy正在慢慢地走
    ------------
    abc
    ------------
    99
    

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

    使用type()函数定义类

    前面提到使用type()函数可以查看变量的类型,那么只用它查看类的类型呢?

    class Role:
        pass
    r = Role()
    # 查看变量r的类型 
    print(type(r)) # <class '__main__.Role'>
    # 查看Role类本身的类型
    print(type(Role)) # <class 'type'>
    
    
    输出结果:
    <class '__main__.Role'>
    <class 'type'>
    

    程序使用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()
    print(Dog.age)
    
    输出结果:
    <class '__main__.Dog'>
    <class 'type'>
    fn函数
    6
    

    上面粗体字代码使用 type()定义了 一个 Dog 类。在使用 type()定义类时可指定三个参数。

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

    由此可见,上面粗体字代码定义了 一个 Dog 类 , 该类继承了 object 类 ,还为该类定义了 一个walk()方法和一个 age 类变量。
    从上面的输出结果可以看 出,使用 type() 函数定义的类与直接使用 class 定义的类井没有任何区别。事实上 , Python 解释器在执行使用 class 定义的类时,其实依然是使用 type()函数来创建类的 。因此,无论通过哪种方式定义类,程序最终都是创建一个type的实例。

    使用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)
    

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

    # 定义ItemMetaClass,继承type
    class ItemMetaClass(type):
        # cls代表动态修改的类
        # name代表动态修改的类名
        # bases代表被动态修改的类的所有父类
        # attr代表被动态修改的类的所有属性、方法组成的字典
        def __new__(cls, name, bases, attrs):
            print("类名称:",name)
            # 动态为该类添加一个cal_price方法
            attrs['cal_price'] = lambda self: self.price * self.discount
            return type.__new__(cls, name, bases, attrs)
    
    # 定义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
    # 定义Book类
    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
    
    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())
    
    
    输出结果:
    类名称: Book
    类名称: CellPhone
    67.64
    2039.1499999999999
    

    通过使用metaclass可以动态修改程序中的一批类,对它们集成进行某种修改。这个功能在开发一些基础性框架时非常有用,程序可以通过使用metaclass为某一批具有通用功能的类添加方法。

    多态

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

    多态性

    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指向的对象不同,因此它呈现出不同的行为特征,这就是多态。

    class Canvas:
        def draw_pic(self, shape):
            print('--开始绘图--')
            shape.draw(self)
    
    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())
    print(hasattr(c, 'draw_pic'))
    print(hasattr(c.draw_pic, '__call__'))
    print(Circle.__dict__)
    
    
    输出结果:
    --开始绘图--
    在<__main__.Canvas object at 0x0170C8D0>上绘制矩形
    --开始绘图--
    在<__main__.Canvas object at 0x0170C8D0>上绘制三角形
    --开始绘图--
    在<__main__.Canvas object at 0x0170C8D0>上绘制圆形
    True
    True
    {'__dict__': <attribute '__dict__' of 'Circle' objects>, '__weakref__': <attribute '__weakref__' of 'Circle' objects>, 'draw': <function Circle.draw at 0x0171A078>, '__module__': '__main__', '__doc__': None}
    

    检查类型

    枚举入门

    python提供了如下两个函数检查类型:

    • issubclass(cls,class_or_tuple):检查cls是否为后一个类或元组包含的多个类中任意类的子类
    • 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类的子类: ', issubclass(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('[2, 4]是否是list类的实例: ', isinstance(my_list, list))
    # [2, 4]是object类的子类的实例,输出True
    print('[2, 4]是否是object类及其子类的实例: ', isinstance(my_list, object))
    # list是object类的子类,输出True
    print('list是否是object类的子类: ', issubclass(list, object))
    # [2, 4]不是tuple类及其子类的实例,输出False
    print('[2, 4]是否是tuple类及其子类的实例: ', isinstance([2, 4], tuple))
    # list不是tuple类的子类,输出False
    print('list是否是tuple类的子类: ', issubclass(list, tuple))
    
    data = (20, 'fkit')
    print('data是否为列表或元组: ', isinstance(data, (list, tuple))) # True
    # str不是list或者tuple的子类,输出False
    print('str是否为list或tuple的子类: ', issubclass(str, (list, tuple)))
    # str是list或tuple或object的子类,输出True
    print('str是否为list或tuple或object的子类 ', issubclass(str, (list, tuple, object)))
    
    
    输出结果:
    "Hello"是否是str类的实例:  True
    "Hello"是否是object类的实例:  True
    str是否是object类的子类:  True
    "Hello"是否是tuple类的实例:  False
    str是否是tuple类的子类:  False
    [2, 4]是否是list类的实例:  True
    [2, 4]是否是object类及其子类的实例:  True
    list是否是object类的子类:  True
    [2, 4]是否是tuple类及其子类的实例:  False
    list是否是tuple类的子类:  False
    data是否为列表或元组:  True
    str是否为list或tuple的子类:  False
    str是否为list或tuple或object的子类  True
    

    python为所有类提供了一个__base__属性,通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。示例:

    class A:
        pass
    class B:
        pass
    class C(A, B):
        pass
    print('类A的所有父类:', A.__bases__)
    print('类B的所有父类:', B.__bases__)
    print('类C的所有父类:', C.__bases__)
    
    输出结果:
    类A的所有父类: (<class 'object'>,)
    类B的所有父类: (<class 'object'>,)
    类C的所有父类: (<class '__main__.A'>, <class '__main__.B'>)
    

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

    class A:
        pass
    class B:
        pass
    class C(A, B):
        pass
    print('类A的所有子类:', A.__subclasses__())
    print('类B的所有子类:', B.__subclasses__())
    
    输出结果:
    类A的所有子类: [<class '__main__.C'>]
    类B的所有子类: [<class '__main__.C'>]
    

    枚举类

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

    • 直接使用Enum列出多个枚举值来创建枚举类
    • 通过继承Enum基类来派生枚举类
    import enum
    # 定义Season枚举类
    Season = enum.Enum('Season', ('SPRING', 'SUMMER', 'FALL', 'WINTER'))
    # 直接访问指定枚举
    print(Season.SPRING)
    # 访问枚举成员的变量名
    print(Season.SPRING.name)
    # 访问枚举成员的值
    print(Season.SPRING.value)
    
    # 根据枚举变量名访问枚举对象
    print(Season['SUMMER']) # Season.SUMMER
    # 根据枚举值访问枚举对象
    print(Season(3)) # Season.FALL
    
    print("item:",Season.__members__.items())
    # 遍历Season枚举的所有成员
    for name, member in Season.__members__.items():
        print(name, '=>', member, ',', member.value)
        
    输出结果:
    Season.SPRING
    SPRING
    1
    Season.SUMMER
    Season.FALL
    item: ItemsView(OrderedDict([('SPRING', <Season.SPRING: 1>), ('SUMMER', <Season.SUMMER: 2>), ('FALL', <Season.FALL: 3>), ('WINTER', <Season.WINTER: 4>)]))
    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 , 北
    

    枚举的构造器

    枚举也是类,因此枚举也可以定义构造器。为枚举定义构造器之后,在定义枚举实例时必须为构造器参数设置值,示例:

    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: 柔顺之美
    

    枚举的构造器需要几个参数,就必须指定几个值。


    原文来源于我的语雀,我的微信公众号:细细研磨

  • 相关阅读:
    ARMV8 datasheet学习笔记3:AArch64应用级体系结构之Memory order
    ARMV8 datasheet学习笔记3:AArch64应用级体系结构之Atomicity
    ARMV8 datasheet学习笔记3:AArch64应用级体系结构
    ARMV8 datasheet学习笔记2:概述
    最短路径
    网络流
    二分图
    zabbix 3.4新功能值预处理
    zabbix 3.4新功能值解析——Preprocessing预处理
    Zabbix监控windows的CPU利用率和其他资源
  • 原文地址:https://www.cnblogs.com/onelikeone/p/15162961.html
Copyright © 2020-2023  润新知