📚 文档目录
🎃 类型和对象 - 🎈 程序结构与函数编程 - 🎏 面向对象编程

📚 文档目录
🎃 类型和对象 - 🎈 程序结构与函数编程 - 🎏 面向对象编程

类定义了一组属性,包括变量(指类变量,为所有实例共享)、方法(有三种类型,实例方法、静态方法和类方法)以及由@property支持的特性(需要计算的属性),使用class语句可以创建类对象(注意区分“类对象”和“类实例”),类可以充当一个命名空间,与模块很类似,类的实例是以函数形式调用类对象来创建的(类名后加小括号),然后将创建的实例传递给类的__init__()方法,__init__()方法的参数包括新创建的实例self和用于初始化实例的一些其他参数(可以缺省),注意__init__()只能返回None,可以在其中将一些初始化参数绑定在self对象上作为实例属性,之后可以通过属性名和.运算符访问到实例的这些属性(任意对象都有一个__dict__字典属性,当然也有特例,存储了所有通过obj.property=value方式动态绑定在对象上的属性),在访问属性时,首先会检查实例,如果不知道该属性的任何信息,则会对实例的类进行搜索,如果还没有,会继续搜索其父类,直到没有更多的基类可供搜索

继承是一种创建新类的机制,目的是专门使用或修改现有类的行为,原始类称为基类或超类,新类称为派生类或子类,通过继承创建类时(可以有多个基类,即多继承,否则叫单继承),所创建的类将继承其基类定义中的属性(注意继承的是类属性而不会继承基类的任何实例的绑定属性),且派生类可以重新定义任何这些属性并添加自己的新属性。子类如果没有定义自己的__init__()方法,实例化子类的时候将会自动调用父类的初始化函数(__new__()也一样),反之,则不会调用,除非手动执行super().__init__()(不建议通过父类类名调用__init__()方法),关于多重继承时的属性查找,会按照MRO顺序(第一章节已经谈过这个问题)依序搜索基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Dog:
def __init__(self,name):
self.name=name

class WorkDog(Dog): #工作狗
allnum=0

def __init__(self,name,identity): #工作狗除了姓名,还具有编制identity,当前全部注册的工作狗数量由allnum类属性记录
self.__class__.allnum+=1 #千万不能写成self.allnum+=1或者WorkDog.allnum+=1,如果没有派生类的话,后者才正确,前者将会创建一个与类属性allnum同名的实例绑定属性(但若类定义allnum是一个可变对象的话,则又不会新创建实例绑定属性,除非写成self.allnum=self.allnum+1的形式,因为+=对可变对象来说是一个原址操作,示例参见https://github.com/leisurelicht/wtfpython-cn#-class-attributes-and-instance-attributes%E7%B1%BB%E5%B1%9E%E6%80%A7%E5%92%8C%E5%AE%9E%E4%BE%8B%E5%B1%9E%E6%80%A7),数值将总是为1,但是在访问该类属性的时候,总是可以写成self.allnum,因为实例上找不到,会自动到类中寻找
self.identity=identity
super().__init__(name) #自定义__init__()后,除非手动调用父类初始化方法,否则不会自动调用,在单继承情况下写成Dog.__init__(self,name)也没问题,但是多继承就不一定了,因此总是建议采用super方式,而且后者形式更加简约,super()(python3中等价于super(WorkDog,self))返回super对象(相当于父类但不是原始父类,是绑定了实例self的bound super object,且多继承的时候返回的还可能是兄弟类,具体参见第一章节所提到的钻石继承),不同于其他通过类访问实例方法(返回非绑定方法,只有通过实例访问实例方法才返回绑定方法)的情况,此处返回的也是“绑定方法”,即绑定了self参数的父类__init__()方法,因此调用时不需要再传入self参数

def work(self): #一个公开的未实现的接口,子类应重载之
pass

def __del__(self):
self.__class__.allnum-=1

class ArmyDog(WorkDog):
def work(self):
print(f'{self.name}({self.identity})在追击敌人')

class DrugDog(WorkDog):
def work(self):
print(f'{self.name}({self.identity})在追查毒品')

class SuperDog(DrugDog,ArmyDog):
def __init__(self,name,identity):
self.__class__.allnum=0 #访问SuperDog.allnum时会在基类(ArmyDog或DrugDog)中查找,可能已经非零了
super().__init__(name,identity)

pass

army_dog_1=ArmyDog('Cherry',id('Cherry')) #ArmyDog直接继承了父类的__init__()方法,初始化ArmyDog类实例的时候会自动调用之
army_dog_2=ArmyDog('Black',id('Black'))

print(army_dog_2.__dict__) #{'identity': 1761656222920, 'name': 'Black'} #通过__dict__实例属性字典查找用户绑定在实例上的所有属性
army_dog_2_ref=army_dog_2 #对army_dog_2所指向的对象引用计数+1,于是当前引用数总计为2

