• part6-1 Python 类和对象(动态增加和删除类的实例方法、自动绑定self、类调用实例方法、类方法和静态方法、函数装饰器、类命名空间)



    Python 支持面向对象编程,能完全以面向对象的方式编程。Python 的面向对象相对其他编程语言来说要简单些。在 Python 中创建一个类和对象很容易。

    面向对象的三大特征:封装、继承和多态,子类继承父类同样可以继承到父类的变量和方法。

    一、 类和对象

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

    1、 定义类
    面向对象程序设计两个重要概念:类(class)和对象(object)。其中对象也被称为实例(instance),类是某一批对象的抽象,也可把类看成是某种概念;对象才是具体存在的实体。就像“人”是“人类”这个类的对象(或实例)一样。

    Python 定义类的简单语法如下:
    class 类名:
        执行语句...
        零个到多个类变量...
        零个到多个方法...
    类名只要是一个合法标识符即可。但从程序可读性来看,类名应由一个或多个有意义的单词连接而成,每个单词首字母大写,其他字母小写,单词与单词之间不使用分隔符。

    Python 的类体是以冒号(:)作为开始,以统一缩进的部分作为类体。要注意定义类和定义函数的区别,定义函数用的是 def 关键字,定义类用的是 class 关键字。

    在定义类是时,不为类定义任何类变量和方法,则这个类就是一个空类,可使用 pass 语句作为占位符,例如下面这样定义:
    class Empty:
        pass
    通常情况下,定义空类没有多大的实际意义。

    类中各成员之间的定义顺序没有规定,各成员之间可以相互调用。类包含的最重要的两个成员是变量和方法,其中类变量属于类本身,用于定义类本身所包含的状态数据;实例变量属于该类的对象,用于定义对象所包含的状态数据;方法用于定义该类的对象的行为或功能实现。

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

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

    类中定义的方法默认是实例方法,实例方法的定义与函数方法的定义基本相同。不同之处是,实例方法的第一个参数会被绑定到方法的调用(该类的实例),所以实例方法至少应该定义一个参数,这个参数通常叫做 self。这个 self 参数的名字可以是任意参数名,之所以叫做 self,是约定俗成的,增加程序可读性。

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

    构造方法是一个类创建对象的根本途径,如果在定义类时没有定义任何构造方法,那么 Python 会自动为该类定义一个只包含一个self参数的默认的构造方法。示例如下:
    class Person:
        # 下面的 hair 变量是类变量
        hair = 'yellow'
        def __init__(self, name='michael', age=25):
            # 下面为 Person 对象增加两个实例变量
            self.name = name
            self.age = age
        # 下面定义一个 say 方法
        def say(self, content):
            print(content)
    这里为 Person 类定义了一个构造方法 __init__,该方法的第一个参数同样是 self,被绑定到构造方法初始化的对象。

    在类中可以定义说明文档,说明文档放在类声明之后,类体之前。

    定义完类之后,就可使用该类。类的大致作用有:定义变量、创建对象、派生子类

    2、对象的产生和使用
    创建对象的根本途径是构造方法,调用某个类的构造方法即可创建这个类的对象,Python 无须使用 new 调用构造方法。例如:
    # 调用 Person 类的构造方法,返回一个 Person 对象,并将该对象赋值给 p 变量
    p = Person()
    对象创建完后,就可使用对象,对象的大致作用有:
    (1)、操作对象的实例变量,可访问实例变量的值、添加实例变量、删除实例变量。
    (2)、调用对象的方法。

    对象访问方法或变量的语法是通过点(.)方式,例如:对象.变量|方法(参数)。在这种方式中,对象是主调者,用于访问该对象的变量或方法。下面用 Person 类的对象来调用 Person 的实例和方法。
    # 输出对象 p 的name, age 实例变量
    print(p.name, p.age)        # michael 25
    # 访问对象 p 的实例变量 name,并改变该实例变量的值
    p.name = 'stark'
    # 调用对象 p 的 say() 方法,say() 方法有两个形参,第一个 self 是自动绑定的,在调用时只需要为第二个形参指定一个值
    p.say("Hello, Python !")
    # 再次输出对象 p 的 name、age实例变量
    print(p.name, p.age)        # stark 25
    这里访问对象 p 的实例变量 name、age 是在 Person 类中定义的。在 Person 类的构造方法中,有下面这两行代码:
    self.name = name
    self.age = age
    这两行代码用传入的 name、age 参数对 self 的 name、age变量赋值。这个 self 参数是自动绑定的(在构造方法中就自动绑定到该构造方法初始化的对象),这两行代码是对 self 的 name、age变量赋值,也就是对该构造方法初始化的对象(这里是p对象)的name、age 变量赋值,也即为对象 p 增加了 name、age 两个实例变量。

    此外,上面的代码中对象 p 还调用了 say() 方法,在调用方法时必须为形参赋值。say() 方法的第一个形参 self 自动绑定的,这里就绑定到方法的调用者(对象 p),因此代码中只为 say() 方法传入一个字符串作为参数,这个字符串传递给了 content 参数。

    定义类是为重复创建该类的对象,同一个类的多个对象具有相同的特征,类就定义了多个对象的共同特征。类定义了多个特征,从某个角度来看,类不是一个具体存在的实体,对象才是一个具体存在的实体。就好比每一个人是具体存在的实体,是人类这个类的对象。

    3、 实例变量删除和动态增加对象方法
    实例变量可以动态增加和动态删除。为新变量赋值即可增加动态变量(如:p.a = 10);使用 del 语句可动态删除实例变量。示例如下:
    # 为 p 对象动态增加一个 skills 实例变量
    p.skills = ['reading', 'eating']
    print(p.skills)         # ['reading', 'eating']
    # 删除对象 p 的实例变量 name
    del p.name
    # 再次访问对象 p 的实例变量 name
    print(p.name)           # 报错:AttributeError
    上面这段代码中,p.skills = ['reading', 'eating'] 这一行为对象 p 动态增加一个 skills 实例变量,并对这个变量赋值即可动态新增一个实例变量。del p.name 这行代码删除对象 p 的实例变量,再次访问这个实例变量时程序就会报错AttributeError,并提示:'Person' object has no attribute 'name'

    Python 的动态语言特性体现在还可以为对象动态增加方法。前面定义的 Person 类中只定义了一个 say() 方法,程序中还可以为对象 p 动态增加方法。要注意的是,为对象 p 动态增加的方法,Python 不会自动将调用者自动绑定到第一个参数(即使第一个参数是self也没用)。示例如下:
    # 先定义一个函数 hello
    def hello(self):
        print("hello function: ", self)
    # 使用 hello 函数对对象 p 的 foo 方法赋值,动态增加方法
    p.foo = hello
    # 由于不会将调用者自动绑定到第一个参数,因此要手动将调用者绑定到第一个参数
    p.foo(p)        # 输出:hello function:  <__main__.Person object at 0x000001FE6035DC50>
    p.foo('michael')    # 输出:hello function:  michael
    
    # 使用 lambda 表达式为对象 p 的 bar 方法赋值,动态增加方法
    p.bar = lambda self: print('lambda expression', self)
    p.bar(p)        # 输出:lambda expression <__main__.Person object at 0x000001FE6035DC50>
    上面代码中,分别使用函数、labmda 表达式为 p 对象动态增加方法,对于动态增加的方法,Python 不会将调用者自动绑定到它们的第一个参数,在程序中必须手动为第一个参数传入参数值。当然也可为动态增加的方法自动绑定到第一个参数,这要借助于 types 模块的MethodType 类进行包装。示例如下:
    def reading(self, bookname):
        print("%s在读%s书籍" % (self.name, bookname))
    # 导入 MethodType 类
    from types import MethodType
    # 使用 MethodType 对 reading 函数进行包装,将该函数的第一个参数绑定为 p 对象
    p.read = MethodType(reading, p)
    # 现在调用动态方法时,第一个参数就不需要传入
    p.read('python')        # 输出:michael在读python书籍
    上面代码中,通过 MethodType 包装后的 reading 函数(包装时指定了函数的第一个参数绑定对象p),为对象 p 增加的动态方法read 指向 reading 函数,此时调用这个动态方法(read)无须传入 reading 函数的第一个参数,就像是在类中已经定义的方法一样调用。

    4、 实例方法和自动绑定 self
    在类中定义的实例方法,Python 会自动为方法绑定第一个参数(self),这个参数指向调用该方法的对象。第一个参数出现位置的不同,第一个参数所绑定的对象略有区别。
    (1)、在构造方法中指向该构造方法正在初始化的对象。
    (2)、在普通实例方法中指向调用该方法的对象。

    self 参数的作用是引用当前方法的调用者。还可以在一个实例方法中访问该类的另一个实例方法或变量。例如假设在前面的 Person 类中还有一个实例方法 eat(),在这个方法中可通过 self 参数调用该类中的 say() 方法,调用方式是:self.say()。

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

    需要注意的是,在类中的一个方法中调用另一个方法时,不可以省略 self,例如:self.say() 中的 self 参数不能省略。

    此外,在类的构造方法中,self 参数代表该构造方法正在初始化的对象。例如 p=Person() 这行代码在初始化对象 p,此时在构造方法中的 self 参数就代表 p 这个对象。

    自动绑定的 self 参数并不依赖具体的调用方式,不管是以方法调用还是以函数调用的方式执行它,self 参数一样可以自动绑定。示例如下:
    class User:
        def test(self):
            print('self参数:', self)
    
    # 初始化对象 u
    u = User()
    # 以方法形式调用 test() 方法
    u.test()        # 输出:self参数: <__main__.User object at 0x000001DBF1A2DC50>
    # 将 User 对象的 test 方法赋值给 foo 变量
    foo = u.test
    # 通过 foo 变量(函数)形式调用 test() 方法
    foo()           # 输出:self参数: <__main__.User object at 0x000001DBF1A2DC50>
    上面代码中,u.test() 以方法形式调用 User 对象的 test() 方法。foo = u.test 这行代码将 User 类的 test() 方法赋值给foo 变量,接下来通过函数形式(foo())调用 User 对象的 test() 方法。两种调用方式中都自动绑定了第一个参数 self。所以上面两种调用方式中输出结果是相同的。

    当 self 参数作为对象的默认引用时,程序可以像访问普通变量一样来访问这个 self 参数,甚至可以把 self 参数当成实例方法的返回值。示例如下:
    class ReturnSelf:
        def grow(self):
            if hasattr(self, 'age'):    # 判断 self 是否有 age 属性,有就将其值增加1,否则就为 self 增加一个 age 属性
                self.age += 1
            else:
                self.age = 1
            # return self 返回调用该方法的对象
            return self
    rs = ReturnSelf()
    # 在方法中返回了 self 参数,可以连续调用同一个方法
    rs.grow().grow().grow()
    print("rs的age属性值是:", rs.age)        # 输出:rs的age属性值是: 3
    从上面代码中可看出,在某个方法中把 self 参数作为返回值,则可以多次连续调用同一个方法,使得代码更简洁。要注意的是,这种把self 参数作为返回值的方法可能会造成实际意义的模糊。例如这里的 grow() 方法代表对象的生长,不应该有返回值。

    二、方法

    方法是类或对象的行为特征的抽象,在 Python 类中的方法也是函数,其定义方式、调用形式都与函数相似。

    1、 类也能调用实例方法
    Python 的类可看做是一个命名空间,当程序在类中定义变量、方法时,与在类外边定义变量、函数没有多大的区别。示例如下:
    # 定义全局空间的 foo 函数
    def foo():
        print("全局空间的 foo 函数")
    # 定义全局空间的 me 变量
    me = 25
    class My:
        # 定义 My 空间的 foo 函数,注意没有 self 参数
        def foo():
            print("My 空间的 foo 函数")
        # 定义 My 空间的 me 变量
        me = 100
    
    # 调用全局空间的函数和变量
    foo()           # 全局空间的 foo 函数
    print(me)       # 25
    # 调用 My 空间的函数和变量
    My.foo()        # My 空间的 foo 函数
    print(My.me)    # 100
    这段代码中在全局空间和 My 类(My 空间)中分别定义了 foo() 函数和 me 变量,它们的定义方式没有任何区别,只是在 My 类中定义时需要缩进。接下来在调用 My 空间的 foo() 函数和 me 变量时,只要添加 My 前缀即可,这表明了可通过 My 类来调用 foo() 函数,同时证明了类可以调用实例方法。

    在使用类调用实例方法时,Python 不会自动为第一个参数绑定调用者。因为此时实例方法的调用者是类本身,而不是对象。例如下面程序所示:
    class User:
        def walk(self):
            print(self, "在学走路")
    # 通过类调用实例方法,未绑定 self 参数
    User.walk()
    
    运行代码,输出错误提示:
    TypeError: walk() missing 1 required positional argument: 'self'
    如果要正常调用这个 walk() 函数,可以先实例化一个对象,将这个对象传递给 walk() 方法。调用形式如下:
    u = User()
    # 显式的为方法的第一个参数绑定参数值
    User.walk(u)
    这种调用方式与 u.walk() 的调用方式是一样的。要通过 User 类调用 walk() 实例方法时,只要为第一个参数绑定参数值,并不一定要求是 User 对象,因此调用方式可改为如下形式:
    # 显式的为方法的第一个参数绑定 michael 字符串参数值
    User.walk('michael')        # michael 在学走路
    通过这种方式绑定后,michael 字符串就传递给 walk() 方法的第一个参数 self。因此程序能正常输出。

    小结:Python 的类可以调用实例方法,但不会为实例方法的第一个参数 self 绑定参数值。因此,在调用时必须显式的为第一个参数 self 传入方法调用者。这种调用方式叫作“未绑定方法”。

    2、 类方法和静态方法
    Python 的类方法和静态方法有些相似,都推荐使用类来调用(也可使用对象来调用)。区别在于:在调用类方法时,会自动绑定第一个参数,类方法的第一个参数(cls)会自动绑定到类本身;静态方法在调用时不会自动绑定

    类方法的修饰符是:@classmethod
    静态方法的修饰符是:@staticmethod

    示例如下:
    class Brid:
        # 使用 @classmethod 修饰符,下面的方法是类方法
        @classmethod
        def fly(cls):
            print("类方法 fly: ", cls)
    
        # 使用 @staticmethod 修饰符,下面的方法是静态方法
        @staticmethod
        def info(p):
            print("静态方法 info: ", p)
    
    # 调用类方法,Bird 类会自动绑定到类方法的第一个参数
    Brid.fly()
    # 调用静态方法,不会自动绑定,只能手动绑定第一个参数
    Brid.info('michael')
    
    b = Brid()      # 创建 Brid 对象
    # 下面使用对象 b 调用类方法 fly(),实际仍然是类调用的,因此第一个参数被自动绑定到 Brid 类
    b.fly()
    # 下面使用对象 b 调用静态方法 info(),实际仍然是类调用的,因此必须手动绑定第一个参数
    b.info('stark')
    
    运行上面这估代码,输出如下所示:
    类方法 fly:  <class '__main__.Brid'>
    静态方法 info:  michael
    类方法 fly:  <class '__main__.Brid'>
    静态方法 info:  stark
    在上面的 Brid 类中,定义了一个@classmethod修饰的类方法 fly(),该类方法的第一个参数 cls 在调用时自动绑定到 Brid 类本身,不管程序是使用类调用还是对象调用该方法。从Brid.fly()、b.fly() 的输出即可证实。

    在 Brid 中还定义了一个 @staticmethod 修饰的静态方法 info(),该方法同样可使用类调用和对象调用,不管用何种调用方式,在调用时都不会为静态方法自动绑定。

    在编程时,不常用类方法或静态方法,可使用函数来代替类方法或静态方法。在某些特殊场景(工厂模式)下,类方法或静态方法是不错的选择。

    3、 @ 函数装饰器
    前面用过的 @classmethod 和 @staticmethod 本质是函数装饰器,其中 classmethod 和 staticmethod 都是 Python 的内置函数。

    使用 @ 符号引用已有的函数后,可用于修饰其他函数,装饰被修饰的函数。在程序中使用“@函数”(比如函数A)装饰另一个函数(函数B)时,实际完成下面两步:
    (1)、将被修饰的函数(函数B)作为参数传给 @ 符号引用的函数(函数A)。
    (2)、将函数B替换(装饰)成第(1)步的返回值。

    被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西。通过下面示例理解装饰器:
    def funA(fn):
        print("in funA")
        fn()        # 执行传入的 fn 参数
        return 'michael'
    """
    下面的装饰效果相当于 funA(funB),funB 将会被替换(装饰)成该语句的返回值。
    由于 funA 函数返回 michael,因此 funB 就是 michael
    """
    @funA
    def funB():
        print("in funB")
    print(funB)         # 输出:michael
    
    运行程序,输出如下所示:
    in funA
    in funB
    michael
    代码中使用 @funA 装饰 funB,程序要完成两步操作。
    (1)、将 funB 作为 funA 的参数,也就是 @funA 这行代码相当于执行 funA(funB)。
    (2)、将 funB 替换为第(1)步执行的结果,这里 funA 执行完成后返回 michael,因此 funB 就不再是函数,而被替换成一个字符串。

    装饰器作用:被修饰的函数总是被替换成@符号所引用的函数的返回值,因此被修饰的函数会变成什么,完全由@符号所引用的函数的返回值决定,如果@符号引用的函数返回值是函数,那么被修饰的函数在替换之后还是函数。

    下面用更复杂的函数装饰器来理解,代码如下:
    def foo(fn):
        # 定义一个嵌套函数
        def bar(*args):
            print("1、", args)
            n = args[0]
            print("2、", n * (n - 1))
            # 查看传给 foo 函数的 fn 函数的名字
            print(fn.__name__)
            # 调用 fn 函数
            fn(n * (n - 1))
            print("-" * 15)
            return fn(n * (n - 1))
        return bar
    """
    下面的装饰效果相当于 foo(my_test),my_test 将会被替换(装饰)成该语句的返回值。
    由于 foo() 函数返回 bar 函数,因此 my_test 就是 bar。
    """
    @foo
    def my_test(a):
        print("my_test函数:", a)
    
    # 打印 my_test 函数,将看到实际上是 bar 函数
    print(my_test)      # <function foo.<locals>.bar at 0x000002677C35E2F0>
    
    # 下面代码看似是在调用 my_test() 函数,实际调用的是 bar() 函数
    my_test(10)
    my_test(6, 5)
    
    
    运行程序,输出如下所示:
    <function foo.<locals>.bar at 0x000002677C35E2F0>
    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
    在这段代码中,定义了一个装饰器函数 foo(),该函数执行完后并不是返回普通值,而是返回 bar 函数(关键点),这意味着被 @foo 装饰的函数最终会被替换成 bar 函数。

    这里使用 @foo 装饰 my_test() 函数,因此程序同样会执行 foo(my_test),并将 my_test 替换成 foo() 函数的返回值 bar() 函数。所以 print(my_test) 这行代码在打印 my_test 函数时,实际输出的是 bar() 函数,这说明 my_test 已经被替换成 bar 函数。接着的 my_test(10)、my_test(6, 5) 两次调用 my_test() 函数,实际上调用的是 bar() 函数。

    使用 @ 符号装饰函数是 Python 的一个实用功能,在不改变被装饰函数的代码、调用方式的基础上,为被装饰函数添加额外的功能,可以在被装饰函数的前面和后面添加额外的处理逻辑。还可以在目标方法抛出异常时进行一些修复操作等等。

    这种在被装饰函数之前、之后、抛出异常增加某种处理逻辑的方式,就是其他编程语言中的 AOP(Aspect Orient Programming,面向切面编程)。

    下面代码示例用函数装饰器为函数添加权限查检功能。代码如下:
    def auth(fn):
        def auth_fn(*args):
            # 用一条语句模拟权限检查
            print("===模拟执行权限检查===")
            # 回调被修饰的目标函数
            fn(*args)
        return auth_fn
    
    @auth
    def test(a, b):
        print("执行test函数,参数a: %s,参数b: %s" % (a, b))
    
    # 调用 test() 函数,实际调用装饰后返回的 auth_fn 函数
    test('michael', 25)
    
    运行程序,输出如下所示:
    ===模拟执行权限检查===
    执行test函数,参数a: michael,参数b: 25
    这里使用 @auth 装饰 test() 函数,这使得 test() 函数被替换成 auth() 函数所返回的 auth_fn() 函数,在 auth_fn 函数中的执行流程是:(1)先执行权限检查;(2)回调被装饰的目标函数。

    4、 类命名空间
    Python 程序默认处于全局命名空间内,类体处于类命名空间内,Python 允许在全局范围内放置可执行代码。当 Python 执行该程序时,这些代码就获得执行的机会;同样的,在类范围内也允许放置可执行代码,当程序执行该类定义时,这些代码同样会获得执行机会。

    下面代码测试类命名空间,代码如下:
    class Item:
        # 直接在类命名空间中放置可执行代码
        print('正在定义 Item 类')
        for i in range(10):
            if i % 2 == 0:
                print("偶数:", i)
            else:
                print("奇数:", i)
    在这个 Item 类体中,直接放入普通的输出语句、循环语句、分支语句,这些都是允许的。运行这段程序时,Item 类命名空间中的这些代码都会被执行。这些可执行代码的执行效果与全局空间并没有太大区别。这是因为在类中没有定义“成员”(变量或函数),这些代码执行之后就完了,不会留下什么。

    如果在全局空间和类命名空间分别定义函数后,就会有区别,示例如下:
    global_fn = lambda p: print('执行全局 lambda 表达式,p参数是:', p)
    class Category:
        cate_fn = lambda p: print("执行类中的 lambda 表达式,p 参数是:", p)
    
    # 调用全局空间的 global_fn ,为参数 p 传入参数值
    global_fn('stark')
    
    c = Category()
    # 调用类命名空间内的 cate_fn ,Python 自动绑定第一个参数
    c.cate_fn()
    
    运行代码输出如下:
    执行全局 lambda 表达式,p参数是: stark
    执行类中的 lambda 表达式,p 参数是: <__main__.Category object at 0x000001DEA4D0FFD0>
    这里分别在全局命名空间、类命名空间内定义了两个 lambda 表达式。全局空间的 lambda 表达式就是一个普通函数,因此程序使用调用普通函数的方式调用该 lambda 表达式,并为第一个参数指定参数值。如:global_fn('stark') 代码所示。

    在类命名空间内定义的 lambda 表达式,就是该类命名空间中定义的一个函数,这个函数成了实例方法,因此程序必须使用调用实例方法的方式来调用该 lambda 表达式,Python 会为该方法的第一个参数(相当于 self 参数)绑定参数值。如:c.cate_fn() 代码所示。

  • 相关阅读:
    CSS 选择器
    HTML lable和fieldset
    html image和表格
    HTML a标签
    html 提交后台的标签
    HTML INPUT系列使用
    HTML内标签、换行
    HTML 头部详解
    单例模式
    const 指针的三种使用方式
  • 原文地址:https://www.cnblogs.com/Micro0623/p/11662925.html
Copyright © 2020-2023  润新知