http://c.biancheng.net/view/2287.html
1.1定义类和对象
在面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体。
Python 类所包含的最重要的两个成员就是变量和方法,其中类变量属于类本身,用于定义该类本身所包含的状态数据:而实例变量则属于该类的对象,用于定义对象所包含的状态数据:方法则用于定义该类的对象的行为或功能实现。
Python 是一门动态语言,因此它的类所包含的类变量可以动态增加或删除(程序在类体中为新变量赋值就是增加类变量),程序也可在任何地方为已有的类增加变量;程序可通过 del 语句删除己有类的类变量。
下面程序将定义一个 Person 类:
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 类代码定义了一个构造方法,该构造方法只是方法名比较特殊:init,该方法的第一个参数同样是 self,被绑定到构造方法初始化的对象。
与函数类似的是,Python 也允许为类定义说明文档,该文档同样被放在类声明之后、类体之前,如上面程序中第二行的字符串所示。
1.2 类对象的创建和使用
创建对象的根本途径是构造方法,调用某个类的构造方法即可创建这个类的对象,Python 无须使用 new 调用构造方法。
如下代码示范了调用 Person 类的构造方法:
# 调用Person类的构造方法,返回一个Person对象
# 将该Person对象赋给p变量
p = Person()
创建对象之后,接下来即可使用该对象了。Python 的对象大致有如下作用:
1.操作对象的实例变量(包括访问实例变量的值、添加实例变量、删除实例变量)。
2.调用对象的方法。
下面代码通过 Person 对象来调用 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
1.3 对象动态添加变量和方法
Python 是动态语言,当然也允许为对象动态增加方法。比如上面程序中在定义 Person 类时只定义了一个 say() 方法,但程序完全可以为 p 对象动态增加方法。
但需要说明的是,为 p 对象动态增加的方法,Python 不会自动将调用者自动绑定到第一个参数(即使将第一个参数命名为 self 也没用)。例如如下代码:
# 先定义一个函数
def info(self):
print("---info函数---", self)
# 使用info对p的foo方法赋值(动态绑定方法)
p.foo = info
# Python不会自动将调用者绑定到第一个参数,
# 因此程序需要手动将调用者绑定为第一个参数
p.foo(p) # ①
# 使用lambda表达式为p对象的bar方法赋值(动态绑定方法)
p.bar = lambda self: print('--lambda表达式--', self)
p.bar(p) # ②
上面的第 5 行和第 11 行代码分别使用函数、lambda 表达式为 p 对象动态增加了方法,但对于动态增加的方法,Python 不会自动将方法调用者绑定到它们的第一个参数,因此程序必须手动为第一个参数传入参数值,如上面程序中 ① 号、② 号代码所示。
如果希望动态增加的方法也能自动绑定到第一个参数,则可借助于 types 模块下的 MethodType 进行包装。例如如下代码:
def intro_func(self, content):
print("我是一个人,信息为:%s" % content)
# 导入MethodType
from types import MethodType
# 使用MethodType对intro_func进行包装,将该函数的第一个参数绑定为p
p.intro = MethodType(intro_func, p)
# 第一个参数已经绑定了,无需传入
p.intro("生活在别处")
正如从上面代码所看到的,通过 MethodType 包装 intr_func 函数之后(包装时指定了将该函数的第一个参数绑定为 p),为 p 对象动态增加的 intro() 方法的第一个参数己经绑定,因此程序通过 p 调用 intro() 方法时无须传入第一个参数,就像定义类时己经定义了 intro() 方法一样。
1.4 实例方法和自动绑定self
对于在类体中定义的实例方法,Python 会自动绑定方法的第一个参数(通常建议将该参数命名为 self),第一个参数总是指向调用该方法的对象。根据第一个参数出现位置的不同,第一个参数所绑定的对象略有区别:
1.在构造方法中引用该构造方法正在初始化的对象。
2.在普通实例方法中引用调用该方法的对象。
由于实例方法(包括构造方法)的第一个 self 参数会自动绑定,因此程序在调用普通实例方法、构造方法时不需要为第一个参数传值。
self 参数(自动绑定的第一个参数)最大的作用就是引用当前方法的调用者,比如前面介绍的在构造方法中通过 self 为该对象增加实例变量。也可以在一个实例方法中访问该类的另一个实例方法或变量。假设定义了一个 Dog 类,这个 Dog 对象的 run() 方法需要调用它的 jump() 方法,此时就可通过 self 参数作为 jump() 方法的调用者。
方法的第一个参数所代表的对象是不确定的,但它的类型是确定的,即它所代表的只能是当前类的实例;只有当这个方法被调用时,它所代表的对象才被确定下来谁在调用这个方法,方法的第一个参数就代表谁。
例如定义如下 Dog 类:
class Dog:
# 定义一个jump()方法
def jump(self):
print("正在执行jump方法")
# 定义一个run()方法,run()方法需要借助jump()方法
def run(self):
# 使用self参数引用调用run()方法的对象
self.jump()
print("正在执行run方法")
上面代码的 run() 方法中的 self 代表该方法的调用者:谁在调用 run() 方法,那么 self 就代表谁。因此该方法表示:当一个 Dog 对象调用 run() 方法时,run() 方法需要依赖它自己的 jump() 方法。
dog = Dog()
dog.run()
在现实世界里,对象的一个方法依赖另一个方法的情形很常见,例如,吃饭方法依赖拿筷子方法,写程序方法依赖敲键盘方法,这种依赖都是同一个对象的两个方法之间的依赖。
当 Python 对象的一个方法调用另一个方法时,不可以省略 self。也就是说,将上面的 run()方法改为如下形式是不正确的:
class Dog:
# 定义一个jump()方法
def jump(self):
print("正在执行jump方法")
# 定义一个run()方法,run()方法需要借助jump()方法
def run(self):
#省略self,下面代码会报错
jump()
print("正在执行run方法")
dog = Dog()
dog.run()
2.1 类调用实例方法
在 Python 的类体中定义的方法默认都是实例方法,也示范了通过对象来调用实例方法。
现在问题来了,如果使用类调用实例方法,那么该方法的第一个参数(self)怎么自动绑定呢?例如如下程序:
class User:
def walk (self):
print(self, '正在慢慢地走')
# 通过类调用实例方法
User.walk()
请看程序最后一行代码,调用 walk() 方法缺少传入的 self 参数,所以导致程序出错。这说明在使用类调用实例方法时,Python 不会自动为第一个参数绑定调用者。实际上也没法自动绑定,因此实例方法的调用者是类本身,而不是对象。
如果程序依然希望使用类来调用实例方法,则必须手动为方法的第一个参数传入参数值。例如,将上面的粗体字代码改为如下形式:
u = User()
# 显式为方法的第一个参数绑定参数值
User.walk(u)
此代码显式地为 walk() 方法的第一个参数绑定了参数值,这样的调用效果完全等同于执行 u.walk()。
实际上,当通过 User 类调用 walk() 实例方法时,Python 只要求手动为第一个参数绑定参数值,并不要求必须绑定 User 对象,因此也可使用如下代码进行调用:
# 显式为方法的第一个参数绑定fkit字符串参数值
User.walk('fkit')
总结:
Python 的类可以调用实例方法,但使用类调用实例方法时,Python 不会自动为方法的第一个参数 self 绑定参数值;程序必须显式地为第一个参数 self 传入方法调用者。这种调用方式被称为“未绑定方法”。
2.2 静态方法和类方法的区别和应用
实际上,Python 完全支持定义类方法,甚至支持定义静态方法。Python 的类方法和静态方法很相似,它们都推荐使用类来调用(其实也可使用对象来调用)。
类方法和静态方法的区别在于,Python会自动绑定类方法的第一个参数,类方法的第一个参数(通常建议参数名为 cls)会自动绑定到类本身;但对于静态方法则不会自动绑定。
使用 @classmethod 修饰的方法就是类方法;使用 @staticmethod 修饰的方法就是静态方法。
下面代码示范了定义类方法和静态方法:
class Bird:
# classmethod修饰的方法是类方法
@classmethod
def fly (cls):
print('类方法fly: ', cls)
# staticmethod修饰的方法是静态方法
@staticmethod
def info (p):
print('静态方法info: ', p)
# 调用类方法,Bird类会自动绑定到第一个参数
Bird.fly() #①
# 调用静态方法,不会自动绑定,因此程序必须手动绑定第一个参数
Bird.info('crazyit')
# 创建Bird对象
b = Bird()
# 使用对象调用fly()类方法,其实依然还是使用类调用,
# 因此第一个参数依然被自动绑定到Bird类
b.fly() #②
# 使用对象调用info()静态方法,其实依然还是使用类调用,
# 因此程序必须为第一个参数执行绑定
b.info('fkit')
使用 @classmethod 修饰的方法是类方法,该类方法定义了一个 cls 参数,该参数会被自动绑定到 Bird 类本身,不管程序是使用类还是对象调用该方法,Python 始终都会将类方法的第一个参数绑定到类本身,如 ① 号、② 号代码的执行效果。
上面程序还使用 @staticmethod 定义了一个静态方法,程序同样既可使用类调用静态方法,也可使用对象调用静态方法,不管用哪种方式调用,Python 都不会为静态方法执行自动绑定。
在使用 Python 编程时,一般不需要使用类方法或静态方法,程序完全可以使用函数来代替类方法或静态方法。但是在特殊的场景(比如使用工厂模式)下,类方法或静态方法也是不错的选择。
2.3 @函数装饰器及用法
前面介绍的 @staticmethod 和 @classmethod 的本质就是函数装饰器,其中 staticmethod 和 classmethod 都是 Python 内置的函数。
使用 @ 符号引用已有的函数(比如 @staticmethod、@classmethod)后,可用于修饰其他函数,装饰被修饰的函数。那么我们是否可以开发自定义的函数装饰器呢?
答案是肯定的。当程序使用“@函数”(比如函数 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
上面程序使用 @funA 修饰 funB,这意味着程序要完成两步操作:
1.将 funB 作为 funA() 的参数,也就是上面代码中 @funA 相当于执行 funA(funB)。
2.将 funB 替换成上一步执行的结果,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)
上面程序定义了一个装饰器函数 foo,该函数执行完成后并不是返回普通值,而是返回 bar 函数(这是关键),这意味着被该 @foo 修饰的函数最终都会被替换成 bar 函数。
上面程序使用 @foo 修饰 my_test() 函数,因此程序同样会执行 foo(my_test),并将 my_test 替换成 foo() 函数的返回值:bar 函数。所以,上面程序第二行粗体字代码在打印 my_test 函数时,实际上输出的是 bar 函数,这说明 my_test 已经被替换成 bar 函数。接下来程序两次调用 my_test() 函数,实际上就是调用 bar() 函数。
通过 @ 符号来修饰函数是 Python 的一个非常实用的功能,它既可以在被修饰函数的前面添加一些额外的处理逻辑(比如权限检查),也可以在被修饰函数的后面添加一些额外的处理逻辑(比如记录日志),还可以在目标方法抛出异常时进行一些修复操作……这种改变不需要修改被修饰函数的代码,只要增加一个修饰即可。
上面介绍的这种在被修饰函数之前、之后、抛出异常后增加某种处理逻辑的方式,就是其他编程语言中的 AOP(Aspect Orient Progiuning,面向切面编程)。
下面例子示范了如何通过函数装饰器为函数添加权限检查的功能。程序代码如下:
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)
上面程序使用 @auth 修饰了 test() 函数,这会使得 test() 函数被替换成 auth() 函数所返回的 auth_fn 函数,而 auth_fn 函数的执行流程是:
1.先执行权限检查;
2.回调被修饰的目标函数。简单来说,auth_fn 函数就为被修饰函数添加了一个权限检查的功能。
2.4 类命名空间
再次重申,Python 的类就像命名空间。Python 程序默认处于全局命名空间内,类体则处于类命名空间内,Python 允许在全局范围内放置可执行代码,当 Python 执行该程序时,这些代码就会获得执行的机会。类似地,Python 同样允许在类范围内放置可执行代码,当 Python 执行该类定义肘,这些代码同样会获得执行的机会。
例如,如下程序测试了类命名空间:
class Item:
# 直接在类空间中放置执行性质代码
print('正在定义Item类')
for i in range(10):
if i % 2 == 0 :
print('偶数:', i)
else:
print('奇数:', i)
正如从上面代码所看到的,程序直接在 Item 类体中放置普通的输出语句、循环语句、分支语句,这都是合法的。当程序执行 Item 类时,Item 类命名空间中的这些代码都会被执行。
从执行效果来看,这些可执行代码被放在 Python 类命名空间与全局空间并没有太大的区别。确实如此,这是因为程序并没有定义“成员”(变量或函数),这些代码执行之后就完了,不会留下什么。
但下面代码就有区别。下面代码示范了在全局空间和类命名空间内分别定义 lambda 表达式:
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 表达式,在全局空间内定义的 lambda 表达式就相当于一个普通函数,因此程序使用调用函数的方式来调用该 lambda 表达式,并显式地为第一个参数绑定参数值,如上面程序中 ① 号代码所示。
对于在类命名空间内定义的 lambda 表达式,则相当于在该类命名空间中定义了一个函数,这个函数就变成了实例方法,因此程序必须使用调用方法的方式来调用该 lambda 表达式,Python 同样会为该方法的第二个参数(相当于 self 参数)绑定参数值,如上面程序中 ② 号代码所示。
3.1 类变量和实例变量
在类体内定义的变量,默认属于类本身。如果把类当成类命名空间,那么该类变量其实就是定义在类命名空间内的变量,在类命名空间内定义的变量就属于类变量,Python 可以使用类来读取、修改类变量。
例如,下面代码定义了一个 Address 类,并为该类定义了多个类变量:
class Address :
detail = '广州'
post_code = '510660'
def info (self):
# 尝试直接访问类变量
#print(detail) # 报错
# 通过类来访问类变量
print(Address.detail) # 输出 广州
print(Address.post_code) # 输出 510660
# 通过类来访问Address类的类变量
Address.detail
addr = Address()
addr.info()
# 修改Address类的类变量
Address.detail = '佛山'
Address.post_code = '460110'
addr.info()
该程序中,第二、三行代码为 Address 定义了两个类变量。
对于类变量而言,它们就是属于在类命名空间内定义的变量,因此程序不能直接访问这些变量,必须使用类名来调用类变量。不管是在全局范围内还是函数内访问这些类变量,都必须使用类名进行访问。
当程序第一次调用 Address 对象的 info() 方法输出两个类变量时,将会输出这两个类变量的初始值。接下来程序通过 Address 类修改了两个类变量的值,因此当程序第二次通过 info() 方法输出两个类变量时,将会输出这两个类变量修改之后的值。
实际上,Python 完全允许使用对象来访问该对象所属类的类变量(当然还是推荐使用类访问类变量)。例如如下程序:
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 对象来访问这两个类变量。
在上面程序的 Record 类的 info() 方法中,程序使用 self 访问 Record 类的类变量,此时 self 代表 info() 方法的调用者,也就是 Record 对象,因此这是合法的。
在主程序代码区,程序创建了 Record 对象,并通过对象调用 Record 对象的 item、date 类变量,这也是合法的。
实际上,程序通过对象访问类变量,其本质还是通过类名在访问类变量。
由于通过对象访问类变量的本质还是通过类名在访问,因此如果类变量发生了改变,当程序访问这些类变量时也会读到修改之后的值。例如为程序增加如下代码:
# 修改Record类的两个类变量
Record.item = '键盘'
Record.date = '2016-08-18'
# 调用info()方法
rc.info()
从上面的输出结果可以看到,通过实例访问类变量的本质依然是通过类名在访问。
需要说明的是,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
上面程序中的第 8、9 行代码通过实例对 item、quantity 变量赋值,看上去很像是对类变量赋值,但实际上不是,而是重新定义了两个实例变量(如果第一次调用该方法)。
上面程序在调用 Inventory 对象的 change() 方法之后,访问 Inventory 对象的 item、quantity 变量,由于该对象本身己有这两个实例变量,因此程序将会输出该对象的实例变量的值;接下来程序通过 Inventory 访问它的 item、quantity 两个类变量,此时才是真的访问类变量。
如果程序通过类修改了两个类变量的值,程序中 Inventory 的实例变量的值也不会受到任何影响。例如如下代码:
Inventory.item = '类变量item'
Inventory.quantity = '类变量quantity'
# 访问iv的item和quantity实例变量
print(iv.item)
print(iv.quantity)
上面程序开始就修改了 Inventory 类中两个类变量的值,但这种修改对 Inventory 对象的实例变量没有任何影响。
同样,如果程序对一个对象的实例变量进行了修改,这种修改也不会影响类变量和其他对象的实例变量。例如如下代码:
iv.item = '实例变量item'
iv.quantity = '实例变量quantity'
print(Inventory.item)
print(Inventory.quantity)
3.2 property函数:定义属性
如果为 Python 类定义了 getter、setter 等访问器方法,则可使用 property() 函数将它们定义成属性(相当于实例变量)。
property(fget=None, fset=None, fdel=None, doc=None)
从上面的语法格式可以看出,在使用 property() 函数时,可传入 4 个参数,分别代表 getter 方法、setter 方法、del 方法和 doc,其中 doc 是一个文档字符串,用于说明该属性。
当然,开发者调用 property 也可传入 0 个(既不能读,也不能写的属性)、1 个(只读属性)、2 个(读写属性)、3 个(读写属性,也可删除)和 4 个(读写属性,也可删除,包含文档说明)参数。
例如,如下程序定义了一个 Rectangle 类,该类使用 property() 函数定义了一个 size 属性:
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
程序中,使用 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属性,只传入2个参数
# 该属性是一个读写属性,但不能删除
fullname = property(getfullname, setfullname)
u = User('悟空', '孙')
# 访问fullname属性
print(u.fullname)
# 对fullname属性赋值
u.fullname = '八戒,朱'
print(u.first)
print(u.last)
此程序中使用 property() 定义了 fullname 属性,该程序使用 property() 函数时只传入两个参数,分别作为 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()
# 修改state属性
c.state = 'Alive'
# 访问state属性
print(c.state)
# 访问is_dead属性
print(c.is_dead)
上面程序中第 3 行代码使用 @property 修饰了 state() 方法,这样就使得该方法变成了 state 属性的 getter 方法。如果只有该方法,那么 state 属性只是一个只读属性。
当程序使用 @property 修饰了 state 属性之后,又多出一个 @state.setter 装饰器,该装饰器用于修饰 state 属性的 setter 方法,如上面程序中第 7 行代码所示。这样 state 属性就有了 getter 和 setter 方法,state 属性就变成了读写属性。
程序中第 15 行代码使用 @property 修饰了 is_dead 方法,该方法就会变成 is_dead 属性的 getter 方法。此处同样会多出一个 @is_dead.setter 装饰器,但程序并未使用该装饰器修饰 setter 方法,因此 is_dead 属性只是一个只读属性。
封装(Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以达到以下目的:
1)隐藏类的实现细节。
2)让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。
3)可进行数据检查,从而有利于保证对象信息的完整性。
4)便于修改,提高代码的可维护性。
为了实现良好的封装,需要从以下两个方面来考虑:
1)将对象的属性和实现细节隐藏起来,不允许外部直接访问。
2)把方法暴露出来,让方法来控制对这些属性进行安全的访问和操作。
因此,实际上封装有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。
Python 并没有提供类似于其他语言的 private 等修饰符,因此 Python 并不能真正支持隐藏。为了隐藏类中的成员,Python 玩了一个小技巧:只要将 Python 类的成员命名为以双下画线开头的,Python 就会把它们隐藏起来。
例如,如下程序示范了 Python 的封装机制:
class User :
def __hide(self):
print('示范隐藏的hide方法')
def getname(self):
return self.__name
def setname(self, name):
if len(name) < 3 or len(name) > 8:
raise ValueError('用户名长度必须在3~8之间')
self.__name = name
name = property(getname, setname)
def setage(self, age):
if age < 18 or age > 70:
raise ValueError('用户名年龄必须在18在70之间')
self.__age = age
def getage(self):
return self.__age
age = property(getage, setage)
# 创建User对象
u = User()
# 对name属性赋值,实际上调用setname()方法
u.name = 'fk' # 引发 ValueError: 用户名长度必须在3~8之间
上面程序将 User 的两个实例变量分别命名为 name 和 age,这两个实例变量就会被隐藏起来,这样程序就无法直接访问 name、age 变量,只能通过 setname()、getname()、setage()、getage() 这些访问器方法进行访问,而 setname()、setage() 会对用户设置的 name、age 进行控制,只有符合条件的 name、age 才允许设置。
u.name = 'fkit'
u.age = 25
print(u.name) # fkit
print(u.age) # 25
从该程序可以看出封装的好处,程序可以将 User 对象的实现细节隐藏起来,程序只能通过暴露出来的 setname()、setage() 方法来改变 User 对象的状态,而这两个方法可以添加自己的逻辑控制,这种控制对 User 的修改始终是安全的。
上面程序还定义了一个 hide() 方法,这个方法默认是隐藏的。如果程序尝试执行如下代码:
# 尝试调用隐藏的__hide()方法
u.__hide()
最后需要说明的是,Python 其实没有真正的隐藏机制,双下画线只是 Python 的一个小技巧,Python 会“偷偷”地改变以双下画线开头的方法名,会在这些方法名前添加单下画线和类名。因此上面的 __hide() 方法其实可以按如下方式调用(通常并不推荐这么干):
# 调用隐藏的__hide()方法
u._User__hide()
类似的是,程序也可通过为隐藏的实例变量添加下画线和类名的方式来访问或修改对象的实例变量。例如如下代码:
# 对隐藏的__name属性赋值
u._User__name = 'fk'
# 访问User对象的name属性(实际上访问__name实例变量)
print(u.name)
总结:
Python 并没有提供真正的隐藏机制,所以 Python 类定义的所有成员默认都是公开的;如果程序希望将 Python 类中的某些成员隐藏起来,那么只要让该成员的名字以双下画线开头即可。
即使通过这种机制实现了隐藏,其实也依然可以绕过去。