类和方法

Python的OOP模型主要思想:在一堆对象中查找属性,并为函数定一个特殊的第一个参数。


1. 类代码编写基础

1.1 类和实例化

python面向对象中有两种对象:类对象和实例对象。

类对象提供默认行为,实例对象是程序处理的实际对象:各自都有自己的命名空间。

在class语句内,任何赋值语句都会产生类属性:

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

class ShareDate:

spam = 42 # 不是在__init__函数中,有点类似于C++的静态成员变量



x = ShareDate()

y = ShareDate()

print(x.spam) # 42

print(y.spam) # 42

ShareDate.spam = 89

print(x.spam) # 89

print(y.spam) # 89

1.2 类方法

类方法:与普通的def函数不同的是,类中的方法第一个参数是self,即引用正处理的实例对象。self参数同C++的this指针很相似。

1.3 类继承,重载

除了继承和重载,在子类中还可以重新定制构造函数,这也是很常见的。在1.8中会通过完整的例子来讲定制构造函数。

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

class FirstClass:

def setdata(self, value):

self.data = value

def display(self):

print(self.data)



class SecondClass(FirstClass): # 类继承

def display(self): # 类函数的重载

print('current value = "%s" '%self.data)



z = SecondClass()

z.setdata(42)

z.display() # current value = "42"

1.4 类是模块的属性

类名称总是存在与模块文件中的,类是模块对象的属性。类和模块都是命名空间,但类对应于语句(而不是整个文件),而且支持多个实例、继承以及运算符重载这些OOP概念。

需理解_init_.py文件。

1.5 运算符重载,类可以截获Python运算符

实际上,“运算符重载”只是意味着在类方法中拦截内置的操作……当类的实例出现在内置操作中,Python自动调用你的方法,并且你的方法的返回值变成了相应操作的结果。以下是对重载的关键概念的复习:

  • 运算符重载让类拦截常规的Python运算。

  • 类可重载所有Python表达式运算符

  • 类可以重载打印、函数调用、属性点号运算等内置运算

  • 重载使类实例的行为像内置类型。

  • 重载是通过特殊名称的类方法来实现的。

举个栗子:

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

class ThirdClass(SecondClass):

def __init__(self, value):

self.data = value

def __add__(self, other):

return ThirdClass(self.data + other)

def __str__(self):

print("__str__ is called")

return "[ThirdClass: %s]" % self.data

def mul(self, other):

self.data *= other



a = ThirdClass("abc")

a.display()

print(a) # 自动调用__str__

b = a + "xyz" # 自动调用__add__

b.display()

a.mul(3) # 显示结果与python内置类型的行为不同

a.display()



# 运行结果:



current value = "abc"

__str__ is called

[ThirdClass: abc]

current value = "abcxyz"

current value = "abcabcabc"

意思就是,当你的类出现在内置函数比如+,print这样的表达式中,就会被_add_,__str__这样的类方法拦截。但如果在你的类中,没有定义这样的方法,就会很有可能报错了~那么,为什么会拦截呢?还需要看底层代码~猜想应该是对自定义的类执行+操作时,就会自动从类树中寻找__add__方法吧~

| 方法 | 重载 | 调用 |

| —————| ———| ————————–|

| _init_ | 构造函数 | 对象建立:X = Class(args)|

|_del_ |析构函数| X对象收回|

|_add_ |运算符 + | 如果没有__iadd__, X + Y, X += Y

|_or_ |运算符(位OR)| 如果没有_ior_

|_repr_, _str_| 打印、转换| print(X), repr(X), str(X)

|_call_ |函数调用| X(*args, **kargs)

|_getattr_| 点号运算| X.undefined

|_setattr_| 属性赋值语句| X.any = value

|_delattr_| 属性删除| del X.any

|_getattribute_| 属性获取 |X.any

|_getitem_| 索引运算|X[key],X[i:j],没__iter__时的for循环和其他迭代器

|_setitem_ |索引赋值语句| X[key] = value, X[i:j] = sequence

|_delitem_|索引和分片删除| del X[key], del X[i:j]

|_len_| 长度 |len(X), 如果没有__bool__, 真值测试

|_bool_| 布尔测试| bool(X), 真测试(在Python 2.6中叫做__nonzero__)

|_lt_, _gt_,_lt_,_ge_,_eq_, _ne|特定比较| X<Y, X>Y, X<=Y, X>=Y, X == Y, X != Y(或者在Python 2.6中只有_cmp)

|_radd_| 右侧加法| Other + X

