5-4-类代码编写细节

1. class 语句

Python 的 class 并不是声明式的,class 语句是对象的创建者并且是一个隐含的赋值运算。

1.1 一般形式

class 是复合语句。在头一行中,超类列在类名称之后的括号内,由逗号相隔。列出一个以上的超类会引起多重继承:

1
2
3
4
class name(superclass, ...):          # 赋值给变量名
data = value
def method(self, ...):
self.member = value

1.2 例子

当 Python 执行 class 语句时(不是调用类),会从头至尾执行其主体内的所有语句。在这个过程中,进行的赋值运算会在这个类作用域中创建变量名,从而成为对应的类对象内的属性。类就像模块和函数:

  • 就像函数一样,class 语句是本地作用域,由内嵌的赋值语句建立的变量名,就存在于这个本地作用域内。
  • 就像模块内的变量名,在 class 语句内赋值的变量名会变成类对象中的属性。

在 class 语句内赋值的变量名,会创建类属性,而内嵌的 def 则会创建类方法,但是,其他赋值语句也可制作属性。

例如,把简单的非函数的对象赋值给类属性,就会产生数据属性,由所有实例共享:

1
2
3
4
5
6
class SharedData:
spam = 42

x = SharedData()
y = SharedData()
x.spam, y.spam
(42, 42)
1
2
SharedData.spam = 99
x.spam, y.spam, SharedData.spam
(99, 99, 99)
1
2
x.spam = 88
x.spam, y.spam, SharedData.spam
(88, 99, 99)

对实例的属性进行赋值运算会在该实例内创建或修改变量名,而不是在共享的类中。

1
2
3
4
5
6
7
8
9
10
class MixedNames:
data = 'spam' # 赋值类属性
def __init__(self, value):
self.data = value # 赋值实例属性
def display(self):
print(self.data, MixedNames.data)

x = MixedNames(1)
y = MixedNames(2)
x.display(); y.display() # self.data 不同,MixedNames.data 相同
1 spam
2 spam

2. 方法

方法位于 class 语句的主体内,是由 def 语句建立的函数对象。方法的第一个参数总是接收方法调用的隐形主体,也就是实例对象。

在类方法中,按惯例第一个参数通常都称为 self。这个参数给方法提供了一个 hook,从而返回调用的主体,也就是实例对象。

2.1 例子

1
2
3
4
class NextClass:
def printer(self, text):
self.message = text
print(self.message)
1
2
3
x = NextClass()
x.printer('instance call')
x.message
instance call

'instance call'

2.2 调用超类构造函数

在构造时,Python 会找出并且只调用一个 __init__。如果要保证子类的构造函数也会执行超类构造时的逻辑,一般都必须通过类明确地调用超类的 __init__ 方法:

1
2
3
4
5
6
7
8
9
10
class Super:
def __init__(self, x):
pass

class Sub(Super):
def __init__(self, x, y):
Super.__init__(self, x) # 运行超类的 __init__
pass # 运行自己的 __init__

I = Sub(1, 2)

3. 继承

当对对象进行点号运算时,就会发生继承,而且涉及了搜索属性定义树(一个或多个命名空间)。

3.1 属性树的构造

  • 实例属性是由对方法内 self 属性进行赋值运算而生成的。
  • 类属性是通过 class 语句内的语句(赋值语句)而生成的。
  • 超类的连接是通过 class 语句首行的括号内列出类而生成的。

3.2 继承方法的专有化

继承会现在子类寻找变量名,然后才查找超类,子类就可以对超类的属性重新定义来取代默认的行为。

1
2
3
4
5
6
7
8
9
10
11
12
class Super:
def method(self):
print('in Super.method')

class Sub(Super):
def method(self): # 重载方法
print('starting Sub.method') # 添加行为
Super.method(self) # 运行默认行为
print('ending Sub.method')

x = Super()
x.method()
in Super.method
1
2
x = Sub()
x. method()
starting Sub.method
in Super.method
ending Sub.method

3.3 抽象超类

类的部分行为默认是由其子类所提供的。如果预期的方法没有在子类中定义,当继承搜索失败时,Python 会引发未定义变量名的异常。

4. 命名空间:完整的内容

  • 无点号运算的变量名与作用域相对应。
  • 点号的属性名使用的是对象的命名空间。
  • 有些作用域会对对象的命名空间进行初始化(模块和类)。

4.1 简单变量名:如果赋值就不是全局变量

无点号的简单变量名遵循 LEGB 作用域法则:

  • **赋值语句(X = value)。**使变量名成为本地变量:在当前作用域内,创建或改变变量名 X,除非声明它是全局变量。
  • **引用(X)。**在当前作用域内搜索变量名 X,之后是在任何以及所有的嵌套的函数中,然后是在当前的全局作用域中搜索,最后在内置作用域中搜索。

