类和方法

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


1. 类代码编写基础

1.1 类和实例化

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

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

1
2
3
4
5
6
7
8
9
10
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
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
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__ 特定比较 XY, 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
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
['__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
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
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
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
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
# 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
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
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
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
A.spam
B.spam
8

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
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