- 类是一种数据结构, 可用来定义对象, 对象再把数据值和行为融合在一起, 编程形式上的现实世界的抽象实体
实例是类的一个具体信息(真正实物),创建一个实例的过程称作实例化
当创建一个类时, 实际也就创建了一个自己的数据类型(2.2以后类型和类进行了统一)
新式类和旧式类最大的不同:有没有从祖先类派生 (即新式类必须继承至少一个父类(
object
是默认父类), 旧式类不指定父类) - 类的简单用法:当作名称空间,作为容器对象来共享名称空间(名字空间容器) 一个类被定义的目的是为了当作一个模块来用
- 类的方法:只能被类实例所调用, 所有方法声明中都有
self
参数(自动由解释器传递), 代表实例对象本身(其他语言中是this
) __init__()
:相当于类构造器, 主要在实例化时被调用(相当于隐式调用), 实例化调用返回这个实例之前, 去执行一些特定代码(设置初始值、初步诊断) 实例化时,__init__()
中self
参数接收实例对象(self=实例名
), 该调用不应返回任何对象- 靠继承来进行子类化是创建和定制新类型的一种方式, 新的类将保有继承类的所有特性而不会改动原来类的定义
- 如果子类没有定义它的构造器, 基类的构造器将会被调用
- 当在子类构造器中显式调用父类的构造器时, 也要显式地把self传入, 因为我们是在一个子类中调用(而不是直接通过实例)
- 命名类(
class Xxx
)时通常由大写字母打头, 数据值应该使用名词(email/times), 方法使用谓词(update_date), 最好用骆驼记法(AddrBook) - 面向对象编程:实现了数据层与逻辑层的融合, 用一个可用以创建这些对象的简单抽象层来描述, 类提供定义, 实例负责实现
- OOD(面向对象设计)不会特别要求面向对象编程语言, 如纯结构化语言C, 不过当一语言内建OO特性, 开发会更方便高效
- OOP常用术语: 抽象/实现 封装/接口 合成 派生/继承/继承结构 泛化/特化 多态 自省/反射
- 抽象: 指对现实世界问题和实体的本质表现、行为和特征建模, 建立一个相关的子集, 可以用于描述程序结构, 从而实现这种模型
- 封装: 描述对数据/信息进行隐藏的概念, 它对数据属性提供接口和访问函数(python类属性是公开的, 所以要提供相应接口防止偷数据)
- 合成: 扩充对类的描述, 使得多个不同类合成为一个大的类来实现抽象模型, 形式:1)通过联合关系 2)通过聚合(封装的组件仅能用接口访问)
- 派生: 描述子类的创建, 新类保留父类中的数据和行为并可以独立修改
- 继承: 子类的属性从祖先类继承 多”代”派生就是一个继承结构(想成”族谱”)
- 泛化: 所有子类与其父类及祖先类有一样的特点
- 特化: 所有子类的自定义(即与祖先类不同的属性)
- 多态: 对象如何通过他们共同的属性和动作来操作及访问, 表明了动态绑定的存在, 允许重载及运行时类型确定和验证
- 自省: 即反射, 展示某对象是如何在运行期取得自身信息的(进行”手工检查类型”的工作能力,
dir()
、type()
)
- 类与函数的声明相似, 允许创建函数、闭包、方法等, 最不同在于调用函数会运行, 调用类是创建实例 类属性:数据属性(即所定义的类的变量), 方法(绑定/非绑定)
- 属性: 属于一个对象的数据或者函数元素, 通过句点属性标志符
.
访问 当访问一个属性时, 它同时也是一个对象, 拥有自己的属性, 可以访问 类属性仅与其被定义的类相绑定 类(实例也适用)的自省:用dir()
或者类的__dict__
属性 - 静态变量(静态数据): 这些数据与它们所属的类对象绑定, 不依赖于任何类实例, 通常仅用于追踪与类相关的值
- 特殊的类属性(自带):
字符串名__name__ 、字符串文档__doc__、父类组成的元祖__bases__、属性__dict__(访问类属性时的搜索源)、定义所在模块__module__(模块间的类继承) 、对应的类(新式)__class__、Cls.__dict__['method'] 等价于 Cls.method
- 从类型对象取对象类型名的方便方法:
type(obj).__name__
- 当访问一类属性时, 解释器将搜索
__dict__
, 若没有则从基类集的字典中再搜(从左到右),对类的修改会影响此类的字典(但基类的不变) - 创建实例 -> 检查
__init__()
方法 -> 返回实例化对象 -> 无引用计数时 -> 调用__del__()
方法 并运行垃圾对象回收机制 __new__()
:真正的”构造器”方法(是类方法, 需要显式传入类), 返回一个合法的实例, 传入的参数是在类实例化操作时生成的,会调用父类的__new__()
(会向上代理)__del__()
:”解构器”方法, 只能被调用一次, 在实例释放前提供特殊处理功能的方法, 实例对象的引用清除后才调用(不常实现, 实例很少显式释放) 不要忘记首先调用父类的__del__()
如果定义__del__()
的实例是某个循环的一部分, 垃圾回收器将不会终止这个循环(需要自己显式调用del)- 实例属性: 在构造(调用
__init__()
)时可以最早设置实例属性, 还可带默认参数(参数要求是不变的对象)- Python允许运行时创建实例属性
- 实例属性的
__dict__
中只有实例属性, 没有类属性或特殊属性, 但dir()
可以有
- 如果定义了构造器, 它不应当返回任何对象, 因为实例对象是自动在实例化调用后返回的
- 特殊的实例属性:
来自哪个类__class__、自己的属性__dict__
- 内建类型也是类, 也有可以通过
dir()
来获取属性, 但是没有__dict__
- 类是类属性的名字空间, 实例则是实例属性的
- 类属性访问顺序:实例中找 -> 类中找 -> 基类中找 (给一个与类属性同名的实例属性赋值(不可变类型), 可以有效的”隐藏”类属性)
- 类属性的修改会影响所有的实例(当一个实例在类属性被修改前/后创建, 更新类的值生效), 所以慎用 在python中没有明确方法来只是你想要修改同名的类属性
self
变量是在类实例方法中引用方法所绑定的实例, 用于关联类, 在方法调用中总是第一个参数- 绑定/非绑定:方法是类内部定义的函数, 方法只有在类有实例时才能被调用(绑定), 没有实例时方法就是未绑定的
当没有实例并且需要调用一个非绑定方法时必须传递
self
函数 非绑定方法应用场景:派生一个子类时, 需要调用父类中的某方法 - 静态方法: 不通过创建实例来调用方法的情况, 取代了用户需要在全局名字空间中创建函数的做法; 不用在方法声明中加参数
类方法: 需要第一个参数不是实例而是类, 由解释器传给方法(很多人用cls作为变量名字)
用装饰器
@staticmathod/classmethod
等价于 静态方法:staticmethod(method)
类方法:classmethod(method)
- 组合(合成):在一个类和其他类之间定义了一种“has-a”的关系 (例如C类有一个a类实例和一个b类实例)
- 可以包含其他对象的对象叫复合对象, 例如:列表,字典,类实例等
- 在一个层次的派生关系中相关类(垂直相邻)是父类和子类关系, 同一个父类的派生类(水平相邻)是同胞关系, 父类和所有高层类都被认为是祖先
- 文档字符串对类、函数/方法、模块来说都是唯一的,
__doc__
不会从基类中继承过来 - 如何调用被覆盖的基类方法(P357):
给出子类实例调用未绑定的基类方法
在子类的重写方法里显式调用基类方法
(PaCls.method(self))
用super(Child, self).method()
- 从标准类型派生:不可变/可变类型(不可变类型需要标准类的
__new__()
方法) - 多重继承关键点:正确找到没有在子类(当前)类中定义的属性; 正确调用对应父类方法及在子类中处理好自己的义务
MRO(Method Resolution Order):方法解释顺序; 每个类都有
__mro__
属性:由被搜索时的顺序组成的元祖 由于在新式类中需要出现基类, 这样就在继承结构中形成了一个菱形, 使得属性查找顺序被改变了 - 经典类使用深度优先算法(先垂直找最亲父类), 新式类的解释顺序是广度优先(先水平找同胞兄弟)
- 与类/实例相关的BIF:
判断是否派生类
issubclass(sub, tuple(sup))
、判断是否为实例(检查类型)isinstance(ins, tuple(cls))
、*attr(obj, 'attrname')
系列: 检查属性hasattr
, 取得属性getattr
(三参) 赋予属性setattr
(三参) 删属性delattr()
dir()
:- 用于实例:显示实例变量以及其类和基类的方法和类属性
- 用于类: 显示类以及它的所有基类中
__dict__
的内容, 但不会显示在元类中的类属性 - 用于模块:显示模块的
__dict__
的内容 - 无参数: 显示调用者的局部变量(即
locals.keys()
) - 除了实例变量名和常用方法外, 它还显示那些通过特殊标记来调用的方法
super(type, [obj])
:仅对新式类有效, 捕获父类; 通过类名和显式传入self
(优点:不需要给出任何基类名字), 调用类方法时常用- 如果obj是个实例
isinstance(obj, type)
为真 - 如果obj是个类或类型
issubclass(obj, type)
为真 - 如果希望父类被绑定, 可以传入
obj
参数 - 实际上
super()
创造了一个super object
, 为一个给定的类使用__mro__
去查找相应的父类
- 如果obj是个实例
vars()
:参数对象必须有__dict__
属性, 返回一个包含了__dict__
中属性和值的字典, 如果无参数则显示locals()
- 定制类: 可用于 1)模拟标准类型 2)重载操作符 特殊方法一览 P367
- “原位”操作符:用一个
i
代替星号的位置, 表示左结合操作与赋值的结合(num += num2)
__str__()
和__repr__()
实现定制类的字符串表示形式 (可以把__repr__()
作为__str__()
的一个别名)- 如果没有
__repr__()
, 不用print
进行内容显示时输出的是python的标准形式:<some_object_information>
self.__class__()
:调用实例化self
的那个类__i*__()
:进行增量赋值后必须返回self
__iter__()
将一个对象声明为迭代器, 返回self
- “原位”操作符:用一个
- 如果在重载情况下使用一个操作符却没有定义对应的特殊方法, 那么会发生
TypeError
- 定制类迭代器的应用:无穷迭代(无损读取序列) 任意项的迭代(根据参数控制条目)
- 双下划线(类元素级私有化):导入一个模块时不能直接访问到这些数据元素(防止在被外部导入时与同名变量相冲突),实际上会在名字前面加上下划线和类名(
_ClsN__AttrN
) 但只是一种对导入源代码无法获得的模块或对同一模块中的其他代码的保护机制 - 单下划线(模块级私有化): 可以防止被
from X import *
载入, 这是严格基于作用域的, 所以也适用于函数 - 包装:对一个已存在的对象(无论是代码块或数据类型)进行包装, 为其增加/删除/修改功能 包装包括定义一个类, 它的实例拥有标准类型的核心行为
- 授权:包装的特性之一, 用于简化处理相关命令性功能, 采用已存在的功能以达到最大限度的代码重用
- 授权的过程即更新的功能都是由新类的某部分来处理, 但已存在的功能就授权给对象的默认属性
- 关键方法:
__getattr__()
,内建getattr()
, 当属性不存在时获取默认对象属性
- 引用一个属性时的搜索顺序: 局部名称空间 -> 类名称空间 -> 对原对象进行授权请求,此时调用
__getattr__()
- 所有Python数值类型中只有复数拥有属性
- 只有已存在的属性是在此代码中授权的, 特殊行为没有在类型的方法列表中, 不能被访问, 因为它们不是属性(例如list的切片操作, 但可以通过访问实际对象来解决)
- 布尔类型操作符的巧用法:
ans and 'nice' or 'good'
(如果ans为真返回’nice’, 否则返回’good’) - 时间顺序(chronological)数据:
- 创建时间
ctime
:实例化的时间 - 修改时间
mtime
:核心数据升级的时间(通常调用新的set()
方法) - 访问时间
atime
:最后一次对象的数据值被获取或者属性被访问时的时间戳 time.ctime
:接收分钟数转换成字符串格式(从1970年1月1日00:00:00开始算)
- 创建时间
- 新式类中最主要特性:能够子类化Python数据类型, 使得原先内建的转换函数现在都是工厂函数(当调用它们时, 实际上是在实例化)
结合该特性配合
isinstance
使用更加清晰明朗, 但如果判断对象是一个给定类型的实例或其子类的实例也会返回True
, 所以若要严格匹配, 需用is
__slots__
:类变量, 由一序列型对象组成, 由所有合法标志构成的实例属性的集合来表示(可以是列表/元祖/可迭代对象/字符串)- 使用情况:当一个类的属性数量很少, 但实例很多时,可以用其替代
__dict__
- 利弊: 主要可以节约内存;但是会防止用户随意的动态增加实例属性, 带该属性的类定义不会存在
__dict__
了(除非在slots
中手动加入)
- 使用情况:当一个类的属性数量很少, 但实例很多时,可以用其替代
__getattribute__()
:与__getattr__()
不同在于当属性被访问时, 它一直都可以被调用(而不是局限于不能找到的情况)- 如果在
__getattribute__()
中再次调用了__getattribute__()
, 将会进入无穷递归 - 为了避免无穷递归, 应该调用祖先类的同名方法:
super(obj, self).__getattribute__(attr)
- 如果在
- 描述符:新式类中的关键要素之一, 为对象属性提供强大的API, 可认为是表示对象属性的一个代理
- 静态方法、类方法、属性、函数都是描述符
- “描述符协议”:至少实现三个中的一个, 取值
__get__()
, 赋值__set__()
, 删值__delete__()
- 同时覆盖
__get__()
和__set__()
的类被称作数据描述符 - 没有实现
__set__()
的类是方法描述符(非数据描述符) - 整个描述符的心脏是
__getattribute__()
, 高优先级, 用来查找类属性的同时也是一个代理, 调用它可进行属性的访问等操作
- 若要为一个属性写个代理, 必须把它作为一个类的属性, 让这个代理来为我们做所有的工作;当它处理对属性的操作时, 会有一个描述符来代理全部的函数功能
- 描述符优先级别: 类属性 > 数据描述符(有
get
和set
) > 实例属性(局部对象dict
的值) > 非数据描述符(值不存在时提供一个值) > 默认__getattr__()
- 描述符会根据函数的类型确定如何封装这个函数和函数被绑定的对象, 然后返回调用对象
函数本身就是一个描述符, 函数的
__get__()
方法用来处理调用对象并返回结果 property(fget=None, fset=None, fdel=None, doc=None)
:- 属性是一种特殊类型的描述符, 用来处理所有对实例属性的访问
- 当用点属性符号处理一个实例属性时, 实际上是对实例的
__dict__
属性进行处理 - 优势:它使用了方法, 可以写一个和属性有关的函数来处理实例属性的
get
,set
,del
操作, 使代码更明朗(实例属性一多,那些特殊方法就显得臃肿) - 一般用法:写在类定义中,以函数作为参数, 在所在的类被创建时被调用
- 在调用函数时
self
作为第一个参数被传入, 所以需要加一个伪变量把self
丢弃 - P398有名字空间与
property()
的例子
- 元类(metaclass):用于创建类, 可理解为类中类, 元类的实例是其他的类
执行类定义时, 解释器必须知道这个类正确的元类, 查找(
__metaclass__
)顺序: 类属性-父类-对象或类型中继承-全局变量-没有:定义为传统类(元类为types.ClassType
)- 元类通常传递三个参数(到构造器):类名、从基类继承数据的元祖和(类的)属性字典
- 元类的最终使用者是程序员, 可以通过定义一个元类来使程序员按某种方式实现目标类, 便于简化工作并使程序符合特定标准
- 类的相关模块:
User*
(UserList
,UserDict
,UserString
):提供给用户需要的封装类, 还可以作为基类提供子类化和定制operator
:标准操作符的函数接口types
:定义所有Python对象的类型在标准Python解释器中的名字
温故知新,高级复读机