Python 中的类
首先,牢记这句话:在 Python 的世界里,一切皆对象。
咱们的 Python 学习课程,从来不讲长篇大论,全都是通过一段段代码来拆解基本的知识点,最终让你能够写出真正的 Pythonic 代码。
所以,本节课的内容也是一步步让大家去了解 Python 类的基本概念、运行机制以及有哪些魔法方法。
一、什么是类?
说到面向对象编程,大家一定对“类”这个名字不陌生,英文是 class。类其实就是用来表示具有相同属性的对象的一个概念,比如人、猫、狗、兔子等不同的动物种类,我们可以叫做动物类。
可能有的同学在学 C/C++ 、Java 等高级语言的面向对象编程时,被告知对象是类的一个实例,就会惯性地认为 Python 中类和对象是同级别的存在。
事实上并不是这样,还记得文章开头的那句话——“在 Python 的世界里,一切皆对象。”——吗?类也不例外。所以,对象才是 Python 中的最顶级的存在!而经常和“类”相提并论的“对象”则是指类的实例化对象。
这么干说可能不太容易理解,下面我们先喝口水(就没那么干了,哈哈,活跃下气氛!),然后再来看看 Python 中的类是长什么样子。
class Say: """一个简单的类""" to_name = 'xxx' def say(self): return '520 1314'print(Say.to_name)I = Say() # 初始化一个实例对象 Iprint(I.to_name, I.say())# 结果:xxx 520 1314
上面的例子中,Say 就是一个类,而 I 就是 Say 类的一个实例化对象(简称实例)。我们应该这样理解:I 这个实例,具有 Say 类的属性 to_name 和 say。所以,大家以后不要再把“类”和“对象”混为一谈了。
在 Say 类中,to_name 是类属性,可以不用初始化直接调用,比如 print(Say.to_name);而 say 方法(类里一般把函数叫做方法)则是实例属性,需要初始化一个实例对象后才可以调用。
如果想修改类或实例属性的值,直接赋值修改即可。
Say.to_name = 'ddd'print(Say.to_name)I.to_name = 'yyy'print(I.to_name, I.say())# 结果:yyy 520 1314
注意:实例的属性可以任意增加,比如已经存在的实例 I,通过 I.name = 'Jim',就相当于给实例对象 I 增加了一个值为 'Jim' 的属性 name,但你再通过 you = Say() 初始化一个实例 you 仍然不会有 name 属性。
有个问题,这个类的功能太单一,只能显示和修改 to_name,say的返回值只有 520 1314。那么,要如何修改才能更加通用呢?
请看下方的修改内容:
class Say: """一个灵活的类""" def __init__(self, to_name='xxx', say_words='520 1314'): self.to_name = to_name self.say_words = say_words def say(self): return self.say_wordsI = Say() # 对象以默认值初始化一个实例print(I.to_name, I.say()) # 结果:xxx 520 1314I = Say('yyy', '555') # 对象以指定值初始化一个实例print(I.to_name, I.say()) # 结果:yyy 555
修改后的代码可以看出一个最大的改动,就是增加了一个 __init__ 函数。
__init__ 方法是类的构造函数,在类初始化时一定会调用。在第一个例子中虽然没写,但 Python 默认也是有这个方法存在的,只不过是一个空函数。一旦写了,我们就覆盖了默认的空 __init__ 方法,可以在这个函数里做我们想做的事情,比如在类初始化的时候给实例的两个属性赋初值 yyy 和 555。
然后是 to_name 变成了 self.to_name,新增了 self.say_words。如果同样的属性名称同时出现在实例和类中,属性查找会优先选择实例。
这时就有一个重要的问题需要解答:类里面经常看到的self是什么?
很多初学者认为 self 是一个关键字,其实并非如此!它只是一个约定俗成的参数名称,你可以写成任何合法的变量名,比如 cls:
class Say: """一个灵活的类""" to_name = 'ooo' # 类成员变量 def __init__(cls, to_name='xxx', say_words='520 1314'): cls.to_name = to_name # 类实例的成员变量初始化 cls.say_words = say_words def say(cls): return cls.say_words
而且还需要记住一个关键概念:self 指向的是实例的内存地址,而不是类的。证明代码如下:
class Person: def __init__(self, name): self.name = name # 打印 self 的内存地址,证明它就是实例本身 print(f"Inside __init__, self id: {id(self)}")print(f"Class Person id: {id(Person)}") # 和 Inside 打印的 id 不同p = Person("xxx")print(f"Outside, p id: {id(p)}") # 和 Inside 打印的 id 一致
注意:Python 中一切皆对象,不仅 self 和 p 是对象,Person 类本身也是一个对象。我们可以使用 id(obj) 查看任何一个对象的id(内存地址)。
类属性 vs 实例属性:一个经典的“坑”
这是面试和实战中极易翻车的点。
陷阱:当通过实例去修改一个可变对象类型的类属性时,会发生什么?class MyClass: shared_list = [1, 2, 3]print(MyClass.shared_list)obj1 = MyClass()obj2 = MyClass()obj1.shared_list.append(4)obj2.shared_list.append(5)print(obj1.shared_list)print(obj2.shared_list)print(MyClass.shared_list)
运行上述代码后,你会发现,最后3个 print 打印出来的结果都是 [1, 2, 3, 4, 5]。因为在类属性中的可变对象在实例化对象后是共享内存的,所以上面的 obj1 和 obj2 修改 shared_list 后,类属性 share_list 的值也随之改变。
金句: “千万不要在类属性中定义可变对象(如列表、字典),除非你非常清楚自己在做什么,否则这就是一颗共享状态的定时炸弹。”
下面是在类中正确使用可变对象的方式:
class MyClass: def __init__(self): self._list = [1, 2, 3]obj1 = MyClass()obj2 = MyClass()obj1._list.append(4)obj2._list.append(5)print(obj1._list) # 结果:[1, 2, 3, 4]print(obj2._list) # 结果:[1, 2, 3, 5]
说到这里,我们又看到了 __init__ 函数,之前说这个函数是类默认就有的方法,像这种双下划线( __ )包含的方法,我们称之为魔法方法。那么,类中还有哪些类似的魔法方法呢?下面,我们就一起来看看吧!
二、类中的魔法方法
字符串的门面
class MyClass: def __init__(self): self.name = 'CMOS三好先生'I = MyClass()print(I)
这段代码打印实例对象 I 时会出现 <__main__.MyClass object at 0x...> 这种看上去不可解读的代码。那么,如何打印出可读性更强的内容呢?
解决方案:使用魔法方法 __str__ 和 __repr__。
__repr__:给开发者看(交互式命令行中调用)。
class MyClass: def __init__(self): self.name = 'CMOS三好先生' def __str__(self): return f'[__str__]这是一个 MyClass 类:{self.name}' def __repr__(self): return f'[__repr__]这是一个 MyClass 类:{self.name}'
在 Python 命令行中运行的结果如下:
这个只是一个功能展示,一般来说类都是比较复杂的,在 __str__ 方法里可以把需要展示的属性都构造为字符串展示给用户,__repr__ 方法则一般不用实现。
让对象可迭代
__getitem__ 与 __len__
在了解了两个比较基础的魔法方法后,让我们再来学习复杂一点儿的。
下面的代码展示了一个实战例子:编写一个类似 List 的自定义扑克牌类。
import randomclass Poke: point = [str(n) for n in range(2, 11)] + list('JQKA') suits = '♠ ♣ ♥ ♦'.split() def __init__(self): self._cards = [f'{suit}{point}' \ for suit in self.suits for point in self.point] def __len__(self): return len(self._cards) def __getitem__(self, position): return self._cards[position]# 只要实现了__getitem__ 与 __len__# 这个类就自动支持切片、迭代、random.choice!poke = Poke()print(poke[0]) # 支持索引print(poke[:3]) # 支持切片for i in range(0, 13): print(random.choice(poke), end=' ')
让对象成为迭代器
__iter__ 与 __next__
这两个魔法方法能够让类实例对象成为一个迭代器(后面的课程会详细讲解),比如下面的代码就是一个执行反向循环的迭代器。
class Reverse: """对一个序列执行反向循环的迭代器。""" def __init__(self, data): self.data = data self.index = len(data) def __len__(self): return len(self.data) def __iter__(self): return self def __next__(self): if self.index == 0: raise StopIteration self.index = self.index - 1 return self.data[self.index]# 只要实现了 __iter__ 和 __next__# 这个类的实例就自动具有迭代器的特性it = Reverse('!uoy evol I')for _ in range(0, len(it)): print(next(it), end='')
除了上述的那些魔法方法,还有其他的一些方法,比如:__new__、__unicode__、__call__、__setattr__、__getattr__、__getattribute__、__delattr__、__setitem__、__delitem__、__del__、__dir__、__dict__、__exit__、__all__等等,由于这些方法基本用不到,在这里就不多占篇幅一一讲解了。(事实上,我也不知道还有这么多(ಥ◡ಥ))好了,关于类的初次见面就到这里,我相信同学们对类已经有了一定的认识,下节课我们再深入学习一下类的继承与类装饰器。同学们,下课!
本节课的课后作业
实现一个自定义的字典类。要求有dict_obj[key] 和 dict_obj.get(key) 两种获取值的方法,以及 keys()、values() 和 items() 方法。