类和对象
约 2772 字大约 9 分钟
2025-04-22
类和对象是面向对象编程中最基本的两个概念
许多教材从数学角度形式化地定义类,将其描述为数据与子程序的集合,这些数据和子程序共同承担一组内聚且明确定义的职责。然而,若仅仅将类视为类似 C 语言结构体的简单数据容器,便失去了面向对象编程的真正价值。类的本质应是抽象数据类型(Abstract Data Type, ADT),即对现实世界逻辑的代码化抽象,封装数据与行为,体现清晰的职责与灵活的扩展性
类就像是一个模具,用来描述某一类事物的共性(比如人都有名字、年龄、会说话等),它定义了对象应该有哪些属性和可以做什么事。类是“抽象的概念”,现实中是不存在的。而对象呢,就是用这个模具构造出来的一个个具体的鲜活的个体,是“类的具体表现”
类
按照定义,类是由数据(即变量)和子程序(即函数)组成的集合,类中的变量也被称作属性,类中的函数也被称作方法。Python 使用 class
关键字定义一个类,一个典型的类定义方法是这样的:
class MyClass:
"""一个简单的示例类"""
# i 是 MyClass 类的属性
i = 12345
# f 是 MyClass 类的方法
def f(self):
return 'hello world'
类中的属性和方法只能在类内部使用,外部是无法使用的:
class MyClass:
"""一个简单的示例类"""
i = 12345
# 可以这样
print(f'类中直接使用 i = {i}')
def f(self):
# 也可以这样
print(f'类属性中使用 i = {i}')
return 'hello world'
# 不可以这样
# print(f'类外使用 i = {i}')
对象
对象(object,也称实例 instance)是类实例化后的产物,对象要依托于类而存在,没有类就不可能有对象。对类进行实例化操作(也就是“调用”类对象) 会创建一个空对象,可以通过 对象.属性
或 对象.方法
的方式调用属性和方法:
class MyClass:
i = 12345
def f(self):
return 'hello world'
c1 = MyClass()
c2 = MyClass()
print(c1.i) # 12345
print(c2.i) # 12345
print(c1.f()) # hello world
许多类都希望创建的对象实例是根据特定初始状态定制的。 因此一个类可能会定义名为 __init__()
的特殊方法,当一个类定义了 __init__()
方法时,类的实例化会自动为新创建的类实例唤起 __init__()
,就像这样:
class MyClass:
def __init__(self):
self.data = []
c = MyClass()
c.data.append(1)
print(c.data) # [1]
当然,__init__()
方法还有一些参数用于实现更高的灵活性。 在这种情况下,提供给类实例化运算符的参数将被传递给 __init__()
。 例如:
class MyClass:
def __init__(self, a, b):
self.a = a
self.b = b
c = MyClass(1, 2)
print(c.a, c.b) # 1 2
self
Python 类中定义方法时,第一个参数(到目前为止)总会是 self
(不是必须命名为 self
,self
也不是 Python 的关键字,只是一个普通的变量名,但这是在 Python 圈约定俗成的),而当我们使用对象调用方法时,self
参数又不需要我们手动传递,那么 self
表示的是什么呢?看这个例子:
class MyClass:
def f(self):
return f'方法中的 self 是:{self} {id(self)}'
print('类是:', MyClass, id(MyClass)) # <class '__main__.MyClass'> 4611659264
c1 = MyClass()
print(c1.f()) # <__main__.MyClass object at 0x104698c20> 4368993312
print('c1 是:', c1, id(c1)) # <__main__.MyClass object at 0x104698c20> 4368993312
c2 = MyClass()
print(c2.f()) # <__main__.MyClass object at 0x104607250> 4368396880
print('c2 是:', c2, id(c2)) # <__main__.MyClass object at 0x104607250> 4368396880
可以看到,self
就是调用方法的那个对象(例如 c1
对象调用了方法 f
,那么 f
方法的 self
参数就是 c1
对象),Python 中的 self
类似于 Java 中 this
的用法,只是 Python 中的 self
需要明文写出来而已,至于为什么 Python 被设计成显示使用 self
的方式,请参阅 为什么必须在方法定义和调用中显式使用“self”?
类属性和实例属性
在类中直接声明的属性为类属性,在方法中使用 self.属性
声明出来(一般是在 __init__
方法中)的属性为实例属性。实例属性在每个变量中都不相同,而类属性为类的所有实例所共用。另外,类属性可以由类直接调用,而实例属性则只能由实例对象调用
class MyClass:
a = 'alice'
def __init__(self):
self.b = 10
c1 = MyClass()
c2 = MyClass()
print(MyClass.a) # alice
# print(MyClass.b) # 不能这么做
print(c1.a, c1.b) # alice 10
print(c2.a, c2.b) # alice 10
MyClass.a = 'bob'
c1.b = 20
print(MyClass.a) # bob
print(c1.a, c1.b) # bob 20
print(c2.a, c2.b) # bob 20
这里有一点要注意,在修改类属性 a
的时候,我们是直接使用类 MyClass
进行的修改。如果使用对象 c1
或者 c2
是不能对类属性进行修改的,如果这么做了,只是相当于使用实例属性覆盖了类属性:
class MyClass:
a = 'alice'
c1 = MyClass()
c2 = MyClass()
print(MyClass.a) # alice
print(c1.a) # alice
print(c2.a) # alice
c1.a = 'bob'
print(MyClass.a) # alice
print(c1.a) # bob
print(c2.a) # alice
当类属性和实例属性同名时,实例属性会覆盖类属性(通过 get_cls_a
和 get_obj_a
两个方法可以看到,类属性和实例属性是同时存在的,但是使用对象调用属性时,得到的是实例属性):
class MyClass:
a = 10
print('类属性直接查看:', id(a)) # 4383337392 -> 类属性
def __init__(self):
print('声明实例属性之前:', id(self.a)) # 4383337392 -> 类属性
self.a = 20
print('声明实例属性之后:', id(self.a)) # 4383337712 -> 实例属性
@classmethod
def get_cls_a(cls):
"""这是一个类方法,在后面会讨论它的用法"""
print('类方法查看类属性:', id(cls.a)) # 4383337392 -> 类属性
def get_obj_a(self):
print('实例方法查看实例属性:', id(self.a)) # 4383337712 -> 实例属性
c = MyClass()
c.get_cls_a()
c.get_obj_a()
print('类直接调用属性', id(MyClass.a)) # 4383337392 -> 类属性
print('实例直接调用属性', id(c.a)) # 4383337712 -> 实例属性
静态方法
有时我们创建方法时,并不需要调用实例的其他属性或方法。也就是这个方法是“独立”的,不需要知道类或者对象的任何信息。这时,我们就可以将它声明为一个静态方法,通过内置的 staticmethod
装饰器实现:
class MyClass:
@staticmethod
def longly_func(): # 注意静态方法不需要 self 参数
print('我是静态方法')
MyClass.longly_func() # 类可直接调用静态方法
c = MyClass()
c.longly_func() # 对象也可以调用静态方法
print(id(MyClass.longly_func), id(c.longly_func)) # 4339858336 4339858336 类和对象的静态方法是同一个
从上面代码的运行结果我们可以得出结论:类和对象均可调用静态方法,而且类和对象的静态方法是同一个
类方法
虽然类可以直接调用静态方法,但是类无法直接调用普通的实例方法。而静态方法又是与类的其他部分相隔离的,无法修改类属性。比如下面这个列子中,我们想使用类直接修改类属性 name
,用普通方法和静态方法都是做不到的:
class MyClass:
name = 'alice'
def func(self, name: str):
# 类不能直接调用这个方法
self.name = name
@staticmethod
def longly_func(name: str):
# 没有 self,没办法修改类属性 name
pass
# MyClass.func('bob') # 不能这么用
# MyClass.longly_func('bob') # 改不了类属性
这时,我们就可以使用内置的 classmethod
装饰器将方法声明为类方法,通过 cls
参数(与 self
类似,可以改,但是大家约定俗成使用 cls
作为类方法的第一个参数名)
class MyClass:
name = 'alice'
@classmethod
def change_name(cls, name: str):
print('类方法中:', cls, id(cls)) # <class '__main__.MyClass'> 5215636448
cls.name = name
print('外部:', MyClass, id(MyClass)) # <class '__main__.MyClass'> 5215636448
MyClass.change_name('bob')
print(MyClass.name) # bob
可见,类方法的第一个参数 cls
是类本身,具体用法与普通方法大致相同,只不过操作的对象是类而已
私有属性和方法
很多面向对象的编程语言一般都会有私有属性和方法来控制访问权限,确保这些属性和方法只能在类或对象内部使用,不能作为接口提供给外部。
那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的属性或方法名(例如 _name
)是被保护的(protected),原则上不应被外部调用。这个规则目前是程序员直接约定俗成的共识,不是 Python 语言所强制要求的,但按照官网说法,这种情况“可能不经通知即加以改变”。也就是说,未来或许某个版本的 Python 会强制要求一个下划线开头的被保护的属性和方法不可以被外部调用
提示
实际上,Python 中所有的标识符都遵循这个规则,即以一个下划线开头命名的变量名、函数名、类名、模块名、报名,等等,都是受保护的,不应被外部直接调用
看例子:
class MyClass:
_name = ''
def set_name(self, name: str):
self._name = name
def get_name(self) -> name:
return self._name
c = MyClass()
# 应该这么做
c.set_name('alice')
print(c.get_name())
# 不应这么做
# c._name = 'bob'
# print(c._name)
上面的例子中,直接使用 c._name
也可以实现对属性的查看和修改,但因为 _name
是被保护的,所以日常编程中不应这么做
被保护的属性或方法只是程序员间的共识,是一个君子协定,是可能不被遵守的。但有的时候,我们是需要严格要求某个属性或方法不应被外部调用(例如避免名称与子类所定义的名称相冲突),这时可以将其命名为以双下划线开头的名称(至少带有两个前缀下划线,至多一个后缀下划线,例如 __name
)。这样外部就无法直接通过方法名调用这个属性或方法,这种双下划线开头的属性和方法是我们一般情况口语中 Python 的私有(private)属性和方法:
class MyClass:
__name = 'alice'
c = MyClass()
# 这会报错,找不到属性和方法
# print(c.__name)
但其实 Python 的双下划线开头的属性和方法并非真正的私有,外部还是有办法能获取到的,方法就是使用 ._类名属性名
的方式,比如上面的例子,就可以这样拿到 __name
的值:
class MyClass:
__name = 'alice'
c = MyClass()
print(c._MyClass__name)
总结:
- Python 中以一个下划线开头的属性和方法是被保护(protected)的,实际编程中不应在外部调用
- 以双下划线开头但不以双下划线结尾的属性和方法是私有(private)的,外部无法直接使用原有的属性和方法名调用,但是仍然能通过
._类名属性名
的方式查询出来,实际编程中不应这么用
版权所有
版权归属:Shuo Liu