class 后面紧接着是类名,即 Student,类名通常是大写开头的单词,紧
接着是(object),表示该类是从哪个类继承下来的
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数
永远是实例变量 self,并且,调用时,不用传递该参数。除此之外,类
的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变
参数、关键字参数和命名关键字参数。
类是抽象的模板,比如 Student 类,而实例是根据类创建出来的一个个
具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同
类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有
的数据都互相独立,互不影响;
方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例
的数据;
通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知
道方法内部的实现细节。
和静态语言不同,Python 允许对实例变量绑定任何数据,也就是说,对
于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名
称都可能不同
,实例的变量名如果以__开头,就变成了一个私有变量
(private),只有内部可以访问,外部不能访问,
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限
制的保护,代码更加健壮。
是如果外部代码要获取 name 和 score 怎么办?可以给 Student 类增加
get_name 和 get_score 这样的方法
如果又要允许外部代码修改 score 怎么办?可以再给 Student 类增加
set_score 方法
原先那种直接通过 bart.score = 59 也可以修改啊,为什么
要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传
入无效的参数:
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。
不能直接访问__name 是因为 Python 解释器对外把__name 变量改成了
_Student__name,所以,仍然可以通过_Student__name 来访问__name 变量:
但是强烈建议你不要这么干,因为不同版本的 Python 解释器可能会把
__name 改成不同的变量名。
总的来说就是,Python 本身没有任何机制阻止你干坏事,一切全靠自觉。
继承有什么好处?最大的好处是子类获得了父类的全部功能。
继承的第二个好处需要我们对代码做一点改进。
当子类和父类都存在相同的 run()方法时,我们说,子类的 run()覆盖了
父类的 run(),在代码运行的时候,总是会调用子类的 run()。这样,我
们就获得了继承的另一个好处:多态。
所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数
据类型也可以被看做是父类。但是,反过来就不行:
对于一个变量,我们只需要知道它是 Animal 类型,无需确切地知道它的
子类型,就可以放心地调用 run()方法,而具体调用的 run()方法是作用
在 Animal、Dog、Cat 还是 Tortoise 对象上,由运行时该对象的确切类型
决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们
新增一种 Animal 的子类时,只要确保 run()方法编写正确,不用管原来
的代码是如何调用的。这就是著名的“开闭”原则:
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象
只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类
只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。
动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
Python 的“file-like object“就是一种鸭子类型。对真正的文件对象,它有
一个 read()方法,返回其内容。但是,许多对象,只要有 read()方法,
都被视为“file-like object“。许多函数接收的参数就是“file-like object“,
你不一定要传入真正的文件对象,完全可以传入任何实现了 read()方法
的对象
换句话说,isinstance()判断的是一个对象是否是该
类型本身,或者位于该类型的父继承链上。
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就
可以判断是否是 list 或者 tuple:
isinstance([1, 2, 3], (list, tuple))
类似__xxx__的属性和方法在 Python 中都是有特殊用途的,比如__len__
方法返回长度。在 Python 中,如果你调用 len()函数试图获取一个对象
的长度,实际上,在 len()函数内部,它自动去调用该对象的__len__()
方法
如果要获得一个对象的所有属性和方法,可以使用 dir()函数,它返回
一个包含字符串的 list,比如,获得一个 str 对象的所有属性和方法
我们自己写的类,如果也想用 len(myObj)的话,就自己写一个__len__()
方法:
仅仅把属性和方法列出来是不够的,配合 getattr()、setattr()以及
hasattr(),我们可以直接操作一个对象的状态:
hasattr(obj, 'x') # 有属性'x'吗?
setattr(obj, 'y', 19) # 设置一个属性'y'
getattr(obj, 'y') # 获取属性'y'
如果试图获取不存在的属性,会抛出 AttributeError 的错误:
可以传入一个 default 参数,如果属性不存在,就返回默认值:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值 404
也可以获得对象的方法:
getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>> >>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量 fn >>> fn # fn 指向 obj.power <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>> >>> fn() # 调用 fn()与调用 obj.power()是一样的 81
通过内置的一系列函数,我们可以对任意一个 Python 对象进行剖析,
拿到其内部的数据
假设我们希望从文件流 fp 中读取图像,我们首先要判断该 fp 对象是否
存在 read 方法,如果存在,则该对象是一个流,如果不存在,则无法读
取。hasattr()就派上了用场。
请注意,在 Python 这类动态语言中,根据鸭子类型,有 read()方法,不
代表该 fp 对象就是一个文件流,它也可能是网络流,也可能是内存中
的一个字节流,但只要 read()方法返回的是有效的图像数据,就不影响
读取图像的功能
由于 Python 是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过 self 变量:
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例
都可以访问到
在编写程序的时候,千万不要把实例属性和类
属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是
当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
静态语言 vs 动态语言
对于静态语言(例如 Java)来说,如果需要传入 Animal 类型,则传入的
对象必须是 Animal 类型或者它的子类,否则,将无法调用 run()方法。
对于 Python 这样的动态语言来说,则不一定需要传入 Animal 类型。我
们只需要保证传入的对象有一个 run()方法就可以了:
class Timer(object): def run(self): print('Start...')
但是,给一个实例绑定的方法,对另一个实例是不起作用的:
为了给所有实例都绑定方法,可以给 class 绑定方法:
from types import MethodType
>>> def set_score(self, score): ...
self.score = score ...
>>> Student.set_score = MethodType(set_score, Student)
给 class 绑定方法后,所有实例均可调用:
通常情况下,上面的 set_score 方法可以直接定义在 class 中,但动态绑
定允许我们在程序运行的过程中动态给 class 加上功能,这在静态语言
中很难实现。
但是,如果我们想要限制实例的属性怎么办?比如,只允许对 Student
实例添加 name 和 age 属性。
为了达到限制的目的,Python 允许在定义 class 的时候,定义一个特殊
的__slots__变量,来限制该 class 实例能添加的属性:
class Student(object): __slots__ = ('name', 'age') # 用 tuple 定义允许绑定的属性名称
由于'score'没有被放到__slots__中,所以不能绑定 score 属性,试图绑
定 score 将得到 AttributeError 的错误。
使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对
继承的子类是不起作用的:
除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自
身的__slots__加上父类的__slots__。
@property 的实现比较复杂,我们先考察如何使用。把一个 getter 方法变
成属性,只需要加上@property 就可以了,此时,@property 本身又创建
了另一个装饰器@score.setter,负责把一个 setter 方法变成属性赋值,
于是,我们就拥有一个可控的属性操作:
property 广泛应用在类的定义中,可以让调用者写出简短的代码,同时
保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich
继承自 Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以
实现,比如,让 Ostrich 除了继承自 Bird 外,再同时继承 Runnable。这
种设计通常称之为 MixIn
MixIn 的目的就是给一个类增加多个功能,这样,在设计类的时候,我
们优先考虑通过多重继承来组合多个 MixIn 的功能,而不是设计多层次
的复杂的继承关系
由于 Python 允许使用多重继承,因此,MixIn 就是一种常见的设计。
只允许单一继承的语言(如 Java)不能使用 MixIn 的设计
看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,这些在
Python 中是有特殊用途的。
Python 的 class 中还有许多这样有特殊用途的函数,可以帮
助我们定制类
__iter__
如果一个类想被用于 for ... in 循环,类似 list 或 tuple 那样,就必须实
现一个__iter__()方法,该方法返回一个迭代对象,然后,Python 的 for
循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,
直到遇到 StopIteration 错误时退出循环
class Fib(object): def __init__(self): self.a, self.b = 0, 1 # 初始化两个计数器 a,b
def __iter__(self): return self # 实例本身就是迭代对象,故返回自己
def __next__(self): self.a, self.b = self.b, self.a + self.b # 计算下一个值 if self.a > 100000: # 退出循环的条件 raise StopIteration(); return self.a # 返回下一个值
要表现得像 list 那样按照下标取出元素,需要实现__getitem__()方法:
class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a
任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。
其实,更多的时候,我们
需要判断一个对象是否能被调用,能被调用的对象就是一个 Callable 对
象,比如函数和我们上面定义的带有__call__()的类实例:
Python 的 class 允许定义许多定制方法,可以让我们非常方便地生成特
定的类。
更多定制方法:https://docs.python.org/3/reference/datamodel.html#special-method-names