|_iadd| 实地(增强的)加法| X += Y(or else _add)

|_iter_, _next_ |迭代环境| I = iter(X), next(I); for loops, in if no _contains_, all comprehensions, map(F, X), 其他(__next__在Python2.6中成为next)

|_contains_ |成员关系测试| item in X(任何可迭代的)

|_index_| 整数值 |hex(X), bin(X), oct(X), O[X], O[X:](替代Python 2中的_oct_,__hex__)

|_enter_, _exit_ |环境管理器| with obj as var:

|_get_, _set_,_delete_|描述符属性| X.attr, X.attr = value, del X.attr

|_new_ |创建 |在__init__之前创建对象

所有重载方法的名称前后都有两个下划线,以便把同类中定义的变量名区别开来。特殊方法名称和表达式或运算的映射关系,是由Python语言预先定义好的(在标准语言手册中有说明)。例如名称,__add__按照Python语言的定义,无论__add__方法的代码实际在做些什么,总是对应到了表达式 + 。

如果没有定义运算符重载方法的话,它可能继承自超类,就像任何其他的方法一样。运算符重载方法也都是可选的……如果没有编写或继承一个方法,你的类直接不支持这些运算,并且试图使用它们会引发一个异常。一些内置操作,比如打印,有默认的重载方法(继承自Python 3.x中隐含的object类),但是,如果没有给出相应的运算符重载方法的话,大多数内置函数会对类实例失败。

也就是虽然自定义的类里面没有重载类方法__str__或是_add_,但Python3所有自定义的类都是继承了object类的,含有默认的_add_,_str_,但大部分会对实例失败。

还是举个之前的那个栗子,这次我们不定义_add_,_str_:

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

class ThirdClass(SecondClass):

def __init__(self, value):

self.data = value



print(dir(ThirdClass))

print("***************")

a = ThirdClass("abc")

print(a)

b = a + "xyz"

b.display()

运行结果:

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

['__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__', 'display', 'setdata']

***************

<__main__.ThirdClass object at 0x7f30d8472da0>

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-40-8717227036a8> in <module>()

7 a = ThirdClass("abc")

8 print(a)

----> 9 b = a + "xyz"

10 b.display()



TypeError: unsupported operand type(s) for +: 'ThirdClass' and 'str'

我们可以看到,ThirdClass类本身继承了object的很多内置方法,包括__str__,只是显示的与我们之前自定义的__str__不一样而已。而__add__就会出现异常了。

1.6 类和字典的关系

python的类模型相当动态。类和实例只是命名空间,属性是通过赋值语句动态建立的。

模块的命名空间实际上是以字典的形式出现的,类和实例对象也是如此。可用__dict__来显示这一点,属性点号运算其实就是字典内的索引运算,而属性继承其实就是搜索链接的字典。

1.7 测试脚本文件代码

1
2
3
4
5
6
7

if __name__ == "__main__":

# self-test

pass

只有在当前脚本文件下运行时,上述if语句条件才为真。因此,这样就可以在文件底部运行测试语句,而不会在导入文件的时候运行。

1.8 定制构造函数

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

class Person():

def __init__(self, name, job=None, pay=0):

self.name = name

self.job = job

self.pay = pay

def LastName(self):

return self.name.spilt()[-1]

def giveRaise(self, persent):

self.pay = int(self.pay * (1 + persent))

def __str__(self):

return self.__class__.__name__ + ':{0} {1}'.format(self.name, self.pay)



class Manager(Person):

def __init__(self, name, pay): # 定制构造函数

Person.__init__(self, name, 'mgr', pay) # 必须手动调用超类

def giveRaise(self, persent, bonus=0.1):

Person.giveRaise(self, persent + bonus)

if __name__ == "__main__":

tom = Manager('Tom Jones', 5000)

print(tom)

print(tom.__class__.__name__)

print(Manager.__bases__)

print(tom.__dict__.keys())

print(getattr(tom, 'name') == tom.name) # 虽然类和字典类似,但是不能写tom['name']



# 运行结果:



Manager:Tom Jones 5000

Manager

(<class '__main__.Person'>,)

dict_keys(['name', 'job', 'pay'])

True

在初始化中self.name=name这里看起来有些多余,但实际上name, job在__init__函数中只是本地变量但self.job是实例中的一个属性,这两个是不同的变量,只是恰好名字一样__init__函数没什么奇妙之处,只是在产生一个实例时,会自动调用,并且有特殊的第一个参数