drug_dog_1=DrugDog('HelloKit',id('HelloKit'))
drug_dog_2=DrugDog('Jack',id('Jack'))
drug_dog_3=DrugDog('Pig',id('Pig'))

del army_dog_2 #del语句看似是“删除对象”,准确说其实是对象引用计数-1,且del语句并不会立即执行__del__()方法,只有在对象的引用计数降为0的时候才会执行,这时候对象才会真的从内存中删除
print(ArmyDog.allnum) #2 #写成army_dog_1.allnum也正确,但是语义不明,因为allnum明明是一个类属性,通过实例访问会让人误以为是实例属性
print(DrugDog.allnum) #3
del army_dog_2_ref
print(ArmyDog.allnum) #1

drug_dog_2.work() #Jack(2607617062368)在追查毒品

super_dog_1=SuperDog('Hero',id('Hero'))
print(super_dog_1.__class__.mro()) #[<class '__main__.SuperDog'>, <class '__main__.DrugDog'>, <class '__main__.ArmyDog'>, <class '__main__.WorkDog'>, <class '__main__.Dog'>, <class 'object'>]
super_dog_1.work() #Hero(2359695357632)在追查毒品 #SuperDog并未实现自己的work()方法,因此在属性查找时,会顺着MRO列表依次在“基类”中寻找,MRO列表中位于SuperDog之后的正是DrugDog,而DrugDog类确实定义了自己的work()方法,所以属性查找到此结束,就是它

def work(self):
print(f'{self.name}({self.identity})在追击敌人,同时在追查毒品')

SuperDog.work=work #给类动态绑定属性方法,且默认是实例方法,相当于你一开始在定义SuperDog的地方写了work()方法
super_dog_1.work() #Hero(1559184520896)在追击敌人,同时在追查毒品

@classmethod
def print_allnum(cls):
print(f'{cls}类总数量为{cls.allnum}')

SuperDog.print_allnum=print_allnum #给类对象动态绑定类方法,同理静态方法也可以
SuperDog.print_allnum() #<class '__main__.SuperDog'>类总数量为1 #也可以通过实例调用类方法,super_dog_1.print_allnum()

通常你可以为类实例动态绑定任意数量的任意属性,当你要限制此行为时,或者当一个类需要创建大量实例要节省内存时,可以设置__slots__类属性声明实例可动态绑定的属性范围,一旦定义__slots__,类实例就不再有__dict__属性,且类中不能再定义与__slots__列表中同名的类属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class student:
__slots__=('name','score')
#name='xxx' #ValueError: 'name' in __slots__ conflicts with class variable #类中不能再定义出现在__slots__列表中的属性
pass

s=student()
s.name='dy'
s.score=8.0
#s.sex='female' #AttributeError: 'student' object has no attribute 'sex' #无法为实例动态绑定非__slots__列表属性
#print(s.__dict__) #AttributeError: 'student' object has no attribute '__dict__' #定义__slots__类属性的类实例不再有__dict__属性字典

student.school='nuist' #__slots__不会限制类本身可动态绑定属性的范围
print(student.__dict__) #{'__module__': '__main__', '__slots__': ('name', 'score'), 'name': <member 'name' of 'student' objects>, 'score': <member 'score' of 'student' objects>, '__doc__': None, 'school': 'nuist'} #注意看类的__dict__中有了name和score两个属性,这也是为什么上面在类中定义同名属性name时会发生冲突

在实际使用中,__slots__不是作为一种安全特性,虽然定义了__slots__的类不再依赖__dict__字典进行实例属性的存储和查找,从而达到隐藏实例动态绑定的属性的目的,但是并没有对属性访问本身增加任何控制,其主要目的还是对内存和性能的优化,使用__slots__的类实例不再使用字典来存储实例绑定属性,它会使用基于数组的更加紧密的数据结构,在创建大量对象的程序中,使用__slots__可以显著减少内存占用和执行时间(直接通过数组内存地址偏移访问__slots__中定义的属性比通过__dict__字典进行属性查找要快),但是也会带来一些需要注意的问题,1)__slots__无法被继承,每个子类都要重新定义一遍,如果忘记这一点,子类的执行速度将比没有使用__slots__时更慢,2)实例只能动态绑定那些在__slots__列表中出现的属性,极大影响了程序的灵活性,3)实例不能被弱引用,除非将'__weakref__'放进__slots__

关于上述第一点,子类如果没有重新定义__slots__,动态绑定将不受限制,重新定义时如果和基类一致,可以直接赋一个空值__slots__=(),也可以在基类的基础上进行增补,但无法删除

1
2
3
4
5
6
7
8
class graduate(student):
__slots__=('sex') #一个笔误,之前以为__slots__必须是一个列表,此处等同于:__slots__='sex',当前子类graduate的__slots__在基类基础上扩展,属性范围限制在['name','score','sex']

g=graduate()
g.name='dy'
g.score=25
g.sex='female'
print(g.sex)

