Python 支持面向对象编程,能完全以面向对象的方式编程。Python 的面向对象相对其他编程语言来说要简单些。在 Python 中创建一个类和对象很容易。
面向对象的三大特征:封装、继承和多态,子类继承父类同样可以继承到父类的变量和方法。
一、 类和对象
类是面向对象的重要内容,可把类当成一种自定义类型,可以使用类定义变量,也可使用类创建对象。
1、 定义类
面向对象程序设计两个重要概念:类(class)和对象(object)。其中对象也被称为实例(instance),类是某一批对象的抽象,也可把类看成是某种概念;对象才是具体存在的实体。就像“人”是“人类”这个类的对象(或实例)一样。
Python 定义类的简单语法如下:
class 类名:
执行语句...
零个到多个类变量...
零个到多个方法...
类名只要是一个合法标识符即可。但从程序可读性来看,类名应由一个或多个有意义的单词连接而成,每个单词首字母大写,其他字母小写,单词与单词之间不使用分隔符。
Python 的类体是以冒号(:)作为开始,以统一缩进的部分作为类体。要注意定义类和定义函数的区别,定义函数用的是 def 关键字,定义类用的是 class 关键字。
在定义类是时,不为类定义任何类变量和方法,则这个类就是一个空类,可使用 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() 代码所示。