这其中用到了特殊的类属性,

  • _class_

  • _name_

  • _bases_

  • _dict_

1.9 把对象存储到数据库中

  • pickle 任意Python对象和字符串之间的序列化

  • dbm 实现一个可通过键访问的文件按系统,以存储字符串

  • shelve 使用另两个模块把python对象存储到文件中

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

bob = Person('bob smith')

sue = Person('Sue Jones', job='dev', pay=10000)

tom = Manager('Tom Jones', pay=5000)



import shelve

db = shelve.open('Persondb')

for object in (bob, sue, tom):

db[object.name] = object

db.close()



db = shelve.open('Persondb')

print(db)

print(len(db))

print(list(db.keys()))

print(db['bob smith'])





#运行结果:

<shelve.DbfilenameShelf object at 0x7f915bdce588>

3

['bob smith', 'Sue Jones', 'Tom Jones']

Person:bob smith 0

1.10 抽象超类

对于抽象基类,需要子类来填充。当行为无法预测,非得等到更为具体的子类编写时才知道,通常可用这种方式把类通用化。OOP软件框架也使用这种方式作为客户端定义、可定制的运算的实现方法。

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

from abc import ABCMeta, abstractmethod



class Super(metaclass=ABCMeta):

def delegate(self):

self.action()

@abstractmethod

def action(self):

pass



class Sub1(Super): # 没有定义action函数,就会报错

pass



class Sub2(Super):

def action(self):

print('spam')



X2 = Sub2()

X2.delegate()

X1 = Sub1()

X1.delegate()



# 运行结果

spam

TypeError: Can't instantiate abstract class Sub1 with abstract methods action

1.11 Python命名空间

直接看代码,看懂了也就了解了Python的命名空间了~

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

# manyname.py



X = 11



def f():

print(X)



def g():

X = 22

print(X)



class C:

X = 33

def m(self):

X = 44

self.X = 55



print(X) # 11 module,模块属性

f() # 11 global

g() # 22 local,函数内的本地变量

print(X) # 11 模块属性没变



obj = C()

print(obj.X) # 33 类属性

obj.m()

print(obj.X) # 55 实例属性

print(C.X) # 33



1.12 super()超类

首先要弄清楚为什么要使用super()。是因为我们想要调用父类中已经被子类覆盖的方法。最常见的比如 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

class A:

def __init__(self, batch_size, vocab_size):

self._batch_size = batch_size

self._vocab_size = vocab_size

print('A.spam')



class B(A):

def __init__(self):

print('B.spam')



b = B()

print(b._batch_size)

运行结果:

1
2
3
4
5
6
7
8
9
10
11

B.spam

Traceback (most recent call last):

File "/home/panxie/Documents/NLP实战/text classification/06-memory-networks/test.py", line 25, in <module>

print(b._batch_size)

AttributeError: 'B' object has no attribute '_batch_size'

结果报错:子类中不存在_batch_size这个属性。所以必须在子类的 init()函数中调用父类的 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

class A:

def __init__(self, batch_size, vocab_size):

self._batch_size = batch_size

self._vocab_size = vocab_size

print('A.spam')



class B(A):

def __init__(self):

super(B, self).__init__(8, 1000)

print('B.spam')



b = B()

print(b._batch_size)

运行结果:

1
2
3
4
5
6
7

A.spam

B.spam

8

那我们可不可以不用super()呢,直接调用A类的方法也行呀,比如:

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

class A:

def __init__(self, batch_size, vocab_size):

self._batch_size = batch_size

self._vocab_size = vocab_size

print('A.spam')



class B(A):

def __init__(self):

A.__init__(self, 8, 10)

print('B.spam')



b = B()

print(b._batch_size)

运行结果跟上面一样,貌似也没毛病。但是在多继承的时候,父类会重复调用什么的,比如

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

class Base:

def __init__(self):

print('Base.__init__')



class A(Base):

def __init__(self):

Base.__init__(self)

print('A.__init__')



class B(Base):

def __init__(self):

Base.__init__(self)

print('B.__init__')



class C(A,B):

def __init__(self):

A.__init__(self)

B.__init__(self)

print('C.__init__')



c = C()

print(C.__mro__)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13

Base.__init__

A.__init__

Base.__init__

B.__init__

C.__init__

(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>)

可以发现基类被重复调用了两次。这显然是不好的。但是换成 super() 每个类的 init()方法保证只会调用一次。具体原理可以参考 python-cookbook

作者

Xie Pan

发布于

2018-03-25

更新于

2021-01-27

许可协议

评论