4.2 属性名称:对象命名空间

  • **赋值语句(object.X = value)。**在进行点号运算的对象的命名空间内创建或修改属性名 X。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时。
  • **引用(object.X)。**就基于类的对象而言,会在对象内搜索属性名 X,然后是其上所有可读取的类。对于不是基于类的对象而言,则是从对象中直接读取 X。

4.3 嵌套类

类有时在函数中进行嵌套,并生成,这是闭包的一种变体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
X = 1

def nester():
print(X) # 全局:1
class C:
print(X) # 全局:1
def method1(self):
print(X) # 全局:1
def method2(self):
X = 3
print(X) # 本地:3
I = C()
I.method1()
I.method2()

print(X) # 全局:1
nester()
print('-' * 40)
1
1
1
1
3
----------------------------------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
X = 1

def nester():
X = 2 # 隐藏全局变量
print(X) # 本地:2
class C:
print(X) # 嵌套 def 中:2
def method1(self):
print(X) # 嵌套 def 中:2
def method2(self):
X = 3 # 隐藏嵌套变量
print(X) # 本地:3
I = C()
I.method1()
I.method2()

print(X) # 全局:1
nester()
print('-' * 40)
1
2
2
2
3
----------------------------------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
X = 1

def nester():
X = 2 # 隐藏全局变量
print(X) # 本地:2
class C:
X = 3 # class 本地变量隐藏 nester 的 C.X 或 I.X
print(X) # 本地:3
def method1(self):
print(X) # 嵌套 def 中:2
print(self.X) # 继承的类本地变量:3
def method2(self):
X = 4 # 隐藏嵌套中的变量(nester,不是 class)
print(X) # 本地:4
self.X = 5 # 隐藏类中的变量
print(self.X) # 位于实例中:5
I = C()
I.method1()
I.method2()

print(X)
nester()
print('-' * 40)
1
2
3
2
3
4
5
----------------------------------------

4.4 命名空间字典

属性点号运算其实内部就是字典的索引运算,而属性继承其实就是搜索链接的字典。

1
2
3
4
5
6
7
class Super:
def hello(self):
self.data1 = 'spam'

class Sub(Super):
def hola(self):
self.data2 = 'eggs'
1
2
X = Sub()
X.__dict__ # 实例的命名空间字典
{}
1
X.__class__                           # 实例的类 
__main__.Sub
1
Sub.__bases__                         # 类的超类
(__main__.Super,)
1
Super.__bases__
(object,)

属性最后会位于实例的属性命名空间字典内,而不是类。

1
2
3
Y = Sub()
X.hello()
X.__dict__
{'data1': 'spam'}
1
2
X.hola()
X.__dict__
{'data1': 'spam', 'data2': 'eggs'}
1
Sub.__dict__.keys()
dict_keys(['__module__', 'hola', '__doc__'])
1
Super.__dict__.keys()
dict_keys(['__module__', 'hello', '__dict__', '__weakref__', '__doc__'])
1
Y.__dict__                        # Y 还是空的命名空间字典
{}
1
dir(X)                            # dir(object) 类似于 object.__dict__.keys() 调用,不过会引入一些系统属性
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'data1',
 'data2',
 'hello',
 'hola']

4.5 命名空间链接:树爬升

可以用实例和类的特殊属性 __class____bases__ 来显示类树:

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
# classtree.py 文件
"""
Climb inheritance trees using namespace links,
displaying higher superclasses with indentation for height
"""

def classtree(cls, indent):
print('.' * indent + cls.__name__) # 打印类名
for supercls in cls.__bases__: # 递归所有超类
classtree(supercls, indent + 3)

def instancetree(inst):
print('Tree of %s' % inst)
classtree(inst.__class__, 3)

def selftest():
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
class E: pass
class F(D, E): pass
instancetree(B())
instancetree(F())

if __name__ == '__main__': selftest()
Tree of <__main__.selftest.<locals>.B object at 0x0000022E079E8BA8>
...B
......A
.........object
Tree of <__main__.selftest.<locals>.F object at 0x0000022E079E8BA8>
...F
......D
.........B
............A
...............object
.........C
............A
...............object
......E
.........object

5. 类与模块的关系

  • 模块
  • 是数据/逻辑包。
  • 通过编写 Python 文件或 C 扩展来创建。
  • 通过导入来使用。
  • 实现新的对象。
  • 由 class 语句创建。
  • 通过调用来使用。
  • 总是位于一个模块中。
1