类
撰写时间:2025-09-10
修订时间:2025-09-16
类是数据与函数的封装。Python的类共同借鉴了C++及Modula-3的类的特性。
Python内置类型可由子类继承,支持运算符的重载。
类的定义及使用
下面代码,定义一个类,实例化为对象,最后调用对象的方法。
class MyClass:
"""A sample class"""
secret_num = 3.14
def show_value(self):
print(self.secret_num)
obj = MyClass()
obj.show_value()
print(obj.__doc__)
在MyClass类中,定义了一个名为secret_num的变量,也定义了一个名为show_value的函数。它们均可统称为属性 (attribute)。__doc__也是属性之一,返回该类的docstring。
类中的变量,称为数据属性 (data attribute)。
类中的函数,称为方法 (method)。
在定义类的方法时,第一个参数必须为self。这是面向对象编程技术所需。我们可以从一个类实例化多个对象,而每个对象均可以拥有各自独立的数据属性。在引用特定对象的方法时,编译器需要知道引用了哪个具体对象的数据属性。因此,当我们在调用:
obj.show_value()
时,因为obj是MyClass的一个实例,编译器在后台实际上调用:
MyClass.show_value(obj)
因此,类的方法定义中,第一个参数self必不可少。一些编程语言中对应的参数名称为this,并让用户在编写类方法的代码时有意隐藏了此参数,但Python不予隐藏。
在方法定义中不隐藏self参数的好处是,明示该参数只存活于各个方法中,在类的定义中无法使用。因此下面代码将抛出异常:
class MyClass:
self.secret_num = 3.14
即,在类的作用域内,不存在self此变量。
在类的定义中,可通过self来调用类的其他方法:
class MyClass:
def a_func(self):
value = self.get_sum(5, 3) # invoke get_sum method in MyClass
print(value)
def get_sum(self, a, b):
return a + b
代码:
obj = MyClass()
以函数调用的方式来进行类的实例化,返回该类的一个实例 (instance),并赋值于变量obj。最后,调用该变量的show_value方法,以打印数据属性secret_num的值。
实例初始化方法
类往往有较多的内部数据,因此在实例化一个类时,往往需要初始化这些内部数据。如果提供一个名为__init__的方法,则类在实例化过程中将自动调用它。因此可在该方法内完成所有数据的初始化工作。
class Car:
def __init__(self):
self.speed = 100
car = Car()
print(car.speed)
Python内部在此应用了设计模式中的模板模式 (template pattern )。像__init__这种只要定义就会被自动调用的方法,称为钩子方法。钩子方法的名称均以两条下划线__
作为前缀及后缀。定义钩子方法的本质是实现特定接口的过程,但它又多了一层约定,即这种接口方法在特定场合下将被自动调用
。这是回调函数被动地得到调用的典型用例。__init__方法是在类的初始化过程中将被自动调用。Python许多内置的类有多种这样的钩子方法。
可为__init__方法添加其他参数,以从客户端接收相应的初始值。
class Car:
def __init__(self, year, speed):
self.year = year
self.speed = speed
car = Car(1950, 120)
print(car.year, car.speed)
在Python中可以动态地创建或删除对象属性。通过赋值语句而自动创建,通过del函数来删除:
class Car:
def __init__(self):
self.speed = 100
car = Car()
car.year = 1920
print(car.speed, car.year)
del(car.speed)
print(car.speed)
类变量与实例变量
类中的变量,即上面所提及的数据属性,共分为2种。分别为类变量 (class variable) 与实例变量 (instance variable)。类变量是被所有实例共享的变量,实例变量是各个实例独享的变量。
class MyClass:
PI = 3.14
def __init__(self, num):
self.num = num
obj1 = MyClass(5)
obj2 = MyClass(10)
print(obj1.num)
print(obj2.num)
print(MyClass.PI)
上面,在类的定义中,在类的作用域内所定义的变量PI为类变量,在__init__方法中通过self.num来引用的变量num为实例变量。
而在函数调用中,通过obj1.num及obj2.num的方式来分别引用两个不同对象的实例变量num,通过MyClass.PI来引用MyClass的类变量PI。
各个对象的实例变量独立存在,不会相互影响。而类变量只有一份,所有实例均可共享。
class MyClass:
PI = 3.14
def __init__(self, num):
self.num = num
obj1 = MyClass(5)
obj1.num = 33
obj2 = MyClass(10)
obj2.num = 44
print(obj1.num)
print(obj2.num)
在引用类变量时,Python允许在各个实例上直接引用类变量:
class MyClass:
PI = 3.14
obj1 = MyClass()
obj2 = MyClass()
print(obj1.PI)
print(obj2.PI)
尽管PI是类变量,但从上面的代码来看,PI好像是各个实例独立存在的实例变量。而如果使用这种方式来进行赋值操作,则很容易出问题:
class MyClass:
PI = 3.14
obj1 = MyClass()
obj2 = MyClass()
obj1.PI = 50
print(obj1.PI)
print(obj2.PI)
print(MyClass.PI)
此时,当使用obj1.PI = 50,代码obj1.PI引用的不再是类变量,而是在obj1上创建了一个实例变量,该实例变量的名称正好与类变量名称完全相同。因此,obj1.PI = 50导致自此隐藏 (shadow) 了obj1对类变量PI的引用。以后,当我们看到代码obj1.PI时,我们还会清楚地记得它引用的到底是实例变量还是类变量吗?
为避免这种情况出现,最好的方法就是统一使用MyClass.PI来引用类变量。
类的继承
继承语法
使用以下代码编写具有继承关系的类:
class BaseClass:
pass
class DerivedClass(BaseClass):
pass
b = DerivedClass()
print(b)
在子类的定义中,将所继承的父类名称放在括号(...)
内即可。
调用isinstance函数来检测特定对象是否某类的实例。第一个参数为要判断的对象,第二个参数为类名。
class BaseClass:
pass
class DerivedClass(BaseClass):
pass
a = BaseClass()
b = DerivedClass()
print(isinstance(b, DerivedClass))
print(isinstance(b, BaseClass))
调用issubclass函数来检测特定类是否某类的子类。第一个参数为子类名称,第二个参数为父类名称。
class BaseClass:
pass
class DerivedClass(BaseClass):
pass
print(issubclass(DerivedClass, BaseClass))
子类的覆盖与扩展
class Shape:
def area(self):
print('This area method is of Shape.')
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
rect = Rectangle(5, 10)
print(rect.area())
triangle = Triangle(5, 10)
print(triangle.area())
Shape为父类,代表一个多边形的建模,只有一个方法area,用于求出多边形的面积。而在Shape类中,由于具体是多少边形未确定,因此area只不过是一个标志,表示每个Shape都有一个area方法,但具体实现将由子类来实现。因此,该方法只是简单地输出一条信息。
子类Rectangle及Triangle均是Shape的子类,这两个子类都定义了各自的area方法,都直接覆盖 (override ) 了父类的该方法。
上面代码,由于两个子类的area方法均覆盖了父类的方法,因此导致不会调用父类的area方法,也即不会输出文本信息。
继承的意义在于,我们可以说,每一个子类都是一个父类。例如,每个Rectangle或每个Triangle都是一个Shape。或说,只要父类具备的特征,保证每个子类都会具备。因此,我们可以使用父类来统一遍历每个不同的子类。
class Shape:
def area(self):
print('This area method is of Shape.')
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
rect = Rectangle(5, 10)
triangle = Triangle(5, 10)
shapes = [rect, triangle]
for shape in shapes:
print(shape.area())
有时,子类的方法不一定需要完全覆盖父类,而是希望在父类特定方法的基础上予以扩展,则可以在子类方法的定义中调用父类方法。
class Shape:
def area(self):
print('This area method is of Shape.')
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
super().area() # invoke area method of base class
# Shape.area(self) # another style of calling base
return self.width * self.height # and does business of its' own
rect = Rectangle(5, 10)
print(rect.area())
调用父类方法时,使用:
super().area()
或
Shape.area(self)
均可。
多重继承
在定义子类时,可将多个父类名称一并放在括号(...)
中,中间以逗号,
相隔,则可实现多重继承关系。
class BaseA:
def method_of_a(self):
print('This method is of BaseA.')
class BaseB:
def method_of_b(self):
print('This method is of BaseB.')
class C(BaseA, BaseB):
pass
c = C()
c.method_of_a()
c.method_of_b()
当两个以上的父类拥有相同的名称时,将根据深度优先
的原则,从左到右搜索父类的名称。
class BaseA:
def info(self):
print('This method is of BaseA.')
class BaseB:
def info(self):
print('This method is of BaseB.')
class C(BaseA, BaseB):
pass
c = C()
c.info()
私有属性
Python没有私有属性的约束。如果需要定义一个私有变量或私有方法,约定成俗的做法是在其名称前面添加一个下划线_
。
class MyClass:
def __init__(self):
self._count = 5
def get_count(self):
return self._count
a = MyClass()
print(a.get_count())
print(a._count) # still public attribute
print(a.count) # Exception: access to non-exsited attribute
正如上面所见,_count属性依旧是公共属性。只不过因为确实没有count属性,从而形成貌似count属性被隐藏起来的效果。
如果在类属性的名称前面添加2个下划线__
,则编译器会自动将其改名。
class MyClass:
def __init__(self):
self.__name = 'A secret name'
a = MyClass()
print(a._MyClass__name)
print(a.__name)
属性__name被自动改名为_MyClass__name。这种做法叫命名修饰 (name mangling),是编译器内部用以唯一标识具有复杂继承关系的各类的名称的一种方式。
它的命名规则是:名称前面至少2个下划线__
,名称后面至多1个下划线_
。
下列代码可确保父类特定方法被子类覆盖时,依旧可以在初始化方法中调用父类版本的方法。
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
a = Mapping([1, 2, 3])
print(a.items_list)
b = MappingSubclass([1, 2, 3])
print(b.items_list)
b.update([1, 2, 3], [4, 5, 6])
print(b.items_list)
这段代码的效果是,尽管子类的update方法被重载了,但依旧确保子类在初始化时,先运行父类的update方法;当运行到代码b.update(...)时,再调用子类的update方法。
原因是,在初始化方法中,父类的__update方法的名称将被替换为_Mapping.__update,在父类中,该名称指向了父类自己的update方法:
_Mapping.__update = update # update is version of Mapping
而当子类初始化时,将依旧运行上面的代码,从而先调用父类版本的update方法,以特定序列值初始化了一个list。而当后面显式地调用子类的update方法时:
b.update([1, 2, 3], [4, 5, 6])
则是调用了重载版本。此时,实例变量items_list已经存在先前的初始值。
类方法中的内置变量
__self__
实例方法中的__self__指向实例自身。
class MyClass:
def m(self):
pass
obj = MyClass()
print(obj)
print(obj.m.__self__)
print(id(obj.m.__self__) == id(obj) )
__func__
实例方法中的__func__指向在类中定义的方法。
class MyClass:
def m(self):
pass
obj = MyClass()
print(obj.m.__func__)
print(obj.m)
print(id(obj.m.__func__) == id(obj.m))
obj.m.__func__是在类上定义的方法MyClass.m;而obj.m将导致编译器内部将obj作为第一个参数来调用MyClass.m,这是一个代理对象。两者不是同一个对象。
我们甚至可以在客户端这样调用:
class MyClass:
def __init__(self, name):
self.name = name
def greet(self):
print(self.name)
obj1 = MyClass('Mike')
obj2 = MyClass('Tom')
MyClass.greet(obj1)
obj1.greet.__func__(obj1)
魔术方法
所谓魔术方法,是指一旦被定义,将会在特定场合下自动被调用的方法。
__str__ 与 __repr__
内置函数str与repr均可用于输出特定对象的字符串表示。
s = 'Hello'
print(s)
print(str(s))
print(repr(s))
直接打印一个字符串,与先使用str函数转换后再打印,效果一样。
打印str函数返回值时,没有单引号;打印repr函数返回值时,左右两边均有单引号。
str函数主要目的在于方便阅读,repr函数主要目的是在开发过程中精准地查看对象的类型及其内容。
如果提供,类的__str__方法将供str函数调用,类的__repr__方法将供repr函数调用。
class MyClass:
def __init__(self, category):
self.category = category
def __str__(self):
return self.category
def __repr__(self):
return f"'{self.category}'"
obj = MyClass('Tree')
print(obj)
print(str(obj))
print(repr(obj))
为类自定义遍历方法
特定类可以为该类所封装的数据,提供自定义的遍历方法。
我们所熟悉的遍历
arr = [1, 2, 3]
for elem in arr:
print(elem)
使用for ... in语句,可以直接遍历一个list。
但list只不过是数据的静态组合,为何能一个一个依序取出来?
两个因素,一是对象为可迭代对象,二是for ... in能自动处理可迭代对象的数据。
初识可迭代对象
可迭代对象 (iterable object) 是指,可从该对象中,一次依序只取出一个数值。
常见的实现了iterable接口的数据有:str, list, tuple, dict等等。
迭代器基本原理
对实现了iterable接口的对象,调用内置函数iter以获取一个iterator对象。我们将之称为迭代器。
a_iterator = iter([1, 2, 3])
print(a_iterator)
next_element = a_iterator.__next__()
print(next_element)
iterator对象以流的方式,封装了集合中的各个数据。它有好几个方法,其中最常用的方法是__next__,用以返回流中的下一个数据。
可以持续调用__next__方法,以不断返回流中的下一个数据。如果流中不再有任何数据,则抛出StopIteration异常。
a_iterator = iter([1, 2, 3])
print(a_iterator.__next__()) # 1
print(a_iterator.__next__()) # 2
print(a_iterator.__next__()) # 3
print(a_iterator.__next__()) # raise StopIteration exception
上面代码,当第4次读取数据时,抛出了StopIteration异常。
根据此特点,我们可以使用while语句编写安全遍历的代码如下:
a_iterator = iter([1, 2, 3])
while True:
try:
value = a_iterator.__next__()
print(value)
except StopIteration:
break
except Exception as e:
print(e)
break
调用iter函数返回一个迭代器,调用迭代器的__next__方法返回流中下一个数据,迭代完毕时,将抛出StopIteration异常。这就是迭代器的基本原理。
隐藏难看的代码
迭代器iterator对象的方法__next__前后各有两条下划线,终究难看。内置函数next应运而生,以一个迭代器iterator对象作为参数来调用next函数,则next函数在内部将自动调用迭代器的__next__方法。
a_iterator = iter([1, 2, 3])
while True:
try:
value = next(a_iterator)
print(value)
except StopIteration:
break
except Exception as e:
print(e)
break
再探可迭代对象
迭代器接口 (iterator protocol) 要求有2个方法:
iterator.__iter__()
iterator.__next__()
其中,__iter__返回iterator对象自身。__next__返回iterator的下一个数据。
iterator对象实现了以上的接口。
任何一个对象,只要其定义了__iter__方法,且调用它后能返回一个iterator对象,则它就是可迭代对象 (iterable object)。
print('abc'.__iter__)
print([1, 2, 3].__iter__)
print((1, 2, 3).__iter__)
print({'year': 1920, 'speed': 120}.__iter__)
for ... in语句利用这种特性,帮助我们快速地遍历各种可迭代对象。
for item in [1, 2, 3]:
print(item)
相比于上节我们自己通过while语句来编写的遍历代码,它无需我们自己处理异常,代码更高效。
将类转换为可迭代对象
按照上面的规则,我们将一个类转换为可迭代对象。
class MyClass:
def __init__(self):
self.items = [1, 2, 3]
def __iter__(self):
return iter(self.items)
obj = MyClass()
for item in obj:
print(item)
但这种方式,我们只能说,MyClass是一个可迭代对象,但我们不能说,MyClass实现了迭代器接口。
上面,MyClass能被迭代的原因,是利用实例变量items创建了一个迭代器,然后返回此迭代器供for ... in语句迭代。
为类实现迭代器接口
如果希望MyClass自身能被迭代,则第一:将其__iter__方法改为返回它自己;第二:为其定义一个__next__方法,并在该方法中确定如何返回下一个数据、何时应抛出StopIteration异常的规则。
class MyClass:
def __init__(self):
self.items = [1, 2, 3]
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index == len(self.items):
raise StopIteration
item = self.items[self.index]
self.index += 1
return item
obj = MyClass()
for item in obj:
print(item)
唯有这样,我们才能说MyClass实现了迭代器接口。
生成器
生成器函数 (generator function),返回一个生成器迭代器 (generator iterator)。生成器迭代器可用于for ... in语句或next函数中。
生成器函数也可简称为生成器。
下面是使用生成器函数的代码:
def gen():
yield 1
yield 2
gen_iter = gen()
print(type(gen_iter))
for i in gen_iter:
print(i)
gen函数即是生成器函数。在该函数中,两次使用yield表达式,依序生成了1和2两个数值。
生成器函数虽不使用return语句,却有返回值。返回值的数据类型为generator,也即上面所谈到的生成器迭代器。之后,将其投喂给for ... in语句迭代。
def gen():
yield 1
yield 2
gen_iter = gen()
print(isinstance(gen_iter, iter))
print(gen_iter.__iter__)
print(gen_iter.__next__)
从上面代码可看出,generator也是一种迭代器,故称生成器迭代器。它实现了迭代器接口。
综上,在生成器函数中,它能将多个yield语句所生成的结果,自动打包为一个迭代器并返回,为码农节省了许多时间。
可将yield语句置于for ... in语句中一并生成多个数值。
class MyClass:
def __init__(self):
self.items = [1, 2, 3]
def get_iter(self):
for item in self.items:
yield item
obj = MyClass()
generator = obj.get_iter()
for item in obj.get_iter():
print(item)
而若使用yield 表达式,则可编写更简练的代码:
class MyClass:
def __init__(self):
self.items = [1, 2, 3]
def get_iter(self):
yield from self.items
obj = MyClass()
generator = obj.get_iter()
for item in obj.get_iter():
print(item)
Lambda表达式也是一种可生成匿名函数的生成器。由于yield语句需放在函数内使用,因此两者结合使用,可编写更简练的代码:
class MyClass:
def __init__(self):
self.items = [1, 2, 3]
obj = MyClass()
for item in (lambda : (yield from obj.items))():
print(item)
上面,MyClass无需再定义一个多余的方法。
一些函数支持可迭代对象作为参数传入。这种情况下,可使用生成器表达式。
result = sum(i for i in range(5))
print(result)
函数sum的参数类型是可迭代对象,上面代码参数部位的表达式即为生成器表达式。下面只对其中的所有偶数求和。
result = sum(i if i % 2 == 0 else 0 for i in range(5))
print(result)