关于第三点,弱引用的问题,定义了__slots__的类实例无法被弱引用,因为此时不仅是类实例的__dict__属性被禁用,__weakref__属性也被禁用,__weakref__属性是干嘛的?假设对对象进行弱引用,那么创建的弱引用将存储在对象的__weakref__属性中,因此解决办法就是将'__weakref__'放进__slots__列表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class graduate(student):
__slots__=('sex','__weakref__')

import weakref

g=graduate()
print(g.__weakref__) #None #此时还没有弱引用
ref=weakref.ref(g) #如果没有将'__weakref__'放到__slots__中,则报错:TypeError: cannot create weak reference to 'graduate' object
print(g.__weakref__) #<weakref at 0x000002595C39C4F8; to 'graduate' at 0x000002595C60CDC8>

g.name='muggle'
g.score=25
g.sex='female'

print(f'{ref().name},{ref().sex},{ref().score}') #muggle,female,25 #调用弱引用将获取被弱引用的原始对象
弱引用

weakref模块允许用户创建对对象的弱引用(weak reference),相比于普通的引用(为了和“弱引用”这一名词对应,在官方文档中明确有strong reference“强引用”一说)来说,只要存在任何一个对对象的强引用(即引用计数大于1,注意弱引用不计在内,要删除某个强引用,使用del语句,引用计数减一),这个对象就不会被垃圾收集器销毁,而当引用计数为0时(没有强引用),无论还有多少弱引用,那么它将会被垃圾收集器收回。在对象未被销毁之前,可以通过弱引用访问到对象,否则只能得到None

weakref运行在“观察者模式”,当我们创建一个对对象的弱引用,意味着对该对象进行观察,当对象被回收,观察者将立即得到一个反馈并执行回调函数,假设存在的话。另外并非所有对象都可以创建弱引用,可以创建弱引用的对象包括类实例、函数对象、实例方法、集合、冰冻集合、文件对象、生成器、类对象、sockets、数组、队列(deque)、正则表达式对象(regular expression pattern)以及code对象,而列表和字典是不能直接支持弱引用的:

