类
类定义了一组属性,包括变量(指类变量,为所有实例共享)、方法(有三种类型,实例方法、静态方法和类方法)以及由@property
支持的特性(需要计算的属性),使用class
语句可以创建类对象(注意区分“类对象”和“类实例”),类可以充当一个命名空间,与模块很类似,类的实例是以函数形式调用类对象来创建的(类名后加小括号),然后将创建的实例传递给类的__init__()
方法,__init__()
方法的参数包括新创建的实例self
和用于初始化实例的一些其他参数(可以缺省),注意__init__()
只能返回None
,可以在其中将一些初始化参数绑定在self
对象上作为实例属性,之后可以通过属性名和.
运算符访问到实例的这些属性(任意对象都有一个__dict__
字典属性,当然也有特例,存储了所有通过obj.property=value
方式动态绑定在对象上的属性),在访问属性时,首先会检查实例,如果不知道该属性的任何信息,则会对实例的类进行搜索,如果还没有,会继续搜索其父类,直到没有更多的基类可供搜索
继承是一种创建新类的机制,目的是专门使用或修改现有类的行为,原始类称为基类或超类,新类称为派生类或子类,通过继承创建类时(可以有多个基类,即多继承,否则叫单继承),所创建的类将继承其基类定义中的属性(注意继承的是类属性而不会继承基类的任何实例的绑定属性),且派生类可以重新定义任何这些属性并添加自己的新属性。子类如果没有定义自己的__init__()
方法,实例化子类的时候将会自动调用父类的初始化函数(__new__()
也一样),反之,则不会调用,除非手动执行super().__init__()
(不建议通过父类类名调用__init__()
方法),关于多重继承时的属性查找,会按照MRO顺序(第一章节已经谈过这个问题)依序搜索基类
1 | class Dog: |
通常你可以为类实例动态绑定任意数量的任意属性,当你要限制此行为时,或者当一个类需要创建大量实例要节省内存时,可以设置__slots__
类属性声明实例可动态绑定的属性范围,一旦定义__slots__
,类实例就不再有__dict__
属性,且类中不能再定义与__slots__
列表中同名的类属性:
1 | class student: |
在实际使用中,__slots__
不是作为一种安全特性,虽然定义了__slots__
的类不再依赖__dict__
字典进行实例属性的存储和查找,从而达到隐藏实例动态绑定的属性的目的,但是并没有对属性访问本身增加任何控制,其主要目的还是对内存和性能的优化,使用__slots__
的类实例不再使用字典来存储实例绑定属性,它会使用基于数组的更加紧密的数据结构,在创建大量对象的程序中,使用__slots__
可以显著减少内存占用和执行时间(直接通过数组内存地址偏移访问__slots__
中定义的属性比通过__dict__
字典进行属性查找要快),但是也会带来一些需要注意的问题,1)__slots__
无法被继承,每个子类都要重新定义一遍,如果忘记这一点,子类的执行速度将比没有使用__slots__
时更慢,2)实例只能动态绑定那些在__slots__
列表中出现的属性,极大影响了程序的灵活性,3)实例不能被弱引用,除非将'__weakref__'
放进__slots__
关于上述第一点,子类如果没有重新定义__slots__
,动态绑定将不受限制,重新定义时如果和基类一致,可以直接赋一个空值__slots__=()
,也可以在基类的基础上进行增补,但无法删除
1 | class graduate(student): |
关于第三点,弱引用的问题,定义了__slots__
的类实例无法被弱引用,因为此时不仅是类实例的__dict__
属性被禁用,__weakref__
属性也被禁用,__weakref__
属性是干嘛的?假设对对象进行弱引用,那么创建的弱引用将存储在对象的__weakref__
属性中,因此解决办法就是将'__weakref__'
放进__slots__
列表中:
1 | class graduate(student): |
Python中无法为类声明私有类型的属性,声明私有类型的目的是阻止子类继承该属性,并且只能在类定义中访问私有属性,在类外任何地方都无法直接通过属性名访问它。一种变通是python内部会对类定义中所有以__
双下划线开头且不超过一个下划线结尾(这样命名的属性就被认为是“私有属性”)的属性进行名字转换,具体的,在原始属性名前加上“_类名
”,譬如:类Foo
的“私有属性”__spam
被替换为_Foo__spam
,使得子类无法通过原始属性名访问它,python把这种技术叫做“name mangling”(属性名轧压机制),示例:
1 | class A(object): |
输出倒数第二行为什么是A.__private()
而不是B.__private()
(上述示例既展示了python中“私有属性”的工作方式,也展示其愚蠢,为了纠正,应这样实现B
的初始化函数:self.__private();self.public()
,而不是偷懒直接调用父类的初始化函数),不急,先来看看属性名轧压机制是否真的工作了:
1 | a.__private() |
对象a
确实访问不到原始的属性方法__private
了,名字已经转变为_A__private
,实际上解释器在遇到类定义语句时,会首先修改这些“私有”属性的名字,即在内存中的类定义如下(在类定义外动态绑定的双下划线属性名不会被轧压):
1 | class A(object): |
现在可以回答上述问题了,当实例化B
类时,会调用父类A
类的初始化函数,并传入B
类实例,记作b
,于是在__init__()
中将先后执行b._A__private()
和b.public()
,真相大白
另外经常遇到以_
单下划线开头的变量名(实际上是以任何数量下划线开头的变量,仅习惯上以单下划线打头),如果位于模块级别,在from module import *
时是无法导入的(可以预防错误,因为这种方式导入的变量很容易导致“覆盖”,即“命名空间污染”),如果要设置from module import *
所允许导入的变量,可以使用__all__
指定
__new__()
是一个静态方法,通用的定义为__new__(cls,*args,**kwargs)
(第一个参数cls
表示要实例化的类,你总是需要手动传递它),其中*args
和**kwargs
接收的参数与传递给__init__()
的参数相同,注意__new__()
是唯一可以编写在创建实例之前执行的代码的地方,其必须有返回值,返回的是创建的实例对象,一般调用父类或object
类的__new__()
方法来创建,如果返回值不是当前类的实例那么__init__()
函数将不会被调用
1 | class A: |
__new__
方法可用于当你继承一些不可变的类型时(如int
、str
、tuple
),提供给你一个自定义这些类的实例化过程的途径,譬如你要继承int
类型实现一个永远为正数的整型,由于是不可变对象,显然你无法在实例创建之后(如__init__()
中)进行任何修改:
1 | class PositiveInteger(int): |
还可以用来实现“单例”,具体后面再说
所谓元类,就是创建类对象本身的类,我们在创建类实例的时候,是通过类名加括号(括号中是初始化参数)的方式,又知道“类的类”是type
类型,可想而知类对象是通过type(...)
形式创建的,现给定一个类定义,解释器究竟做了什么呢?譬如:
1 | class Foo(object): |
解释器扫描完这一段内容后,可以得到类名class_name
、继承的基类bases
以及类主体class_body
三个内容:
1 | class_name='Foo' |
然后在局部空间class_dict={}
中执行类主体:exec(class_body,globals(),class_dict)
,最后使用默认元类也就是type
类创建类对象:Foo=type(class_name,bases,class_dict)
除了使用默认的type
类创建类对象,还可以继承type
类,也就是自定义元类,通过重载__new__()
或__init__()
来进一步控制用户自定义类的定义内容(如果是修改类定义的话,应该在__new__()
中类对象创建之前修改,如果是一些检查逻辑如类属性值是否合法等,则在类对象创建后再检查也不迟,即放在__init__()
中),看个示例,已知有一个factory
类,现要求其只能有三个属性,名称name
、地址location
以及业务business
,其实就是添加一个类属性__slots__
(注意类对象创建后,再添加类属性__slots__
是没效果的,必须写在类定义中):
1 | class MyMetaCls(type): #自定义元类 |
如上,要设置元类,只需要在基类元组中提供metaclass
关键字参数,或者在类定义中指定__metaclass__
属性。尽管使用元类可以显著改变用户定义的类的行为和语义,但在使用元类时,不应使类的工作方式和其文档描述相距甚远,这会使得用户对代码感到困惑
属性访问控制
在类定义中,所有方法默认都是在实例上操作的,特点是总是存在一个名为self
(当然名称可以自定义)的位置参数,在通过实例访问这些方法属性时,譬如obj.method
(obj
是某一类实例,method
是类中定义的普通实例方法),python会利用“特性”(特殊属性)机制来进行访问控制,事实上,用户得到的不是原始函数对象method
,而是会得到所谓的“绑定方法”,绑定方法类似于偏函数(obj.method
返回的相当于是partial(method,self=obj)
),其中self
参数已经填入,保存在绑定方法的执行环境中,但是其他参数仍需在调用该(绑定)方法时提供(其实你也可以通过类调用实例方法,不过这时候你就需要手动传入实例对象了,如cls.method(obj,...)
,cls
为obj
的所属类,省略号省略了方法可能存在的其他参数),这种绑定方法是由在后台执行的特性函数静默创建的,你还可以手动指定其他两种特性函数分别用于创建静态方法和类方法,静态方法是一种普通函数,但是位于类定义的命名空间中,要定义静态方法,可使用@staticmethod
装饰器,譬如:
1 | class Foo: |
不同于实例方法,静态方法属性访问返回的就是原始的函数对象而非绑定方法,且既可以通过类也可以通过实例来调用静态方法。如果在编写类时需要采用很多不同的方式来创建新实例,而类中只能由一个__init__()
方法,可以使用静态方法,替代的创建函数通常按如下形式定义:
1 | import time |
类方法是将类本身作为对象进行操作的方法,类方法使用@classmethod
装饰器,与实例方法不同在于,类方法定义的第一个参数通常命名为cls
,且将类本身作为第一个参数进行传递,譬如:
1 | class Times: |
类似于实例方法的属性访问,类方法属性访问同样返回绑定方法,因此上述示例中TwoTimes.mul
返回的相当于partial(mul,cls=TwoTimes)
,且类方法也能通过实例调用,譬如t=TwoTimes();t.mul(4)
,其中t.mul
返回的相当于partial(mul,cls=t.__class__)
在之前的Date
类示例中我们使用静态方法实现多个创建函数,假设有一个子类EuroDate
继承它以输出欧洲日期格式,但是你会发现结果不符合预期:
1 | class EuroDate(Date): |
输出之所以还是旧格式,这是因为EuroDate.now()
返回的是一个Date
对象,应采用类方法实现创建函数来避免该错误:
1 | import time |
通常,访问实例或类属性时,会返回所存储的相关值,由@property
特性支持的属性,可以在访问时经过计算得到返回值,譬如:
1 | import math |
上述示例中,计算面积的area()
实例方法和计算周长的perimeter()
实例方法,通过装饰器@property
修饰后,支持以简单属性的形式访问(不需要加()
调用),用户很难发现正在“计算”一个属性,除非在试图重新定义该属性时生成了错误消息(AttributeError
异常),这种特性使用方式遵循所谓的统一访问原则,你不需要费力了解何时添加额外的()
所带来的不必要的混淆
事实上,该特性还可以拦截操作,以设置和删除属性,这是通过向特性附加setter
和deleter
操作来实现的,譬如:
1 | class student: |
在上述示例中,首先使用@property
装饰器将相关方法score()
转变成了只读属性,后面的@score.setter
装饰器将其他方法与score
属性上的设置操作相关联,这个“其他方法”的名称应该与原始特性的名称完全匹配
使用@property
特性后,对属性的访问将由一系列用户定义的get、set和delete函数控制,这种属性控制方式可以由“描述符”进一步推广,描述符就是一个表示属性值的对象,凡是实现__get__()
、__set__()
和__delete__()
方法的就是描述符,注意描述符只能在类级别上进行实例化,当你通过实例访问一个描述符对象时,会自动调用描述符的__get__()
、__set__()
或__delete__()
,并总是传入实例作为参数之一
1 | class movie(object): |
上述示例中定义了一个通过@property
进行属性访问控制的电影类,可以看到score
和ratings
两个属性拥有相同的访问控制,从而导致上述代码有较高的重复率,而描述符的访问控制可以被多个属性重复利用,需要注意的是你总是应该通过实例而非类名访问描述符,示例如下:
1 | from weakref import WeakKeyDictionary |
当访问实例属性的时候,obj.x
总是调用__getattribute__()
方法(因此你也可以通过重载__getattributr__()
进行最简单的属性访问控制,在这种情况下,不要忘记调用父类的__getattributr__()
方法以防属性查找失败,所以不太建议这么做),其行为是,1)检查类中是否有同名的数据描述符对象,如果有,则自动变形为obj.__class__.__dict__['x'].__get__(obj,obj.__class__)
,否则,2)检查实例字典,即在实例的__dict__
中查找,找到则返回,否则,3)检查类字典,即在obj.__class__.__dict__
中查找,找到则返回,如果是同名的非数据描述符,则还会自动变形,否则,4)按照MRO顺序依次检查基类字典,如果还找不到,则检查obj
所在类是否定义了__getattr__()
方法,有则返回__getattr__()
的调用结果,否则抛出AttributeError
当访问实例属性并进行赋值的时候,obj.x=value
总是调用__setattr__()
方法,其行为是,1)如果类中有同名的数据描述符对象,则自动变形为obj.__class__.__dict__['x'].__set__(obj,value)
,否则,2)在实例的字典__dict__
中查找,有则修改相应的键值,没有则将属性x
直接绑定在实例上,即__dict__.update({'x':value})
(del obj.x
语句将触发调用__delattr__()
方法,和obj.x=value
触发调用__setattr__()
类似,只不过其默认行为是删除obj
的__dict__
中存储的键值,除非请求的属性正好是一个描述符对象,此时将会执行obj.__class__.__dict__['x'].__delete__(obj)
,如果实例字典中没有该属性,则抛出AttributeError
)
注:实现了__set__()
和__get__()
方法的描述符类被称为数据描述符,如果只实现了__get__()
方法,则称为非数据描述符
上面的描述符示例中采用字典存储所有的实例和属性值,但如果实例是不可哈希对象呢,为此我们还可以直接将属性值绑定到实例(的__dict__
)上面:
1 | class descriptor: |
上述在类层级定义描述符的时候需要手动传入属性名称字符串,score=descriptor('score',0)
,这不太方便,为了隐藏该细节,可以借助元类,使得定义变回score=descriptor(0)
这种简洁形式:
1 | class descriptor: |
未完待续…