1
2
3
4
5
6
>>> import weakref
>>> a=[1,2,3]
>>> wr=weakref.ref(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create weak reference to 'list' object

通过子类化列表或字典可以增加这样的支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import weakref
class new_list(list):
pass

def callback(ref):
if ref is wr:
print('列表对象[1,2,3]被回收')

a=new_list([1,2,3])
wr=weakref.ref(a,callback)
del a

'''OUTPUT
列表对象[1,2,3]被回收
'''

但是像元组和整型,即使子类化也不能支持弱引用。在上述示例中,通过weakref.ref()创建了一个弱引用对象,可以像调用函数那样调用它以获取被引用的对象(因为弱引用类实现了__call__()方法),称之为referent,如果referent不再存活,调用将返回None,因此如果你要测试一个referent是否存活,可以使用逻辑表达式ref() is not None(其中ref是一个弱引用对象)

简单介绍weakref模块的类或方法:

  • class weakref.ref(obj[, callback])(创建对对象的弱引用)
    参数obj是被弱引用的原始对象(被称为referent),callback是回调函数,当obj引用计数减为0被当作垃圾回收清理的时候,会自动调用该函数,且传入该弱引用作为唯一参数,可以创建多个弱引用,当referent被回收的时候,会根据弱引用创建的时间由近及远地逐个执行回调函数,假设定义了的话

    假设referent是可哈希的,弱引用对象也是可哈希的,即使referent被销毁,弱引用对象仍能维持其哈希值,但是如果你在referent被销毁之后才第一次计算弱引用对象的哈希值,将导致TypeError

    弱引用对象支持等值测试(==),但是不支持排序,不同弱引用之间的等值关系等同于各自对应的referent之间的等值关系(举个例子,假设有两个不同对象满足a==b,且wra是对a的弱引用,wrb是对b的弱引用,那么立即推,wra==wrb是成立的,若其中一方的referent被销毁,则不再相等),特别的,对同一对象所创建的不同弱引用总是等值的(创建弱引用时所传入的回调函数参数不影响等值测试的结果,但会影响is判断的结果),示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import weakref
    class A:
    def __init__(self,name):
    self.name=name
    def __hash__(self):
    return hash(self.name)
    def __eq__(self,another):
    if self.name==another.name:
    return True
    return False

    a=A('msy')
    b=A('msy')
    wra=weakref.ref(a)
    wrb=weakref.ref(b)
    print(a==b,wra==wrb)

    print(hash(wra))
    del b
    #print(hash(wrb)) #TypeError: weak object has gone away
    print(wra==wrb) #False

    def callback(ref):
    pass
    wra2=weakref.ref(a,callable)
    print(wra==wra2,wra is wra2) #True False
    wra3=weakref.ref(a)
    print(wra==wra2,wra is wra3) #True True
  • weakref.proxy(obj[, callback])(创建对对象的代理)
    代理对象是不可哈希的(无论referent可不可哈希),这阻止了将其用于字典的键,和weakref.ref()一样,支持回调,代理和弱引用的显著的区别是,1)要从弱引用获取referent,你需要使用()调用这个弱引用对象,但是代理不需要,使用代理对象就如同使用对象本身一样,2)当referent销毁后,弱引用调用会返回None,而访问代理则导致ReferenceError

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import weakref

    a={1,2,3}
    wr=weakref.ref(a)

    def callback(ref):
    print('删除对象')

    proxy=weakref.proxy(a,callback)

    print(wr()) #{1, 2, 3}
    print(repr(proxy))
    print(proxy) #{1, 2, 3}

    proxy.add(4)
    print(proxy) #{1, 2, 3, 4}

    del a #对象a的引用计数降为0后,自动调用代理的回调函数,输出:删除对象

    print(wr()) #None
    print(proxy) #ReferenceError: weakly-referenced object no longer exists
  • weakref.getweakrefcount(obj)(返回对象obj的全部弱引用以及代理数量)

  • weakref.getweakrefs(obj)(返回对象obj的全部弱引用和代理对象,以列表返回)

  • class weakref.WeakMethod(method)(创建对绑定方法的弱引用)
    假设你有一个类实例af是实例方法,要对a.f创建弱引用,使用WeakMethod(a.f)即可,而标准弱引用对象构造办法(ref(a.f)()将返回None)无法做到这一点,因为a.f返回的是一个全新的“绑定了实例的方法”,其引用计数为0,如果一定要使用标准弱引用对象构造办法,只要将a.f赋给一个变量即可(使引用计数非0),因为WeakMethodref的子类,因此要获取被弱引用的原始绑定方法对象,也是通过()进行调用来获取:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import weakref

    class A:
    def f(self):
    print('实例方法')

    @classmethod
    def cf(cls):
    print('类方法')

    @staticmethod
    def sf():
    print('静态方法')

    a=A()
    m1=weakref.WeakMethod(a.f) #替换成WeakMethod(a.cf)也可以,但是替换成WeakMethod(a.sf)不行,因为a.sf返回的是非绑定方法
    m1()()

    print(weakref.ref(a.f)()) #None
    t=a.f #替换成a.sf或a.cf都可以
    m2=weakref.ref(t)
    m2()()
  • class weakref.finalize(obj, func, *args, **kwargs)
    创建一个可通过()调用的finalizer对象(finalize类的__call__()方法体即是执行func(*args,**kwargs)),当referent对象obj的引用计数降为0时将自动触发调用,另外在程序结束时也总会调用这些finalizer对象(调用顺序和这些对象的创建顺序相反,对比ref(obj,callback),程序结束时则不会调用callback(),除非在程序中手动执行del语句降低引用计数为0),除非其atexit属性被设置为False,finalizer一旦被调用一次将“死亡”,若调用一个死亡了的finalizer,将返回None(另请注意funcargskwargs不能直接或间接对obj存在任何引用,否则会导致obj不能被回收,特别的,func不能是obj的实例绑定方法):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import weakref

    class A:
    pass

    def callback():
    print('回收')
    return 888

    a=A()
    t=weakref.finalize(a,callback)
    print(t())
    print(t()) #1、如果替换这两行为del a,输出结果一致,2、如果注释掉这两行,将先后输出"OVER"和"回收"
    print('OVER')

    '''OUTPUT
    回收
    888
    None
    OVER
    '''

    finalizer对象的属性、方法:

    • __call__()
      当finalizer对象存活时调用__call__()将返回func(*args,**kwargs)的结果,并在此之后宣判其死亡,当finalizer对象死亡时调用__call__()将返回None
    • detach()
      当finalizer对象存活时调用detach()将返回(obj, func, args, kwargs)元组,并在此之后宣判其死亡,当finalizer对象死亡时调用detach()将返回None
    • peek()
      detach(),但是调用后不会导致finalizer对象死亡
    • alive
      判断finalizer对象是否存活
    • atexit
      一个可写的布尔属性,默认值为True,且只有atexit属性为True的finalizer对象才会被触发调用
  • weakref.ReferenceType(返回弱引用对象的类型<class 'weakref'>

  • weakref.ProxyType(返回不可调用对象的弱代理的类型<class 'weakproxy'>

  • weakref.CallableProxyType(返回可调用对象的弱代理的类型<class 'weakcallableproxy'>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import weakref

    s=set() #不可调用对象

    t1=weakref.proxy(s)
    print(type(t1))
    print(isinstance(t1,weakref.ProxyType))

    def f(): #可调用对象
    pass

    t2=weakref.proxy(f)
    print(type(t2))
    print(isinstance(t2,weakref.CallableProxyType))

    '''OUTPUT
    <class 'weakproxy'>
    True
    <class 'weakcallableproxy'>
    True
    '''
  • weakref.ProxyTypes(返回全部代理类型,即(<class 'weakproxy'>, <class 'weakcallableproxy'>)

弱引用的主要用途是实现持有大量“大”对象(指占用内存大)的高速缓存或映射,以往由于对象作为高速缓存或映射的条目被引用,导致用户使用完后无法使用del将这些对象的引用计数降为0(只要高速缓存或映射还存在),从而浪费大量空间,为此weakref模块提供了WeakKeyDictionaryWeakValueDictionary用来解决该问题

  • class weakref.WeakKeyDictionary([dict])
    该字典中的键是弱引用的,当外部不存在对某键的强引用时,会自动删除字典中此键对应的条目,另外WeakKeyDictionary对象拥有一个额外的方法keyrefs(),其返回全部弱引用的键的列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import weakref
    class A:
    pass

    d=weakref.WeakKeyDictionary()
    d[A()]=1
    print(list(d.keys())) #[] #弱字典中空无一物,原因在于"d[A()]=1"中的键"A()"的引用计数为0,它随即被删除

    a=A()
    d[a]=1 #键"a"存在外部引用:a=A(),其引用计数为1
    print(list(d.keys())) #[<__main__.A object at 0x000001FA6AB16EB8>]
    print(d[a]) #1
    del a #del使"a"的引用计数减1,变为0
    print(list(d.keys())) #[]
    #print(d[a]) #NameError: name 'a' is not defined
  • class weakref.WeakValueDictionary([dict])
    该字典中键的值是弱引用的,当外部不存在对某键值的强引用时,会自动删除此字典中该键值对应的条目,另外WeakValueDictionary对象拥有一个额外的方法valuerefs(),其返回全部弱引用的值的列表

除了“弱字典”,模块还提供了“弱集合”class weakref.WeakSet([elements]),当弱集合中的某个元素的引用计数降低为0时,将自动从弱集合中删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import weakref
class A:
pass
s=weakref.WeakSet()
a=A()
b=A()
s.add(a)
s.add(b)
print(list(s))
del a
print(list(s))

'''OUTPUT
[<__main__.A object at 0x0000019127A1D588>, <__main__.A object at 0x0000019127A1D160>]
[<__main__.A object at 0x0000019127A1D588>]
'''

Python中无法为类声明私有类型的属性,声明私有类型的目的是阻止子类继承该属性,并且只能在类定义中访问私有属性,在类外任何地方都无法直接通过属性名访问它。一种变通是python内部会对类定义中所有以__双下划线开头且不超过一个下划线结尾(这样命名的属性就被认为是“私有属性”)的属性进行名字转换,具体的,在原始属性名前加上“_类名”,譬如:类Foo的“私有属性”__spam被替换为_Foo__spam,使得子类无法通过原始属性名访问它,python把这种技术叫做“name mangling”(属性名轧压机制),示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class A(object):
def __init__(self):
self.__private()
self.public()
def __private(self):
print('A.__private()')
def public(self):
print('A.public()')

class B(A):
# def __init__(self): #此处写不写都一样,因为没有实现__init__方法时,本就会自动调用父类的__init__方法
# super(B,self).__init__() #或者A.__init__(self)
def __private(self):
print('B.__private()')
def public(self):
print('B.public()')

a=A()
b=B()

'''OUTPUT
A.__private()
A.public()
A.__private()
B.public()
'''

输出倒数第二行为什么是A.__private()而不是B.__private()(上述示例既展示了python中“私有属性”的工作方式,也展示其愚蠢,为了纠正,应这样实现B的初始化函数:self.__private();self.public(),而不是偷懒直接调用父类的初始化函数),不急,先来看看属性名轧压机制是否真的工作了:

1
2
3
4
5
6
7
8
9
>>> a.__private()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-2-cc12b64d1d3b> in <module>()
----> 1 a.__private()

AttributeError: 'A' object has no attribute '__private'
>>> a._A__private()
A.__private()

对象a确实访问不到原始的属性方法__private了,名字已经转变为_A__private,实际上解释器在遇到类定义语句时,会首先修改这些“私有”属性的名字,即在内存中的类定义如下(在类定义外动态绑定的双下划线属性名不会被轧压):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A(object):
def __init__(self):
self._A__private() #修改1
self.public()
def _A__private(self): #修改2
print('A.__private()')
def public(self):
print('A.public()')

class B(A):
def _B__private(self): #修改3
print('B.__private()')
def public(self):
print('B.public()')

现在可以回答上述问题了,当实例化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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class A:
def __new__(cls,*args,**kwargs): #__new__和__init__方法的参数除第一个参数(cls或self)外必须完全一致
ret=super(A,cls).__new__(cls)
print(f'Return of __new__(): {ret}')
return ret

def __init__(self,*args,**kwargs):
print(f'Init A {self} with {args} and {kwargs}')
pass

class B:
def __new__(cls):
return A.__new__(A) #事实上__new__方法可以生产非当前类的其他任意类的实例对象,这表示__new__方法拥有决定实例对象所属类型的绝对权力,但此时__init__方法则不再被自动调用

def __init__(self):
print('Init B')

a=A('Hello,','muggledy')
b=B()
print('-'*10)
b.__init__()

'''OUTPUT
Return of __new__(): <__main__.A object at 0x0000013B40CFB4A8>
Init A <__main__.A object at 0x0000013B40CFB4A8> with ('Hello,', 'muggledy') and {}
Return of __new__(): <__main__.A object at 0x0000013B40CFB518>
----------
Init A <__main__.A object at 0x0000013B40CFB518> with () and {}
'''

__new__方法可用于当你继承一些不可变的类型时(如intstrtuple),提供给你一个自定义这些类的实例化过程的途径,譬如你要继承int类型实现一个永远为正数的整型,由于是不可变对象,显然你无法在实例创建之后(如__init__()中)进行任何修改:

1
2
3
4
5
6
class PositiveInteger(int):
def __new__(cls,value):
return super(PositiveInteger,cls).__new__(cls,abs(value))

i=PositiveInteger(-3)
print(i) #3

还可以用来实现“单例”,具体后面再说

所谓元类,就是创建类对象本身的类,我们在创建类实例的时候,是通过类名加括号(括号中是初始化参数)的方式,又知道“类的类”是type类型,可想而知类对象是通过type(...)形式创建的,现给定一个类定义,解释器究竟做了什么呢?譬如:

1
2
3
4
5
class Foo(object):
...
def func(self):
pass
...

解释器扫描完这一段内容后,可以得到类名class_name、继承的基类bases以及类主体class_body三个内容:

1
2
3
4
5
6
7
8
9
class_name='Foo'
bases=(object,)
class_body= \
'''\
...
def func(self):
pass
...
'''

然后在局部空间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
2
3
4
5
6
7
8
9
10
11
class MyMetaCls(type): #自定义元类
def __new__(cls,class_name,bases,class_dict):
class_dict['__slots__']=['name','location','business']
return type.__new__(cls,class_name,bases,class_dict)

class factory(metaclass=MyMetaCls):
def __init__(self,name,location,business):
self.name,self.location,self.business=name,location,business

f=factory(name='中石化',location='西北',business='中国能源供给')
f.xxx='xxx' #AttributeError: 'factory' object has no attribute 'xxx'

如上,要设置元类,只需要在基类元组中提供metaclass关键字参数,或者在类定义中指定__metaclass__属性。尽管使用元类可以显著改变用户定义的类的行为和语义,但在使用元类时,不应使类的工作方式和其文档描述相距甚远,这会使得用户对代码感到困惑

属性访问控制

在类定义中,所有方法默认都是在实例上操作的,特点是总是存在一个名为self(当然名称可以自定义)的位置参数,在通过实例访问这些方法属性时,譬如obj.methodobj是某一类实例,method是类中定义的普通实例方法),python会利用“特性”(特殊属性)机制来进行访问控制,事实上,用户得到的不是原始函数对象method,而是会得到所谓的“绑定方法”,绑定方法类似于偏函数(obj.method返回的相当于是partial(method,self=obj)),其中self参数已经填入,保存在绑定方法的执行环境中,但是其他参数仍需在调用该(绑定)方法时提供(其实你也可以通过类调用实例方法,不过这时候你就需要手动传入实例对象了,如cls.method(obj,...)clsobj的所属类,省略号省略了方法可能存在的其他参数),这种绑定方法是由在后台执行的特性函数静默创建的,你还可以手动指定其他两种特性函数分别用于创建静态方法和类方法,静态方法是一种普通函数,但是位于类定义的命名空间中,要定义静态方法,可使用@staticmethod装饰器,譬如:

1
2
3
4
class Foo:
@staticmethod
def add(x,y):
return x+y

不同于实例方法,静态方法属性访问返回的就是原始的函数对象而非绑定方法,且既可以通过类也可以通过实例来调用静态方法。如果在编写类时需要采用很多不同的方式来创建新实例,而类中只能由一个__init__()方法,可以使用静态方法,替代的创建函数通常按如下形式定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
class Date:
def __init__(self,year,month,day):
self.year,self.month,self.day=year,month,day

@staticmethod
def now():
t=time.localtime()
return Date(t.tm_year,t.tm_mon,t.tm_mday)

@staticmethod
def tomorrow():
t=time.localtime(time.time()+86400)
return Date(t.tm_year,t.tm_mon,t.tm_mday)

def __str__(self):
return f'{self.year}-{self.month}-{self.day}'

print(Date(1967,4,9)) #1967-4-9
print(Date.now()) #2021-5-18
print(Date.tomorrow()) #2021-5-19

类方法是将类本身作为对象进行操作的方法,类方法使用@classmethod装饰器,与实例方法不同在于,类方法定义的第一个参数通常命名为cls,且将类本身作为第一个参数进行传递,譬如:

1
2
3
4
5
6
7
8
9
10
class Times:
factor=1
@classmethod
def mul(cls,x):
return cls.factor*x

class TwoTimes(Times):
factor=2

print(TwoTimes.mul(4)) #8

类似于实例方法的属性访问,类方法属性访问同样返回绑定方法,因此上述示例中TwoTimes.mul返回的相当于partial(mul,cls=TwoTimes),且类方法也能通过实例调用,譬如t=TwoTimes();t.mul(4),其中t.mul返回的相当于partial(mul,cls=t.__class__)

在之前的Date类示例中我们使用静态方法实现多个创建函数,假设有一个子类EuroDate继承它以输出欧洲日期格式,但是你会发现结果不符合预期:

1
2
3
4
5
class EuroDate(Date):
def __str__(self):
return f'{self.day:02d}/{self.month:02d}/{self.year}'

print(EuroDate.now()) #2021-5-18

输出之所以还是旧格式,这是因为EuroDate.now()返回的是一个Date对象,应采用类方法实现创建函数来避免该错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
class Date:
def __init__(self,year,month,day):
self.year,self.month,self.day=year,month,day

@classmethod
def now(cls):
t=time.localtime()
return cls(t.tm_year,t.tm_mon,t.tm_mday)

@classmethod
def tomorrow(cls):
t=time.localtime(time.time()+86400)
return cls(t.tm_year,t.tm_mon,t.tm_mday)

def __str__(self):
return f'{self.year}-{self.month}-{self.day}'

class EuroDate(Date):
def __str__(self):
return f'{self.day:02d}/{self.month:02d}/{self.year}'

print(EuroDate.now()) #18/05/2021

通常,访问实例或类属性时,会返回所存储的相关值,由@property特性支持的属性,可以在访问时经过计算得到返回值,譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import math

class Circle:
def __init__(self,radius):
self.radius=radius

@property
def area(self):
return math.pi*self.radius**2

@property
def perimeter(self):
return 2*math.pi*self.radius

c=Circle(4)
print(f'半径:{c.radius},周长:{c.perimeter},面积:{c.area}') #半径:4,周长:25.132741228718345,面积:50.26548245743669
#c.area=8 #AttributeError: can't set attribute #当前area“属性”仅仅是只读的,除非设置setter方法,具体见下

上述示例中,计算面积的area()实例方法和计算周长的perimeter()实例方法,通过装饰器@property修饰后,支持以简单属性的形式访问(不需要加()调用),用户很难发现正在“计算”一个属性,除非在试图重新定义该属性时生成了错误消息(AttributeError异常),这种特性使用方式遵循所谓的统一访问原则,你不需要费力了解何时添加额外的()所带来的不必要的混淆

事实上,该特性还可以拦截操作,以设置和删除属性,这是通过向特性附加setterdeleter操作来实现的,譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class student:
def __init__(self,name):
self.name=name

@property
def score(self):
return self.__score #将值实际存储在其他名称中,通常是在加两个下划线前缀,即“私有属性”中,但是注意“私有属性”其实也直接暴露在外,而且易于辨识,你可以通过访问实例的__dict__看到它,可以定义__slots__来禁用__dict__,但是你依旧无法阻止通过setattr(...)的方式进行非法赋值

@score.setter
def score(self,score):
if score>=0 and score<=100:
self.__score=score
else:
raise ValueError('score must between 0 ~ 100!')

s=student('zj')
s.score=99 #如果设置的数值大于100,将导致错误,因为我们利用特性进行了相关的访问控制
print(s.name,s.score) #zj 99

print(s.__dict__) #{'name': 'zj', '_student__score': 99}

s.__sex='female' #只有在类定义中绑定的以双下划线打头的实例属性被认为是“私有属性”并被倾轧,在类定义外动态绑定的则不会被认为是私有属性,也不会被倾轧
print(s.__dict__) #{'name': 'zj', '_student__score': 99, '__sex': 'female'}

在上述示例中,首先使用@property装饰器将相关方法score()转变成了只读属性,后面的@score.setter装饰器将其他方法与score属性上的设置操作相关联,这个“其他方法”的名称应该与原始特性的名称完全匹配

使用@property特性后,对属性的访问将由一系列用户定义的get、set和delete函数控制,这种属性控制方式可以由“描述符”进一步推广,描述符就是一个表示属性值的对象,凡是实现__get__()__set__()__delete__()方法的就是描述符,注意描述符只能在类级别上进行实例化,当你通过实例访问一个描述符对象时,会自动调用描述符的__get__()__set__()__delete__(),并总是传入实例作为参数之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class movie(object):
def __init__(self,title,score,ratings):
self.title=title
self.score=score
self.ratings=ratings

@property
def score(self):
return self._score

@score.setter
def score(self,newValue):
if newValue<0 or newValue>10:
raise ValueError("movie's score can't be negative or greater than 10")
else:
self._score=newValue

@property
def ratings(self):
return self._ratings

@ratings.setter
def ratings(self,newValue):
if newValue<0 or newValue>10:
raise ValueError("movie's ratings can't be negative or greater than 10")
else:
self._ratings=newValue

m=movie('后天',7.5,3)

上述示例中定义了一个通过@property进行属性访问控制的电影类,可以看到scoreratings两个属性拥有相同的访问控制,从而导致上述代码有较高的重复率,而描述符的访问控制可以被多个属性重复利用,需要注意的是你总是应该通过实例而非类名访问描述符,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from weakref import WeakKeyDictionary

class descriptor:
def __init__(self,default):
self.default=default
self.data=WeakKeyDictionary() #由于描述符是定义在类层级的,因此对于类的所有实例都是可访问的,所以我们定义一个字典,存储类的所有实例及其属性值

def __get__(self,instance,owner): #在访问movie实例m的score属性时,会被解释器解释为m.__class__.score.__get__(m,m.__class__)
#print(owner) #<class '__main__.movie'>
if instance==None: #如果通过类访问描述符,传入的实例将为None,此时返回描述符对象本身,该操作是通用操作,该设置允许我们能够获取到在类层级定义的描述符对象本身
return self
return self.data.get(instance,self.default)

def __set__(self,instance,value):
if value<0 or value>10:
raise ValueError("movie's score or ratings can't be negative or greater than 10")
self.data[instance]=value

class movie:
score=descriptor(0)
ratings=descriptor(0)

def __init__(self,title,score,ratings):
self.title=title
self.score=score #注意千万不能通过类名访问描述符对象,譬如movie.score=score,这将导致类的score属性从一个描述符对象转变为一个数值对象,因为这不会触发调用描述符的__set__()方法,不过print(movie.score)还是会触发调用描述符的__get__()方法的,只是此时传入__get__()的instance实例对象是None
self.ratings=ratings

m1=movie('后天',7.5,3)
print(m1.score,m1.ratings) #7.5 3
#m2=movie('后天',7.5,-3) #ValueError: movie's score or ratings can't be negative or greater than 10

#m1.score=-1 #ValueError: movie's score or ratings can't be negative or greater than 10
movie.score.data[m1]=-1 #在这种基于“实例-属性值”字典实现的描述符下其实你还是可以绕过描述符的访问控制实现非法赋值
print(m1.score) #-1

当访问实例属性的时候,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class descriptor:
def __init__(self,label,default):
self.label=label
self.default=default

def __get__(self,instance,owner):
if instance==None:
return self
return instance.__dict__.get(self.label,self.default)

def __set__(self,instance,value):
if value<0 or value>10:
raise ValueError("movie's score or ratings can't be negative or greater than 10")
instance.__dict__[self.label]=value

class movie:
score=descriptor('score',0) #不同名也行,但这就重蹈@property的覆辙了,譬如名称改为'_score',此时通过“obj.score=非法值”自然会受到访问控制,但是“obj._score=非法值”则不受保护
ratings=descriptor('ratings',0)

def __init__(self,title,score,ratings):
self.title=title
self.score=score
self.ratings=ratings

m1=movie('后天',7.5,3)
print(m1.score,m1.ratings) #7.5 3
try:
m1.score=-10
except Exception as e:
print(e)

print(m1.__dict__) #{'title': '后天', 'score': 7.5, 'ratings': 3}

m1.__dict__['score']=-1000 #在这种情况下你可以通过直接修改实例字典来绕过描述符的访问控制,唉,有完美的解决方案吗
print(m1.score) #-1000

上述在类层级定义描述符的时候需要手动传入属性名称字符串,score=descriptor('score',0),这不太方便,为了隐藏该细节,可以借助元类,使得定义变回score=descriptor(0)这种简洁形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class descriptor:
def __init__(self,default,label=None): #此处参数定义要稍稍修改
self.label=label
self.default=default

def __get__(self,instance,owner):
if instance==None:
return self
return instance.__dict__.get(self.label,self.default)

def __set__(self,instance,value):
if value<0 or value>10:
raise ValueError("movie's score or ratings can't be negative or greater than 10")
instance.__dict__[self.label]=value

def __delete__(self,instance): #__delete__()不太常用,主要用来使实例(描述符)属性不可删除,但是你不能在__del__()方法中通过抛出异常阻止对象的删除
raise AttributeError('Can\'t delete this attribute!')

class addLabelMeta(type):
def __new__(cls,name,bases,local_dict):
for k,v in local_dict.items():
if isinstance(v,descriptor):
v.label=k
return super(cls,cls).__new__(cls,name,bases,local_dict)

class movie(metaclass=addLabelMeta):
score=descriptor(0)
ratings=descriptor(0)

def __init__(self,title,score,ratings):
self.title=title
self.score=score
self.ratings=ratings

m1=movie('后天',7.5,3)
print(m1.score,m1.ratings) #7.5 3
try:
m1.score=-10
except Exception as e:
print(e)

print(m1.__dict__) #{'title': '后天', 'score': 7.5, 'ratings': 3}

try:
del m1.score
except Exception as e:
print(e)

print(m1.score) #7.5

未完待续…